@font-face{font-family:General;src:url('../fonts/general-400.woff2') format('woff2');font-weight:400;font-display:swap}@font-face{font-family:General;src:url('../fonts/general-500.woff2') format('woff2');font-weight:500;font-display:swap}
:root{--field:#f8f2ef;--paper:#fff;--graphite:#232323;--steel:#69635f;--line:#d9cfca;--accent:#baa595;--accent-deep:#6f594b;--mist:#e8e1dd;--shelf:#d1c3b9;--gutter:clamp(20px,4vw,64px);--max:1600px;--ease:cubic-bezier(.16,1,.3,1)}
*{box-sizing:border-box}/* scroll-behavior:smooth is DELIBERATELY ABSENT here (2026-08-09).
   The operator reported: "Every time I scroll on the site it stops abruptly.
   I can't like swipe real hard to keep it scrolling. Scrolling physics seems
   to have been altered?" — on this site only, of the four. Removing this one
   declaration fixed it, confirmed by the operator on a real device.
   Ruled out first, all measured: document height stable during scroll (height
   churn kills momentum), 0 long tasks, worst frame 28ms, touch-action auto
   everywhere, overscroll-behavior auto on html/body (contain only on
   .mobile-menu), no scroll-snap, no inert overlay under the finger. Headless
   cannot simulate touch momentum, so none of that could have found it — it
   took a feel test on a handset.
   WHY THIS SITE CAN DROP IT AND THE OTHERS CANNOT: every in-page anchor here
   goes through the handler in js/main.js, which passes behavior:'smooth'
   EXPLICITLY (it has to — a sticky holder never releases, so the native hash
   jump sees the target as permanently in view and no-ops; that is what killed
   #top). grass, plumbing and wrenhollis have 12, 7 and 4 in-page anchors and
   NO scrollTo in their JS, so they still need the CSS property; removing it
   there would make every anchor snap instantly.
   Do not add it back without moving those anchors into JS first. */
html{background:var(--field);scrollbar-gutter:stable}body{margin:0;background:var(--field);color:var(--graphite);font-family:General,Arial,sans-serif;line-height:1.5;-webkit-font-smoothing:antialiased;overflow-x:hidden}body.menu-open{overflow:hidden}a{color:inherit}img{display:block;width:100%;height:auto}.skip{position:fixed;z-index:300;left:16px;top:8px;padding:10px 14px;background:var(--accent);color:#fff;transform:translateY(-150%)}.skip:focus{transform:none}.site-header{position:absolute;z-index:100;inset:0 0 auto;max-width:var(--max);height:72px;margin:auto;padding:0 var(--gutter);display:flex;align-items:center;justify-content:space-between;color:#fff;border-bottom:1px solid rgba(255,255,255,.26)}.brand{display:flex;align-items:center;gap:11px;font-size:14px;font-weight:500;text-decoration:none;letter-spacing:-.02em}.brand span{display:grid;place-items:center;width:28px;aspect-ratio:1;border:1px solid currentColor;font-size:13px}.desktop-nav{display:flex;align-items:center;gap:30px;font-size:12px}.desktop-nav a{text-decoration:none}.nav-cta{margin-left:8px;padding-bottom:3px;border-bottom:1px solid currentColor}.menu-button,.mobile-menu{display:none}.hero{position:relative;isolation:isolate;height:100vh;min-height:620px;color:#fff;overflow:hidden}.hero-image,.hero-shade{position:absolute;z-index:-2;inset:0;width:100%;height:100%}.hero-image{object-fit:cover;object-position:center}.hero-shade{z-index:-1;background:linear-gradient(90deg,rgba(17,23,22,.8) 0%,rgba(17,23,22,.35) 48%,rgba(17,23,22,.12) 75%),linear-gradient(0deg,rgba(17,23,22,.35),transparent 50%)}.hero-copy{position:absolute;left:max(var(--gutter),calc((100vw - var(--max))/2 + var(--gutter)));bottom:clamp(62px,10vh,108px);max-width:780px}.hero-kicker{margin:0 0 27px;font-size:11px;letter-spacing:.09em;text-transform:uppercase}.hero h1{max-width:8ch;margin:0;font-size:clamp(3.7rem,7.7vw,8rem);font-weight:500;line-height:.88;letter-spacing:-.065em;text-wrap:balance}.hero-copy>p:not(.hero-kicker){max-width:46ch;margin:27px 0 31px;font-size:clamp(1rem,1.3vw,1.22rem)}.button{display:inline-flex;align-items:center;gap:32px;padding:14px 17px;border:1px solid var(--graphite);background:var(--graphite);color:#fff;text-decoration:none;font-size:13px;transition:transform .25s var(--ease),background .25s}.button span{color:var(--accent)}.button:hover{transform:translateY(-2px)}.button:active{transform:scale(.98)}.button.light{background:#fff;color:var(--graphite);border-color:#fff}.proof{max-width:var(--max);margin:auto;padding:clamp(110px,14vw,210px) var(--gutter);display:grid;grid-template-columns:12% minmax(0,8fr) 3fr;gap:5vw}.proof-mark{margin:10px 0;font-size:11px}.proof>div{grid-column:2}.proof h2,.practice-intro h2,.matter-content>h2,.work-lead h2,.contact h2{margin:0;font-size:clamp(2.8rem,5.4vw,5.8rem);font-weight:500;line-height:.92;letter-spacing:-.055em;text-wrap:balance}.proof h2{max-width:11ch}.proof>div p{max-width:46ch;margin:52px 0 0 auto;font-size:clamp(1.05rem,1.4vw,1.25rem)}.practices{padding:clamp(90px,10vw,150px) var(--gutter);background:var(--paper)}.practice-intro,.practice-list{max-width:var(--max);margin:auto}.practice-intro{width:84%;margin-left:8%;margin-bottom:80px}.practice-intro>p,.work-lead>p{margin:0 0 24px;font-size:11px;letter-spacing:.08em;text-transform:uppercase}.practice-intro h2{max-width:10ch}.practice-list{border-top:1px solid var(--line)}.practice-row{position:relative;display:grid;grid-template-columns:60px minmax(280px,1.2fr) 1fr 28px;gap:28px;align-items:center;padding:32px 0;border-bottom:1px solid var(--line);outline:none;transition:padding .35s var(--ease)}.practice-row:before{content:"";position:absolute;left:0;width:3px;height:0;background:var(--accent);transition:height .35s var(--ease)}.practice-row:hover,.practice-row:focus{padding-left:14px}.practice-row:hover:before,.practice-row:focus:before{height:55%}.practice-row>span{font-size:11px}.practice-row h3{margin:0;font-size:clamp(1.7rem,3vw,3.25rem);font-weight:500;line-height:1;letter-spacing:-.035em}.practice-row p{max-width:42ch;margin:0;color:var(--steel);font-size:14px}.practice-row i{font-style:normal;color:var(--accent)}.matterline{position:relative;max-width:var(--max);margin:auto;padding:clamp(100px,13vw,190px) var(--gutter);display:grid;grid-template-columns:minmax(0,5fr) minmax(0,6fr);gap:clamp(55px,9vw,150px)}.matter-visual{align-self:start;margin-left:-12%;position:sticky;top:90px}.matter-visual img{height:min(66vh,720px);object-fit:cover}.matter-visual p{margin:18px 0 0 12%;font-size:12px}.matter-content>h2{max-width:9ch;margin-bottom:80px}.matter-content{position:relative}.matter-progress{position:absolute;left:-44px;top:clamp(240px,25vw,340px);bottom:0;width:14px}.matter-progress:before,.matter-progress i{content:"";position:absolute;left:6px;top:0;width:1px;height:100%;background:var(--line)}.matter-progress i{height:var(--matter-progress,0%);background:var(--accent);transition:height .6s var(--ease)}.matter-progress b{position:absolute;top:var(--docket-top,0%);left:-12px;display:grid;place-items:center;width:38px;aspect-ratio:1;background:var(--accent);color:#fff;font-size:10px;font-weight:500;transform:translateY(-50%);transition:top .6s var(--ease)}.matter-stages{margin:0;padding:0;list-style:none}.matter-stages li{min-height:210px;display:grid;grid-template-columns:46px 1fr;padding:26px 0;border-top:1px solid var(--line);opacity:.3;transform:translateX(8px);transition:opacity .5s,transform .5s var(--ease)}.matter-stages li.is-active{opacity:1;transform:none}.matter-stages span{font-size:11px}.matter-stages h3{margin:0;font-size:clamp(1.8rem,3vw,3.2rem);font-weight:500;letter-spacing:-.04em}.matter-stages p{max-width:38ch;margin:15px 0;color:var(--steel)}.work{padding:clamp(100px,12vw,180px) var(--gutter);background:var(--mist)}.work-lead,.featured-matter,.matter-notes{max-width:var(--max);margin-inline:auto}.work-lead{width:75%;margin-left:16%;margin-bottom:90px}.work-lead h2{max-width:11ch}.featured-matter{min-height:430px;padding:clamp(34px,5vw,74px);display:grid;grid-template-columns:1fr auto;gap:40px;align-items:end;background:var(--accent);color:#fff}.featured-matter span,.matter-notes span{font-size:10px;letter-spacing:.08em;text-transform:uppercase}.featured-matter h3{max-width:15ch;margin:36px 0 20px;font-size:clamp(2.3rem,4.6vw,5rem);line-height:.94;letter-spacing:-.05em}.featured-matter p{max-width:44ch}.featured-matter a{font-size:13px;text-underline-offset:5px}.matter-notes{display:grid;grid-template-columns:1fr 1fr;margin-top:20px;margin-left:7%;gap:20px}.matter-notes article{padding:34px;border-top:1px solid var(--graphite)}.matter-notes h3{margin:45px 0 12px;font-size:clamp(1.6rem,2.5vw,2.6rem);font-weight:500;letter-spacing:-.035em}.matter-notes p{color:var(--steel)}.contact{padding:clamp(100px,13vw,190px) var(--gutter);background:var(--paper)}.contact>p:first-child{margin:0 0 55px;font-size:11px}.contact h2{max-width:10ch;margin-left:8%}.contact>a{display:flex;justify-content:space-between;gap:30px;width:84%;margin:80px 0 0 16%;padding:28px 0;border-top:1px solid var(--graphite);border-bottom:1px solid var(--graphite);font-size:clamp(1.3rem,2.6vw,2.6rem);text-decoration:none;letter-spacing:-.03em}.contact>a span{color:var(--accent)}.concept-note{margin:25px 0 0 16%;font-size:11px;color:var(--steel)}/* OCTOBER26 FOOTER ALIGNMENT. lawyer2 ships `2fr 1fr 1fr auto`, authored for the
   FOUR items it puts below .footer-hub (brand, blurb, legal nav, "Concept / 2026").
   This site has THREE — there is no concept line, because inventing one would be
   a claim about a real business. With three items the `auto` track collapses to
   0px, measured at 1440x900: the row then ended at x=1327 while .footer-hub above
   it ended at 1368, and the brand track inflated to 595px for a 17-character
   wordmark. Neither lined up with the hub, whose three columns sit at x=58/508/958.
   Matching the hub's own grid puts brand, blurb and nav directly under the three
   link columns and squares the right edge. The <=899px and <=599px overrides
   already mirror .footer-hub (1fr 1fr, then 1fr), so only this base rule diverged. */
footer{padding:70px var(--gutter) 40px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:40px;background:var(--graphite);color:#fff;font-size:12px}footer nav{display:flex;gap:18px;align-items:center;align-self:center}.reveal{opacity:1;translate:none}.js .reveal{opacity:0;translate:0 20px;transition:opacity .7s var(--ease),translate .7s var(--ease)}.js .reveal.is-visible{opacity:1;translate:none}
@media(max-width:899px){.site-header{height:64px}.desktop-nav{display:none}.menu-button{display:block;border:1px solid currentColor;background:transparent;color:inherit;padding:8px 12px}.mobile-menu{position:fixed;z-index:90;inset:64px 0 auto;display:grid;padding:16px 20px;background:var(--paper);color:var(--graphite)}.mobile-menu[hidden]{display:none}.mobile-menu a{padding:13px 0;border-bottom:1px solid var(--line);text-decoration:none}.hero{min-height:640px}.hero-image{object-position:58% center}.proof{grid-template-columns:40px 1fr}.proof>div{grid-column:2}.practice-row{grid-template-columns:44px 1fr 24px}.practice-row p{grid-column:2}.practice-row i{grid-column:3;grid-row:1}.matterline{grid-template-columns:1fr}.matter-visual{position:relative;top:auto;width:88%;margin-left:12%}.matter-content{width:90%}.matter-progress{left:auto;right:-24px}.work-lead{width:88%;margin-left:12%}.featured-matter{grid-template-columns:1fr}.matter-notes{margin-left:4%}.contact>a{width:92%;margin-left:8%}footer{grid-template-columns:1fr 1fr}}
@media(max-width:599px){.hero-copy{left:20px;right:20px;bottom:72px}.hero h1{font-size:clamp(3.65rem,18vw,5.1rem);max-width:7ch}.hero-copy>p:not(.hero-kicker){max-width:28ch}.hero-image{object-position:65% center}.proof{padding-inline:20px;grid-template-columns:26px 1fr}.proof h2{font-size:clamp(2.9rem,13vw,4.1rem)}.proof>div p{margin-top:35px}.practices{padding-inline:20px}.practice-intro{width:92%;margin-left:8%;margin-bottom:55px}.practice-row{grid-template-columns:30px 1fr 18px;gap:12px;padding:26px 0}.practice-row p{font-size:13px}.matterline{padding-inline:20px}.matter-visual{width:92%;margin-left:8%}.matter-visual img{height:58vh}.matter-content{width:94%}.matter-content>h2{font-size:clamp(3rem,13vw,4.2rem)}.matter-progress{right:-15px}.matter-stages li{grid-template-columns:34px 1fr}.work{padding-inline:20px}.work-lead{width:92%;margin-left:8%;margin-bottom:58px}.featured-matter{min-height:470px;padding:30px 24px}.matter-notes{grid-template-columns:1fr;margin-left:7%}.matter-notes article{padding:28px 0}.contact{padding-inline:20px}.contact h2{margin-left:6%;font-size:clamp(3rem,13vw,4.3rem)}.contact>a{width:94%;margin:60px 0 0 6%;font-size:1.3rem;overflow-wrap:anywhere}.concept-note{margin-left:6%}footer{grid-template-columns:1fr;padding-inline:20px}}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}.js .reveal{opacity:1;translate:none;transition:none}.matter-stages li{opacity:1;transform:none}.matter-progress i{height:100%;transition:none}.matter-progress b{display:none}.button,.practice-row{transition:none}}
.hero h1{max-width:12ch}
@media(max-width:599px){.hero h1{max-width:7ch}}

/* Video-led hero with an oversized, edge-anchored identity. */
.site-header{border-bottom:0}
.hero-image,.hero-video{position:absolute;z-index:-2;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;transition:opacity .5s ease}
.hero-video{opacity:0}.hero-video.is-playing{opacity:1}
.hero-shade{background:linear-gradient(90deg,rgba(12,17,16,.62),rgba(12,17,16,.12) 58%),linear-gradient(0deg,rgba(12,17,16,.58),transparent 54%)}
/* ── THE HERO COPY AND THE WORDMARK ARE ANCHORED TO OPPOSITE EDGES, AND THAT
      IS WHY THEY COLLIDED ───────────────────────────────────────────────────
   The operator's report was "there is no margin between this button and the
   Northline logo". There is no margin because there is no RELATIONSHIP: these
   are two absolutely-positioned boxes inside the same 100vh .hero, one pinned
   to the top and one to the bottom, neither aware of the other's extent.
     .hero-copy      top-anchored, grows DOWNWARD with its content
     .hero-wordmark  bottom-anchored, height .75 x font-size, and the font-size
                     scaled on 16vw — so it grows UPWARD with viewport WIDTH
   Whether they touch was therefore a pure function of viewport ASPECT RATIO,
   which is exactly the quantity neither rule mentioned. Adding a margin
   somewhere could not have fixed it; the sizes had to learn about height.

   IT WAS A COLLISION, NOT MERELY A TIGHT GAP, AND IT WAS MEASURED AS ONE.
   The gap that matters is to the wordmark's real INK, not to its border box:
   line-height is .75 on a font whose cap sits high, so the glyphs start below
   the box top and the box number flatters the layout.
   HOW THE INK BOX IS TAKEN, because getting this wrong is what produced the
   wrong numbers this note replaces. Canvas measureText, with BOTH corrections
   or the horizontal half is nonsense: ctx.letterSpacing set from the computed
   style, and the string UPPER-CASED to match text-transform:uppercase.
   Measuring the untransformed "Northline" understates the rendered width by
   180-240px, which reads as horizontal clearance that is not there. Model
   cross-checked against a pixel scan of the served screenshot with the
   wordmark forced to #f0f: at 1536x620, 1600x850, 1280x800, 390x844, 1100x640
   and 1024x768 the two agree to 1.1px on ink top, 1.1px on ink left and 2.2px
   on ink right.
   THE GRID, stated because "144 cells" without it is not reproducible:
   18 widths x 8 heights = 144. Widths 320 360 390 414 600 768 900 1024 1100
   1280 1366 1440 1512 1536 1600 1728 1920 2560; heights 620 640 768 800 844
   850 900 1080. A cell COLLIDES only when the button box and the ink box
   overlap on BOTH axes — vertical overlap alone is not a collision, because
   the button is left-aligned in the gutter and the wordmark is centred.
   Swept at git HEAD, before any of the rules below:
       44 of 144 cells collided; worst -218.9px of mutual penetration at
       1920x640; the button's lower border sat inside the cap band at
       1600x850 (-14.3px) and 1920x900 was down to +6.1px.
   The negative band covered ordinary machines, not exotic ones: 1440x800,
   1600x850 and 1920x900 are MacBook- and 1080p-class viewports.
   The skip control is what finally consumed the slack: it adds 78px to
   .hero-copy at 1920x1080 (44px box + 34px margin) and 63.8px at 1100x620
   (44 + min(34px,3.2vh) = 19.84).

   THE FIX GIVES ALL FOUR TERMS A HEIGHT CEILING, rather than moving one box.
   `min(<vw or px>, <vh>)` inside the existing clamp is the idiom this file
   already uses for exactly this problem (see .bench-head's
   `margin-top:min(34px,2.4vh)` in §THE LANDING): the width-driven value keeps
   its say on tall viewports and the vh term takes over as the viewport gets
   short, continuously, with no breakpoint to jump at.

   THE CEILINGS ALONE LEFT A COLLISION BAND, AND THAT IS WHY TWO MORE TERMS
   BELOW GAINED ONE. With only the four ceilings, the 144-cell grid dropped to
   ONE colliding cell — 1100x620, -4.7px vertical and -11.5px horizontal, the
   button's right border inside the "N"'s left stem. A 5px-resolution sweep of
   940-1240 wide x {600 610 620 630 640 650 660 680 700} = 549 cells found it
   was a band, not a point: 18 colliding cells spanning 1075-1135 wide by
   600-630 tall, worst -7.9 vertical / -2.3 horizontal at 1120x620.
   So `.hero-copy{top:...}` takes its clamp FLOOR from 96px to 84px and the
   standfirst's bottom margin becomes min(26px,3vh). Both are one-sided: the
   floor can only bind where 14vh < 96px, i.e. below 686px of viewport height,
   and 3vh can only bind below 867px, so every taller viewport is untouched to
   the value. Together they take 16.6px off .hero-copy's bottom at 1100x620
   (523.1 -> 506.5), 2.0px at 1280x800, 0.7px at 390x844 and 0 at 1440x900.
   Measured on the shipped build, same two sweeps: 0 collisions in 144 and 0
   in 549. The number that matters is the separation at cells that DO overlap
   horizontally, since only those can collide: +11.9px at 1100x620 over the
   grid, +8.7px at 1120x620 over the band. The global minimum vertical
   separation is -4.5px at 1512x620, and that is NOT a collision — the ink
   starts 178px to the right of the button there.
   Vertical separation, git HEAD -> ceilings only -> shipped:
       1600x850  -14.3 -> +75.6 -> +76.1    1920x900   +6.1 -> +94.1 -> +94.1
       1440x800  -10.1 -> +63.5 -> +65.5    1280x800  +40.5 -> +95.1 -> +97.1
       1024x768  +85.4 ->+117.8 ->+120.8     390x844 +257.8 ->+258.8 ->+259.5
       1366x640 -128.3 ->  -4.5 ->  +8.7    1100x620  -68.9 ->  -4.7 -> +11.9
   THE FLOOR DROP DOES NOT PUT THE COPY UNDER THE HEADER, which is the only
   thing the 96px was buying: .hero-copy's top stays at least 14.8px below the
   72px header's bottom edge over the grid (worst 900x620) and 12.0px over the
   band (worst 940x600).
   NOTHING BELOW .hero MOVES. Section offsetTops and scrollHeight are equal to
   the value between the two builds at 1280x800, 1440x900, 390x844 and
   1100x620 — .hero is height:100vh, so shortening its absolutely-positioned
   copy block cannot reach the flow.
   The 1920x1080 cell moves +169.1 -> +201.6 and is unchanged by the floor
   drop (14vh = 151.2px there, nowhere near 96). It is the one place the
   ceilings change a layout that was not broken: the wordmark goes 256px ->
   237.6px. Checked visually at that size rather than by number — it still
   reads as the oversized edge-anchored identity this block's heading claims,
   though "runs past the right gutter" would be wrong: its ink spans
   382.6-1561.5, ending 294.5px inside the 64px right gutter. */
.hero-copy{top:clamp(84px,14vh,170px);bottom:auto;max-width:560px}
.hero h1{max-width:9ch;font-size:clamp(3.4rem,min(6vw,11vh),6.5rem)}
.hero-copy>p:not(.hero-kicker){max-width:34ch;margin-block:22px min(26px,3vh)}
/* The control is the LAST child of .hero-copy, so its top margin is the final
   term in the block's height and the last thing between it and the wordmark.
   Scoped to .hero-copy: .proof's control has a whole section to sit in and
   must keep the flat 34px. */
.hero-copy .next-jump{margin-top:min(34px,3.2vh)}
.hero-wordmark{position:absolute;left:50%;bottom:.015em;width:100%;margin:0;transform:translateX(-50%);font-size:clamp(7rem,min(16vw,22vh),16rem);font-weight:500;line-height:.75;letter-spacing:-.075em;text-align:center;text-transform:uppercase;white-space:nowrap;color:#fff}

/* The 22vh ceiling is repeated here because this block RESTATES font-size
   rather than inheriting it; a width term alone would put the width-only
   behaviour straight back below 900px. The 599 block below restates it again
   and deliberately does NOT carry a vh term — see the note there.
   THE WIDTH TERM IS FITTED TO THE WORD, NOT A ROUND vw. "MIKEHOSTING" at this
   face, weight and -.075em tracking sets 6.03x its font-size wide (measured
   2026-11-02, Chromium; 347.5px at 57.6px), so the old 19vw (and 18vw below)
   made the word 1.15x / 1.09x the viewport and cropped it at EVERY width under
   900: 429px of word in a 390px phone, 896 in a 768 tablet. Dividing the
   gutter-to-gutter width by 6.1 (6.03 plus slack for engine font-metric
   drift) fits the whole word with the page's own --gutter on each side. */
@media(max-width:899px){.hero-copy{top:112px}.hero-wordmark{bottom:.04em;font-size:min(calc((100vw - 2 * var(--gutter)) / 6.1),22vh)}}
/* NO vh CEILING ON THE PHONE WORDMARK, DELIBERATELY. The width term (see the
   899 block above for why it is a fit to the word) is a small absolute number
   on a phone (57.4px at 390 wide; it was 70.2px at 18vw, which cropped the
   word) and the hero has no index label any more (MH / 01 removed
   2026-11-02), so the bottom of the hero is uncrowded. The figures that follow
   were taken at the old 18vw, i.e. with a LARGER word, so they are a floor: measured separation at 390x844
   is 259.5px, and the whole 390 column of the sweep stays clear — its worst
   cell is 390x620 at +68.1px, still five times the +11.9px that the tightest
   desktop cell gets. A vh term would only shrink the identity on short phones
   to buy clearance the phone already has. This rule must also stay AFTER
   the 899 block above, which is where it gets to win — the two have equal
   specificity, so source order is the whole of the override. */
@media(max-width:599px){.hero-copy{top:105px;bottom:auto}.hero h1{max-width:8ch;font-size:clamp(3.25rem,15vw,4.7rem)}.hero-wordmark{left:50%;bottom:20px;width:100%;transform:translateX(-50%);font-size:calc((100vw - 2 * var(--gutter)) / 6.1);text-align:center}.hero-image,.hero-video{object-position:52% center}}
@media(prefers-reduced-motion:reduce){.hero-video{display:none}.hero-image{opacity:1}}

/* Dynacap-derived folio transitions: one logical end corner, never four. */
main{background:var(--accent)}
.skip{background:var(--accent-deep)}
.hero,.proof,.practices,.matterline,.work,.bench,.contact{position:relative;border-end-end-radius:clamp(28px,3.35vw,48px);overflow:clip}
/* The per-class section backgrounds that used to sit here — .hero graphite,
   .proof/.matterline field, .practices/.contact paper, .work mist — are gone.
   Colour is now a function of POSITION; see POSITIONAL TONE BANDS below. */
.practice-row:before{background:var(--accent-deep)}
.practice-row i,.button span,.contact>a span{color:var(--accent-deep)}
.matter-progress i,.matter-progress b{background:var(--accent-deep)}
.featured-matter{background:var(--accent-deep);border-end-end-radius:clamp(24px,3vw,44px);overflow:clip}
.matter-visual img{border-end-end-radius:clamp(24px,3vw,44px)}
.button:focus-visible,.practice-row:focus-visible,.contact>a:focus-visible{outline:3px solid var(--accent-deep);outline-offset:4px}
@media(max-width:599px){.hero,.proof,.practices,.matterline,.work,.bench,.contact{border-end-end-radius:28px}.featured-matter,.matter-visual img{border-end-end-radius:24px}}

/* Each folio corner reveals the actual surface of the following section.
   THE RESET TO 0 IS NOT "NO CORNER", IT IS "NOT WITH A REAL RADIUS". A real
   border-end-end-radius clips to whatever is BEHIND the section, which is
   <main>, and the folio corner has to show the NEXT SECTION's own colour — so
   the curve is handed to the ::after below, which paints --next-bg. That is
   still true at rest and this line stays.
   ONE EXCEPTION, WRITTEN FROM JS AND ONLY WHILE A LIFT IS LIVE. box-shadow
   casts from the border box, so a square box under a painted round corner gave
   the lid-lift shadow a right-angled corner off a curve. js/main.js §THE
   SHADOW'S SILHOUETTE writes the real radius inline for exactly as long as it
   writes the shadow, which is exactly as long as §THE GROUND BEHIND THE LID
   holds <main> at the next section's colour — so during a lift the clip and
   the ::after reveal the SAME colour and the corner's paint is unchanged. The
   radius it writes is measured off the ::after's used width, so --folio-radius
   below is still the single source of that number; do not restate it there. */
main{background:var(--field)}
.hero,.proof,.practices,.matterline,.work,.bench,.contact{--folio-radius:clamp(28px,3.35vw,48px);border-end-end-radius:0}
/* ── WHEN THE CORNER PAINTS, AND WHEN IT DOES NOT ─────────────────────────
   The corner is a PAGE LIFTING TO SHOW THE PAGE UNDERNEATH. Paper lifting to
   reveal paper is a page turn. Paper with a dark hole bitten out of it is
   damage — which is exactly what the operator reported as "a slight black
   outline on the bottom right corner": .contact is near-white, the footer is
   graphite, and its corner previewed the footer as a graphite quarter-disc.
   Measured on the live page: corner pixel rgb(35,35,35) on a section of
   rgb(255,255,255).

   The asymmetry is DIRECTION, not magnitude. The hero corner is the same ΔE00
   78 step the other way round — a LIGHTER corner out of a dark video plate —
   and it reads correctly, as a rounded media corner. So the rule is
   directional and binary: the corner paints when the next surface is lighter
   than, or close to, this one, and is suppressed where the next surface is
   materially DARKER.

   --folio-on is written by syncFolioSurfaces() in js/main.js and authored into
   the generated HTML by src/render.mjs, so the suppression is correct with JS
   off too. The gate is a luminance contrast ratio of 2.0 in the darker
   direction; every real seam on the site is CR <= 1.55 except the two ~ΔE00-78
   ones, and graphite-behind-light is CR >= 9.1, so the threshold sits in a gap
   two orders of magnitude wide and cannot flicker on a client edit. Do NOT
   scale the radius continuously by ΔE00: the hero corner is ΔE00 78 and must
   stay full size.

   THE ALTERNATION DOES NOT FIX THIS ON ITS OWN, and that was checked before
   any change: design.md line 34 fixes the footer as graphite by fiat, so the
   last corner's target is graphite whatever parity assigns to the last
   section. Every light tone in the system is ΔE00 >= 66 / CR >= 9.1 from
   graphite. Alternation moves the defect by at most 12 ΔE00 out of 79. */
.hero:after,.proof:after,.practices:after,.matterline:after,.work:after,.bench:after,.contact:after{content:"";position:absolute;z-index:8;right:0;bottom:0;width:calc(var(--folio-radius) * var(--folio-on,1));height:calc(var(--folio-radius) * var(--folio-on,1));pointer-events:none;background:radial-gradient(circle at 0 0,transparent calc(var(--folio-radius) - 1px),var(--next-bg) var(--folio-radius))}
@media(max-width:599px){.hero,.proof,.practices,.matterline,.work,.bench,.contact{--folio-radius:28px}}

/* ══════════════════════════════════════════════════════════════════════════
   POSITIONAL TONE BANDS
   The sequence is a property of POSITION and never of a section's class.
   js/main.js walks the RENDERED direct-child <section>s of <main> and stamps
   data-band by index parity; src/render.mjs authors the identical attributes
   into the generated HTML, so the no-JS render is already correct and JS only
   ever RE-derives — after a real DOM change, never on a scroll frame. A client
   reordering, hiding or stashing a section in the AI editor therefore still
   gets an alternating page with no adjacent repeat.

   WHY IT EXISTS. Measured at rest, JS blocked: four of the six home seams sat
   at ΔE00 3.65–6.93 / CR <= 1.29, and the eighteen interior routes were worse
   still — .page-head and .page-body were both --field, ΔE00 0.00, no seam at
   all, and the three index routes had two 0.00 seams each. A lid lifting off a
   plane the reader cannot distinguish from the one beneath it reads as
   nothing, which is precisely the operator's report.

   THE THRESHOLD — two gates, and every adjacent RENDERED pair must clear both.
     ΔE00 >= 10.0  ~1 ΔE00 is the classic JND for two flat patches held side by
                   side under ideal viewing, and that is the wrong task. Here
                   the reader judges a MOVING edge, peripherally, on a hinge
                   that casts no shadow. The figure usually quoted for "noticed
                   at a glance, with no reference patch" is ~5; a moving edge
                   wants roughly double that headroom, and 10 also survives
                   8-bit sRGB banding and uncalibrated laptop gamma. It is
                   calibrated against evidence rather than taste: 3.65–6.93 is
                   the range this operator demonstrably could not see.
     CR   >= 1.45  ΔE00 alone can be earned by hue at equal lightness, and a
                   hue-only step is exactly what a shadowless lid edge will NOT
                   show. This second gate forces the separation to be a
                   LIGHTNESS step. 1.45 is roughly half of WCAG's 3:1 non-text
                   minimum, which is the right order of magnitude: the reader
                   is not identifying a control, only seeing that a plane moved.
   Measured, every adjacent rendered pair, in ANY arrangement:
     shelf <-> light   ΔE00 11.21  CR 1.550    (was 0.00–6.93 / 1.00–1.29)
     plate <-> light   ΔE00 78.11  CR 14.174
     plate <-> shelf   ΔE00 64.44  CR  9.140
     light <-> footer  ΔE00 78.11  CR 14.174
     shelf <-> footer  ΔE00 64.44  CR  9.140
   Because the rule is PARITY, no reorder or hide can ever place two light
   bands together, so 11.21 / 1.550 is the worst case for EVERY arrangement,
   not only the shipped one.

   NO TINT SPLIT inside the light band, unlike grass, which alternates two
   creams there. Both candidates fail here and were measured: --paper #fff puts
   a pure-white full viewport on the page, which design.md line 25 forbids
   outright, and --mist #e8e1dd sits ΔE00 6.5 / CR 1.28 from shelf — under both
   gates. One light tone means one worst case, provable everywhere, and it
   returns --paper to the "navigation and restrained reading surfaces only"
   role design.md actually assigns it.

   --shelf #d1c3b9 is the only new value in the system: --mist mixed 50% toward
   --accent, so it is on the existing warm-stone axis rather than a new hue,
   and it reads as FOLDER STOCK against SHEET STOCK — which is what "Matterline
   Folio", the folio corners and the docket index were already saying.
   design.md line 30 permits stone as a large transition field and line 31
   forbids --accent-deep as a large ground; this is the former, not the latter.
   The only lighter value that still clears both gates is #d3c6bd (ΔE00 10.36,
   CR 1.505) — a margin of 0.36 over the ΔE00 gate, which is not shippable.

   THE PLATE IS NOT "THE HERO". It is any section that supplies its own opaque
   full-bleed media, marked data-plate by the builder that knows it does. It
   still TAKES ITS PARITY SLOT — hiding it re-alternates everything after it —
   but its paint is graphite, because a tone assigned to a surface the reader
   never sees is a lie that the folio corner, --next-bg and the lid's ground
   colour would then all consume. Graphite is also the only safe ground under
   the white hero type if the media ever fails to load: --shelf under #fff is
   1.72:1.
   ══════════════════════════════════════════════════════════════════════════ */
main>[data-band="shelf"]{background:var(--shelf)}
main>[data-band="light"]{background:var(--field)}
main>[data-band="plate"]{background:var(--graphite);color:#fff}
/* THE BAND OWNS ITS TEXT TOKENS, exactly as grass's bands own --rule, because
   the ground moved and the tokens were tuned to the old one. On --shelf,
   --steel #69635f is 3.44:1 — a fail for the 14px copy in .practice-row p,
   .matter-stages p, .faq-item p, .index-row p and .concept-note — and --line
   #d9cfca is 1.12:1, a hairline that disappears. These three restore the
   field values' measured relationships on the darker ground:
     --steel       #554e4a  4.74:1 on shelf  (5.33:1 on field)
     --accent-deep #5f4c3c  4.73:1 on shelf  (5.91:1 on field), and white on it
                            is 8.12:1 where .featured-matter / .answer-first
                            use it as their own ground
     --line        #baa595  1.371 on shelf   (1.380 on field), so hairline
                            weight is constant down the page
   Nothing carrying text anywhere on the site is now under 4.5:1; graphite on
   shelf is 9.14:1. The one thing to re-check if a component moves: --accent
   #baa595 is 1.37:1 on shelf and is not used as text on any shelf section. */
main>[data-band="shelf"]{--steel:#554e4a;--line:#baa595;--accent-deep:#5f4c3c}

/* ─────────────────────────────────────────────────────────────────────────
   SEO build additions. Everything below is additive, and what keeps the home
   page pixel-identical to the hand-written original is that no rule above
   this line changes a value for any element the hand-written page had. Bump
   css/main.css?v= in src/render.mjs on any edit here — Cloudflare caches this
   file.

   THE CLAIM USED TO BE THE STRONGER "no rule above this line was edited", and
   at ?v=17 that stopped being true: `.bench` was added to the four folio
   selector lists above (the radius/clip rule, the --folio-radius reset, the
   ::after that paints --next-bg, and the 599px radius override). It is the
   weaker claim that was ever doing the work — .bench is a class the
   hand-written page does not contain, so every one of those rules resolves to
   the identical computed value on every element that existed before it, which
   was re-checked by diffing this file against the ?v=16 bytes with the four
   `.bench` tokens stripped: lines 1-196 are byte-identical. The alternative
   was restating the ::after gradient a fifth time under §THE BENCH, i.e. two
   copies of the site's signature transition that a later edit could silently
   desynchronise. DRY won, and the claim got honest instead.
   ───────────────────────────────────────────────────────────────────────── */

/* .practice-row is now an <a> so the home page's authority flows into the four
   practice pages. These two declarations are the whole cost of that change:
   the anchor's default underline off, colour inherited as before. */
.practice-row{text-decoration:none;color:inherit}
/* Category rows carry no arrow: the page count takes the last column, set like
   the row index ("01") so the two numbers bracket each row; it picks up the
   accent on hover/focus the way the arrow used to. */
.practice-row{grid-template-columns:60px minmax(280px,1.2fr) 1fr auto}
.practice-count{justify-self:end;font-size:11px;font-weight:400;letter-spacing:.08em;text-transform:uppercase;white-space:nowrap;color:var(--steel);font-variant-numeric:tabular-nums;transition:color .25s}
.practice-row:hover .practice-count,.practice-row:focus-visible .practice-count{color:var(--accent-deep)}
@media(max-width:899px){.practice-row{grid-template-columns:44px 1fr auto}.practice-count{grid-column:3;grid-row:1}}
@media(max-width:599px){.practice-row{grid-template-columns:30px 1fr auto}}

/* The footer link hub — three link columns above the existing footer row, in
   the same graphite ground and the same 12px type. It is the internal-linking
   hub: every page reaches all four practices, all three areas and all six
   guides in one hop. */
.footer-hub{grid-column:1/-1;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:40px;margin-bottom:0;padding-bottom:40px;border-bottom:1px solid rgba(255,255,255,.16)}
.footer-col h2{margin:0 0 20px;font-size:11px;font-weight:400;letter-spacing:.08em;text-transform:uppercase;color:rgba(255,255,255,.55)}
.footer-col ul{display:grid;gap:10px;margin:0;padding:0;list-style:none}
.footer-col a{text-decoration:none;color:rgba(255,255,255,.86);transition:color .25s}
.footer-col a:hover{color:#fff}
@media(max-width:899px){.footer-hub{grid-template-columns:1fr 1fr;gap:32px}}
@media(max-width:599px){.footer-hub{grid-template-columns:1fr;gap:28px;padding-bottom:32px}}

/* The header is white type because on the home page it floats over the dark
   video hero. Interior routes open on a light field, so body.page-shell flips
   it to graphite. Home carries no body class and is untouched — the pixel
   diff proves it. */
.page-shell .site-header{color:var(--graphite)}
.page-shell .nav-cta{border-bottom-color:var(--accent-deep)}

/* ── interior routes ──────────────────────────────────────────────────────
   FIRST PASS, written with the pipeline and to be verified against real pages
   in the content stage. Nothing here can reach the home page: every selector
   below is a class the home page does not use.

   Each component is derived from an existing home component rather than
   invented (see SEO.md): .page-head from .hero-copy at the section-statement
   ramp, .crumbs from .hero-kicker utility type, .index-row from .practice-row
   exactly, .prose-block from .matter-stages, .answer-first from
   .featured-matter, .page-faq from .matter-stages records, .includes and
   .related from .matter-notes. No accordion, no equal cards, no shadows.

   js/main.js derives each folio corner's --next-bg from the NEXT top-level
   <section> of <main>, so every interior block is a <section> with an opaque
   background and joins the folio rule below. */
.page-head,.index-list,.page-body,.page-faq,.related{position:relative;--folio-radius:clamp(28px,3.35vw,48px);overflow:clip;padding:clamp(70px,9vw,130px) var(--gutter)}
.page-head:after,.index-list:after,.page-body:after,.page-faq:after,.related:after{content:"";position:absolute;z-index:8;right:0;bottom:0;width:calc(var(--folio-radius) * var(--folio-on,1));height:calc(var(--folio-radius) * var(--folio-on,1));pointer-events:none;background:radial-gradient(circle at 0 0,transparent calc(var(--folio-radius) - 1px),var(--next-bg) var(--folio-radius))}
@media(max-width:599px){.page-head,.index-list,.page-body,.page-faq,.related{--folio-radius:28px}}
/* Interior backgrounds are positional too, and this is where the old per-class
   map hurt most: .page-head and .page-body were BOTH --field, so every service,
   area and guide route had an adjacent pair at ΔE00 0.00 — a folio corner with
   literally nothing to reveal — and the three index routes had two such seams
   each. Deleting the map fixes all eighteen routes at once; a per-class colour
   map would have had to be maintained per component forever. */

/* The page head carries the trail, ONE orientation label, the single h1 and
   the lede. One eyebrow per page, in this block only — design.md prohibits a
   label on every section. */
.page-head{padding-top:clamp(112px,15vh,180px)}
.crumbs ol{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 28px;padding:0;list-style:none;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--steel)}
.crumbs li+li:before{content:"/";margin-right:8px;color:var(--line)}
.crumbs a{text-decoration:none}
.crumbs a:hover{color:var(--accent-deep)}
.crumbs [aria-current]{color:var(--graphite)}
.page-eyebrow{margin:0 0 24px;font-size:11px;letter-spacing:.08em;text-transform:uppercase}
.page-head h1{max-width:14ch;margin:0;font-size:clamp(2.8rem,5.4vw,5.8rem);font-weight:500;line-height:.92;letter-spacing:-.055em;text-wrap:balance}
.page-lede{max-width:52ch;margin:44px 0 0 auto;font-size:clamp(1.05rem,1.4vw,1.25rem)}

/* .index-row IS .practice-row — same grid, same hairline, same deep-stone mark
   and 14px shift on hover. Only the tag changed. */
.index-list{padding-top:clamp(60px,7vw,110px)}
.index-row{position:relative;display:grid;grid-template-columns:60px minmax(280px,1.2fr) 1fr 28px;gap:28px;align-items:center;max-width:var(--max);margin-inline:auto;padding:32px 0;border-bottom:1px solid var(--line);text-decoration:none;color:inherit;outline:none;transition:padding .35s var(--ease)}
.index-list .index-row:first-of-type{border-top:1px solid var(--line)}
.index-row:before{content:"";position:absolute;left:0;width:3px;height:0;background:var(--accent-deep);transition:height .35s var(--ease)}
.index-row:hover,.index-row:focus{padding-left:14px}
.index-row:hover:before,.index-row:focus:before{height:55%}
.index-row>span{font-size:11px}
.index-row h2{margin:0;font-size:clamp(1.7rem,3vw,3.25rem);font-weight:500;line-height:1;letter-spacing:-.035em}
.index-row p{max-width:44ch;margin:0;color:var(--steel);font-size:14px}
.index-row i{font-style:normal;color:var(--accent-deep)}
.index-row:focus-visible{outline:3px solid var(--accent-deep);outline-offset:4px}

/* Prose and the engagement aside: the matter-stages type ramp, the
   matter-notes surface. Two-up on desktop, one on phones. */
.page-body{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,4fr);gap:clamp(48px,7vw,110px);align-items:start;max-width:var(--max);margin-inline:auto}
.page-body>.answer-first,.page-body>.prose-block{grid-column:1}
/* The aside is pinned to column 2 for the full height of the block. Without the
   explicit placement it auto-flows AFTER the last prose row and lands at the
   bottom of an otherwise empty right rail, which also makes position:sticky
   meaningless. Content stage: found on the first real service page. */
.page-body>.includes{grid-column:2;grid-row:1/-1;align-self:start}
.prose-block{padding:26px 0;border-top:1px solid var(--line)}
.prose-block h2{margin:0 0 18px;font-size:clamp(1.8rem,3vw,3.2rem);font-weight:500;line-height:1.02;letter-spacing:-.04em}
.prose-block p{max-width:65ch;margin:0 0 15px;line-height:1.55}
.answer-first{padding:clamp(28px,3.6vw,52px);background:var(--accent-deep);color:#fff;border-end-end-radius:clamp(24px,3vw,44px);overflow:clip}
.answer-first p{max-width:48ch;margin:0;font-size:clamp(1.08rem,1.5vw,1.35rem);line-height:1.42}
.includes{position:sticky;top:96px;padding:34px 0 0;border-top:1px solid var(--graphite)}
.includes h2{margin:0 0 22px;font-size:11px;font-weight:400;letter-spacing:.08em;text-transform:uppercase}
.includes ul{display:grid;gap:12px;margin:0;padding:0;list-style:none;color:var(--steel);font-size:14px}
.includes a{text-decoration:none}
.includes a:hover{color:var(--accent-deep)}

/* Flat question list, never an accordion: design.md prohibits one, and a
   collapsed answer is a weaker snippet candidate. */
.page-faq{max-width:var(--max);margin-inline:auto}
.page-faq>h2{max-width:11ch;margin:0 0 60px;font-size:clamp(2.8rem,5.4vw,5.8rem);font-weight:500;line-height:.92;letter-spacing:-.055em}
.faq-item{padding:26px 0;border-top:1px solid var(--line)}
.faq-item h3{margin:0 0 12px;font-size:clamp(1.3rem,2vw,1.9rem);font-weight:500;letter-spacing:-.03em}
.faq-item p{max-width:65ch;margin:0;color:var(--steel);line-height:1.55}

/* Onward links are records with a top rule, not cards. */
.related{max-width:var(--max);margin-inline:auto}
.related>h2{margin:0 0 34px;font-size:11px;font-weight:400;letter-spacing:.08em;text-transform:uppercase}
.related-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px}
.related-list a{padding:30px 0 0;border-top:1px solid var(--graphite);text-decoration:none;transition:padding .35s var(--ease)}
.related-list a:hover{padding-left:10px}
.related-list span{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--steel)}
.related-list h3{margin:38px 0 0;font-size:clamp(1.4rem,2.2vw,2.2rem);font-weight:500;letter-spacing:-.035em}
.cta-phone{margin:20px 0 0 16%;font-size:12px;color:var(--steel)}
.cta-phone a{text-decoration:none}
@media(max-width:899px){.page-body{grid-template-columns:1fr}.page-body>.includes{grid-column:1;grid-row:auto}.includes{position:relative;top:auto}.index-row{grid-template-columns:44px 1fr 24px}.index-row p{grid-column:2}.index-row i{grid-column:3;grid-row:1}}
@media(max-width:599px){.page-head,.index-list,.page-body,.page-faq,.related{padding-inline:20px}.index-row{grid-template-columns:30px 1fr 18px;gap:12px;padding:26px 0}.page-head h1{font-size:clamp(2.9rem,13vw,4.1rem)}.cta-phone{margin-left:6%}}
@media(prefers-reduced-motion:reduce){.index-row,.related-list a{transition:none}}

/* Site-wide legal disclaimer. Spans the footer grid so it cannot be squeezed
   into a 1fr column; muted but deliberately above the AA floor against the
   graphite ground, because a disclaimer nobody can read is not a disclaimer. */
.footer-legal{grid-column:1/-1;margin:26px 0 0;padding-top:20px;border-top:1px solid rgba(255,255,255,.16)}
.footer-legal small{display:block;max-width:92ch;font-size:11px;line-height:1.5;color:rgba(255,255,255,.72)}
/* Footer sign-off art (dad.png): a backdrop pinned to the bottom-right corner.
   isolation makes the footer its own stacking context, so z-index:-1 paints the
   art above the graphite fill but under every in-flow link and line of text.
   overflow:clip lets it bleed past the right/bottom edges without ever adding
   horizontal scroll. The mask fades its left and top edges into the graphite
   wherever text can sit over it; foot.js measures the worst-case contrast. */
footer{position:relative;isolation:isolate;overflow:hidden;overflow:clip}
.footer-art{position:absolute;z-index:-1;right:0;bottom:0;width:min(40vw,560px);translate:3% 5%;pointer-events:none;-webkit-mask-image:linear-gradient(to right,transparent 0,rgba(0,0,0,.25) 30%,#000 56%),linear-gradient(to bottom,transparent 0,#000 30%);-webkit-mask-composite:source-in;mask-image:linear-gradient(to right,transparent 0,rgba(0,0,0,.25) 30%,#000 56%),linear-gradient(to bottom,transparent 0,#000 30%);mask-composite:intersect}
.footer-art img{display:block;width:100%;height:auto}
/* Where the bottom row passes over the art: the legal nav (shrunk to fit its
   links, so no column-wide band) gets a feathered graphite pad (its ::before joins the footer's stacking context at -1, after
   .footer-art in tree order, so it paints between the art and the links) and the
   blurb/disclaimer a graphite halo. Over the plain graphite both are invisible. */
footer>nav{position:relative;justify-self:start;width:fit-content}
footer>nav::before{content:'';position:absolute;z-index:-1;inset:-12px -18px;background:radial-gradient(closest-side,rgb(35 35 35/.92) 62%,rgb(35 35 35/0));pointer-events:none}
footer>p,.footer-legal small{text-shadow:0 0 2px var(--graphite),0 0 6px var(--graphite),0 0 12px var(--graphite)}
@media(max-width:599px){.footer-art{width:64vw}}

/* ══════════════════════════════════════════════════════════════════════════
   PRIMARY NAV — dropdown panels, built from THIS design's tokens.
   Paper ground (design.md: paper is for "navigation and restrained reading
   surfaces only"), one hairline border and NO shadow (shadows to explain
   layers are prohibited), one folio corner rounded rather than four, the
   .practice-row hover signature reused verbatim inside the panel (6px shift +
   deep-stone marker), and the 11px/.08em uppercase utility role for the index
   row. Nothing here is copied from the reference's stylesheet.

   The open state has three sources and they must not fight:

     · NO JS  — CSS :hover / :focus-within opens the panel, scoped to
                html:not(.js) so it is a genuine fallback and not a second
                implementation racing the first.
     · JS     — the .is-open class only, written by js/main.js. Escape closes
                and returns focus to the trigger, which is exactly why
                :focus-within cannot stay live once JS runs: focus is back ON
                the trigger, INSIDE the item, and the panel would reopen on the
                same frame it closed.
     · TOUCH  — no hover, no open. The trigger is a real link to the index
                page, so a tap always goes somewhere; phones get the accordion.

   Closed state is visibility:hidden, not display:none and not aria-hidden: it
   takes the links out of the tab order and out of the accessibility tree while
   still allowing a transition, and nothing focusable is ever left inside an
   aria-hidden subtree.
   ══════════════════════════════════════════════════════════════════════════ */
.desktop-nav{align-self:stretch}
/* Full header height, so the pointer never crosses a dead gap between the
   trigger and a panel anchored at top:100%. The CTA keeps align-items:center
   from .desktop-nav and so keeps its exact former position. */
.nav-list{display:flex;align-self:stretch;align-items:stretch;gap:30px;margin:0;padding:0;list-style:none}
.nav-item{position:relative;display:flex;align-items:stretch}
.nav-top{display:flex;align-items:center;gap:7px}
.nav-caret{width:5px;height:5px;flex:none;border-right:1px solid currentColor;border-bottom:1px solid currentColor;transform:translateY(-2px) rotate(45deg);opacity:.7;transition:transform .3s var(--ease)}
.nav-item.is-open .nav-caret,html:not(.js) .nav-item:hover .nav-caret{transform:translateY(1px) rotate(-135deg)}
.nav-menu{position:absolute;z-index:2;top:100%;left:-20px;min-width:252px;max-width:330px;margin:0;padding:6px 0;list-style:none;
  background:var(--paper);color:var(--graphite);border:1px solid var(--line);border-end-end-radius:16px;overflow:clip;
  opacity:0;visibility:hidden;transform:translateY(-6px);
  transition:opacity .3s var(--ease),transform .3s var(--ease),visibility 0s linear .3s}
.nav-item.is-open>.nav-menu,
html:not(.js) .nav-item:hover>.nav-menu,
html:not(.js) .nav-item:focus-within>.nav-menu{opacity:1;visibility:visible;transform:none;transition-delay:0s}
/* The row interaction is the practice row's, so the panel reads as part of the
   same system: a 6px shift and a deep-stone marker, no background fill. */
.nav-menu a{position:relative;display:block;padding:11px 20px;font-size:13px;line-height:1.3;letter-spacing:-.01em;color:var(--graphite);transition:padding .35s var(--ease),color .25s}
.nav-menu a:before{content:"";position:absolute;left:0;top:50%;width:3px;height:0;background:var(--accent-deep);transform:translateY(-50%);transition:height .35s var(--ease)}
.nav-menu a:hover,.nav-menu a:focus-visible{padding-left:26px}
.nav-menu a:hover:before,.nav-menu a:focus-visible:before{height:56%}
.nav-menu-all{margin-top:6px;border-top:1px solid var(--line)}
.nav-menu-all a{padding-top:14px;padding-bottom:14px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--steel)}
.nav-menu-all a:hover{color:var(--graphite)}
.nav-menu-all span{color:var(--accent-deep)}
.nav-top:focus-visible{outline:2px solid currentColor;outline-offset:3px}
.nav-menu a:focus-visible{outline:2px solid var(--accent-deep);outline-offset:-2px}

@media(max-width:899px){
  /* Phone: <details> accordions. Hover is meaningless here and a dropdown that
     simply vanished would be half a feature, so the same generated pages are
     reachable in two taps. The index page is the first row in each group. */
  .mobile-menu{max-height:calc(100dvh - 64px);overflow-y:auto;overscroll-behavior:contain}
  .m-group>summary{display:flex;align-items:center;justify-content:space-between;padding:13px 0;border-bottom:1px solid var(--line);list-style:none;cursor:pointer}
  .m-group>summary::-webkit-details-marker{display:none}
  .m-group>summary:after{content:"+";color:var(--accent-deep);font-size:17px;line-height:1}
  .m-group[open]>summary:after{content:"\2013"}
  .m-group>ul{margin:0 0 10px;padding:6px 0 6px 15px;list-style:none;border-left:1px solid var(--line)}
  .m-group>ul a{display:block;padding:10px 0;border:0;font-size:14px;color:var(--steel)}
  .m-group>ul a:hover{color:var(--graphite)}
}

@media(prefers-reduced-motion:reduce){
  .nav-menu,.nav-menu a,.nav-caret{transition:none}
}

/* ── reversed brandappart stack: CSS footprint is this comment plus the .reveal
   swap above, and nothing else. ──────────────────────────────────────────────
   The effect (position:sticky, z-index, transform) is written entirely as
   INLINE style by js/main.js, so no-JS is byte-identical to the page before it
   existed and prefers-reduced-motion is handled by declining to build (and by
   tearing the inline styles back off if the preference flips at runtime).

   WHY .reveal moved from `transform` to `translate`: slot 1 (.proof) carries
   class="reveal", and the stack writes `transform` on that same element. Two
   owners of one property, one of them with a 700ms transition, is a fight the
   stack loses on every scroll frame. The individual `translate` property
   composes BEFORE `transform` in the CSS transform pipeline, so the reveal's
   20px entrance and the stack's recede now stack instead of clobbering — and
   the resting computed `transform` of every section is literally `none`.

   WHY NO ancestor `perspective`: <main> is ~7450px tall, so a perspective there
   puts the vanishing point ~3700px from a section held at the viewport top and
   the tilt renders as an off-axis shear. Each section supplies its own
   projection with `perspective()` as the first function of its own transform,
   which also means no `transform-style:preserve-3d` is needed anywhere — which
   matters, because every section below carries `overflow:clip` and that would
   flatten an inherited 3D context. If you ever move to ancestor perspective,
   those `overflow:clip` declarations WILL break it. */

/* ─────────────────────────────────────────────────────────────────────────
   Z-INDEX HEADROOM for the lid-lift stack (js/main.js). The stack writes
   inline z-indices 1..n+1 onto the <section> children of <main> and the
   footer. With the old .mobile-menu 9 / .site-header 10 the band ran out of
   room at eight rendered sections — the footer's n+1 would have TIED the
   mobile menu and, being later in the DOM, painted over an open menu. The
   editor can add sections at runtime. Raising these three by 10x is a
   z-index-only change: nothing else on the page occupies 9..300, so paint
   order and every pixel are identical, and the section band is now
   permanently uncapped.
   ───────────────────────────────────────────────────────────────────────── */

/* ══════════════════════════════════════════════════════════════════════════
   THE MATTERLINE RAIL MOVES ON THE COMPOSITOR, NOT THROUGH LAYOUT.
   design.md line 91 reads "Matterline advances on scroll; no pinned `dvh`
   elements, no layout-property animation." Until this block the second half of
   that sentence was false: the rail above animates `height` on <i> and `top`
   on <b> — two layout properties, 600ms each, once per stage change. This is
   also the region the reader actually hand-scrolls; it sits at .matterline,
   offsetTop 3214 at 1920x1080, which is below every lid window, so the
   committed advance in js/main.js is inert here and this rail is the only
   thing moving. The operator's "still quite laggy" is partly this.

   WHY BOTH HAD TO GO IN ONE EDIT. Four builds measured A/B/C/D interleaved in
   ONE browser process, variant order reversed on alternate reps so machine
   load cannot favour one arm, 1920x1080, 12 CDP wheel notches at 100ms from
   scrollY 3214 (every arm ended at 4414, so the gesture was identical), CDP
   tracing, 3 reps:
                                    Layout        Paint          RasterTask
     v11, both properties live      93/88/99      241/244/246    296/300/299
     <i> converted, <b> untouched   94/88/88      173/168/176    190/185/191
     <b> converted, <i> untouched   90/98/96      174/179/182    227/235/234
     both converted (shipped)        7/ 7/ 6       40/ 40/ 39    121/123/123
   Either property alone re-dirties the same subtree on every frame of its own
   600ms transition, so converting the other one buys NOTHING on layout —
   94/88/88 and 90/98/96 are both inside the 88-99 spread of doing nothing.
   Converting the pair takes Layout to 7, i.e. to the count you get by deleting
   the animation outright. Convert both or convert neither.
   (The half-fixes are not worthless off the layout axis — either one roughly
   halves Paint — but note this was measured with the elements still fully
   painted, unlike an ablation that removes their ink and flatters the number.)

   THE TWO CONVERSIONS, AND WHY NEITHER NEEDS A PIXEL MEASURED IN JAVASCRIPT.
   <i>  fills from the top, so it becomes a full-height bar scaled about its
        top edge. scaleY() takes a <percentage> (CSS Transforms 2), so
        --matter-progress — which js/main.js writes as "33.333…%" — drops
        straight in with no unit conversion and no JS read.
   <b>  is the harder one, and the reason a previous pass thought this needed a
        JS height read plus a resize hook: `top` is a percentage of the RAIL's
        height, while translateY() is a percentage of the ELEMENT's own height,
        and those are the same number only if the element IS the rail's height.
        So <b> is made full-height and the 38px square is painted as a
        background image pinned to its top-left instead of being the box
        itself. The percentage is then resolved against the same containing
        block `top` used, so it needs no length from JS, no resize hook, and it
        tracks a resize for free. .matter-progress is absolutely positioned with
        both `top` and `bottom` set at every breakpoint, so its height is always
        definite and height:100% always resolves; aspect-ratio is neutralised so
        it cannot silently supply a 38px height if that ever stops being true.
        Below the square the box is transparent, carries pointer-events:none,
        and is clipped by .matterline's existing overflow:clip — measured:
        document scrollHeight identical before and after at 1920x1080, 1440x900,
        768x1024 and 390x844.

   THE DOCKET NUMBER DOES NOT MOVE, and that is arithmetic, not an eyeball.
   place-items:center in a 38px grid box puts the inherited 15px line box at
   (38-15)/2 = 11.5px and the font's content area a further (15-CA)/2 into it,
   so the content area starts at 19 - CA/2. line-height:38px on a block puts it
   at half-leading (38-CA)/2 = 19 - CA/2. Identical for every CA, i.e. for
   whichever font actually loads. text-align:center replaces the inline axis.
   --docket-size is the single source of the 38: width, background square, line
   box and the centring offset all derive from it.

   WHAT THE PIXEL DIFF ACTUALLY SHOWS. 32 screenshots — 4 viewports x
   {no-preference, reduce} x --matter-progress {0, 33.3, 66.7, 100}% — diffed
   against v11 at AE 0 fuzz. 28 are byte-identical. The four that are not are
   the two desktop viewports at the two fractional progress values, and the
   difference is 37-129 pixels out of 2.07M (0.006%), confined to ONE row at
   the top and one at the bottom of the docket square: v11 paints a 38px box at
   a snapped integer position, this paints the same 38px of ink at its exact
   fractional position, so the edge rows carry partial coverage instead of
   rounding to whole pixels (measured 0.861 + 0.146 = 1.007 rows of extra
   coverage against 37 solid, i.e. 38.0px of ink either way). That is the
   ordinary cost of handing a position to the compositor, and it is a gain in
   motion: `top` stepped the square by whole pixels every frame of the slide,
   translateY() moves it sub-pixel.

   AT REST NOTHING IS PROMOTED. No will-change anywhere in this block. Chrome
   promotes for the duration of a transform transition and drops the layer
   afterwards, which is what keeps the lid-lift stack's "will-change: auto at
   rest" invariant true for these two elements too.

   @supports GUARDS THE PAIR TOGETHER, because of the table above: a browser
   without percentage arguments to scaleY() must keep BOTH properties exactly
   as v11 shipped them rather than converting <b> alone and paying for a
   restructure that buys it no layout at all.
   ══════════════════════════════════════════════════════════════════════════ */
@supports (transform:scaleY(50%)){
  .matter-progress i{height:100%;transform-origin:0 0;transform:scaleY(var(--matter-progress,0%));transition:transform .6s var(--ease)}
  .matter-progress b{--docket-size:38px;top:0;aspect-ratio:auto;width:var(--docket-size);height:100%;display:block;line-height:var(--docket-size);text-align:center;pointer-events:none;background:linear-gradient(var(--accent-deep),var(--accent-deep)) 0 0/var(--docket-size) var(--docket-size) no-repeat;transform:translateY(calc(var(--docket-top,0%) - var(--docket-size)/2));transition:transform .6s var(--ease)}
  /* Both reduced-motion resolutions for this rail sit EARLIER in the cascade
     than this block, so they have to be restated or this block would undo
     them: `display:block` would revive the docket that rule hides, and
     `transition:transform` would revive the 600ms slide it removes.
     transform:none on a height:100% bar is the fully resolved line, which is
     exactly what `height:100%` meant there. */
  @media(prefers-reduced-motion:reduce){
    .matter-progress i{transform:none;transition:none}
    .matter-progress b{display:none}
  }
}

/* ── THE NEXT-SECTION CONTROL ───────────────────────────────────────────────
   ONE PER ANIMATED SECTION, IN FLOW. This was `position:fixed;z-index:80;
   right:var(--gutter);bottom:26px` — a single pill on <body> whose label and
   target js/main.js rewrote on every scroll event so that it read as a
   per-section control. It is now the thing it was imitating. js/main.js's
   §THE NEXT-SECTION CONTROL carries the whole derivation: which sections get
   one and how that set was measured, where in each section it sits, and why
   the old comment's structural objection to going inline was already false
   (every section is position:relative on line 46 of this file).

   NOTHING POSITIONED, so there is no z-index and no stacking context: the
   control cannot participate in the lid chain's paint order, cannot cover the
   header or the mobile menu, and cannot be occluded by anything the page does
   not already occlude at that point in the flow. `width:fit-content` on a
   block-level flex box, so it starts its own line in all five containers
   rather than trying to share one with the hero's CTA.

   GHOST, NOT SOLID, and that is forced rather than chosen: the five hosts are
   .hero (white type on a photograph), .proof, .practices, .matterline and
   .bench (graphite type on --field/--paper/--mist/--shelf). A fixed control
   could hard-code graphite-on-white because it was never on the hero; an
   inline one is, so the only colours it can safely name are `currentColor`
   and the accent, which resolve correctly on every one of them. It also puts
   it a step below `.button` in the hierarchy, which is right — the hero's
   primary CTA is a solid `.button.light` two lines above it.
   Hover is `translateY(-2px)` and nothing else, which is this site's hover
   language everywhere else (.button, .nav-cta, .practice-row all move rather
   than recolour).

   THE prefers-reduced-motion BLOCK THAT USED TO BE HERE IS GONE, not
   forgotten. It zeroed the transition and the hover lift, and it can never
   match: stackBuild() returns at its first line under reduced motion so no
   control is ever adopted, and the runtime flip is handled by onMotion() ->
   stackTeardown() -> nextClear(), which puts `hidden` back on every one of
   them. A rule that cannot match is a claim this file would otherwise be
   making and not keeping. (These buttons now SHIP in index.html rather than
   being created at run time, so "no control is ever adopted" is where the
   claim rests, and §THE RESERVED BOX is what makes the shipped-but-unadopted
   button take no space and paint nothing. Measured on the shipped build with
   reduced motion forced, at 1440x900 / 768x1024 / 390x844 / 899x500 / 850x700
   / 844x390 / 1280x800 / 2560x1080: 2 shipped, 0 adopted, 0 with a box, 0
   painted at every one of the eight.)

   THE `display:flex` ON THIS RULE IS WHY §THE RESERVED BOX NEEDS AN EXPLICIT
   `[hidden]{display:none}` AND CANNOT LEAN ON THE UA SHEET. Author rules beat
   the UA sheet whatever the specificity, so this one declaration is enough to
   defeat `[hidden]{display:none}` on its own. Do not move the reservation's
   opt-out or this line without re-reading the note down there — the two are
   one mechanism. */
.next-jump{display:flex;width:fit-content;align-items:center;gap:14px;margin-top:34px;padding:12px 16px;border:1px solid currentColor;background:none;color:inherit;font:inherit;font-size:12px;letter-spacing:-.01em;cursor:pointer;transition:transform .25s var(--ease),opacity .25s}
.next-jump i{display:block;width:9px;height:9px;border-right:1px solid var(--accent);border-bottom:1px solid var(--accent);transform:rotate(45deg);translate:0 -2px}
/* THE ELLIPSIS CLAMP IS GONE, AND IT HAD TO GO RATHER THAN BE WIDENED.
   This read `max-width:22ch;overflow:hidden;white-space:nowrap;
   text-overflow:ellipsis`, with a 20ch override in the 599 block. That
   machinery existed because the label was DERIVED — src/render.mjs truncated
   the destination section's h2 — so it was arbitrarily long and the clamp was
   the backstop. The label is now the fixed 25-character site.nextLabel, so
   there is nothing left to clamp and the clamp had started corrupting it:
   measured on the served build at 11px, the <b> wants 118px against the 100px
   20ch allowed, so "Click here to scroll down" rendered as "Click here to
   scroll d…" at 599x900, 390x844, 360x640 and 320x568. At 600px and up it
   fit (126 into 142) and the defect was invisible, which is exactly the shape
   of bug a dead constraint produces.
   white-space:nowrap IS KEPT and is doing separate work: it is what stops the
   label wrapping to two lines inside a `width:fit-content` flex row, which
   would change the control's height and therefore .hero's internal spacing.
   Measured unclamped at the narrowest viewport this site admits: the control
   is 169px wide at 320px, against 280px of content box — 111px of slack, so
   nowrap cannot overflow the gutter. */
.next-jump b{font-weight:400;white-space:nowrap}
.next-jump:hover{transform:translateY(-2px)}
.next-jump:active{transform:scale(.98)}
.next-jump:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
@media(max-width:599px){.next-jump{margin-top:min(28px,3.2vh);padding:10px 13px;font-size:11px}}

/* ── THE RESERVED BOX ──────────────────────────────────────────────────────
   The five controls ship in index.html carrying `hidden`, and js/main.js
   adopts them — it removes the attribute rather than creating an element. That
   is a CLS fix: built at run time they were inserted AFTER first paint and
   pushed 78px of already-painted page down (0.1083-0.3189 on cold fragment
   loads, 28 of 30 over the 0.10 line; 0 of 30 with the box present from the
   start). src/render.mjs's §THE NEXT-SECTION CONTROL SHIPS IN THE MARKUP
   carries the full A/B.
   `hidden` MEANS display:none HERE ONLY BECAUSE THIS BLOCK SAYS SO. The UA
   sheet's `[hidden]{display:none}` loses to ANY author declaration, and §THE
   NEXT-SECTION CONTROL above authors `.next-jump{display:flex}` unconditionally
   — so leaving the default implicit did not make the button disappear, it made
   it PAINT. That was live, and it was the reason this rule was written: with
   the reservation as the only [hidden] rule and gated on no-preference, a
   reduced-motion reader got two fully visible, fully hit-testable buttons that
   were never wired to anything (measured on that build: display flex,
   visibility visible, opacity 1, 185.7x44 at 1280x800, elementFromPoint
   returning the button, and a real click moving scrollY 0 -> 0). They were
   also absent from the accessibility tree, because the `hidden` attribute
   still did its non-display half — a control visible only to readers who
   cannot use it. `.next-jump[hidden]{display:none}` restates the default at
   author level so the [hidden] pair below is a true opt-in.
   That opt-in is the whole reason the attribute was chosen over a class: space
   is reserved only where a control is going to be adopted, and every path that
   ends in no control takes no space without restating a condition. Those paths
   are real, not hypothetical — measured on the shipped build at 1440x900 /
   768x1024 / 390x844 / 899x500 / 850x700 / 844x390 / 1280x800 / 2560x1080:
   2 shipped and 2 adopted at every one under no-preference, and 2 shipped /
   0 adopted / 0 boxed / 0 painted at every one under reduced motion.
   STILL 2 AND 2 AT STACK_COUNT 3 — re-measured at 1280x800 / 1920x1200 /
   768x1024 / 390x844 on the 0/3 build. What changed is that WANTED is now 3:
   .practices became the chain's last participant, so nextAnimated() answers
   true for it and nextSync() wants a control there. It ships none, takes the
   documented silent skip, and reserves no box — so no height moved and the
   78px / 65px figures below are unaffected. If a third control is ever
   emitted on .practices, every number in this paragraph and every section top
   below .practices has to be re-measured; an in-flow button is height.
   So: reduced motion reserves nothing, because stackBuild() returns at its
   first line and neither control is ever adopted. Removing the two reserved
   boxes there costs the page 78px of document height at 1440x900 and 1280x800
   and 65px at 390x844 — .proof's box; .hero is height:100vh and absorbs its
   own — and no reader under reduced motion ever sees a shift, because the
   attribute is on the element in the served bytes.
   visibility, not opacity, and not merely off-screen, for the reserved box:
   it takes the box, paints nothing, and is dropped from the tab order, from
   hit-testing and from the accessibility tree, so there is no window in which
   a reader can see or reach a control that is not yet wired. */
/* THE .bench EXCLUSION AND ITS TWO OPT-INS ARE GONE — THERE IS NO .bench
   CONTROL LEFT TO RESERVE A BOX FOR. This gate used to carry a second rule,
   `.bench-head .next-jump[hidden]{display:none}`, undone by a matching
   `{display:flex}` inside §THE LANDING and again inside §THE DECK, so that the
   reservation was written in the same block as the condition that made .bench
   animate and the two could not drift. That was good structure for a section
   whose control came and went with the viewport; the operator has now removed
   .bench's control outright (src/build.mjs emits none), so all three rules
   match nothing. Deleted rather than left in place: a selector that cannot
   match is a claim this file makes and does not keep, which is the standard
   §THE NEXT-SECTION CONTROL sets a few lines above.
   The surviving gate below still covers only the two controls that DO ship, on
   .hero and .proof — both of which animate at every viewport, so neither needs
   a per-viewport opt-in of its own. It gained the unconditional
   `[hidden]{display:none}` line above it; see the note in the block head. */
.next-jump[hidden]{display:none}
@media(prefers-reduced-motion:no-preference){
  .next-jump[hidden]{display:flex;visibility:hidden}
}

/* ── THE PROOF PLATE ────────────────────────────────────────────────────────
   .proof declares three columns — 12% / 8fr / 3fr — and has only ever filled
   two of them. The plate goes in the third, so the section gains a photograph
   without gaining a row.

   WHY IT MAY NOT ADD HEIGHT, AND HOW THAT IS ENFORCED RATHER THAN HOPED.
   .proof is slot 1 of the lid chain, and js/main.js derives the whole chain
   from measured participant heights:
       advSpan[i] = min(stackH[i], svh) ; advStart[i] = staticTop[i+1] - advSpan[i]
   so every boundary below .proof moves with it, and past svh the lift stops
   being a full viewport at all (measured 1440x900: stackH 831 against svh 900,
   i.e. 69px of headroom — the same failure §THE SCOPE CONSTANT cites for the
   1505px .work). So column 3 must never be the tallest track, and flex:1 on the
   image is what guarantees it: the plate takes the height LEFT OVER in a row it
   did not size, and can only ever shrink. Column 3's own contribution to the row
   is the caption alone.
   Measured, plate present vs. the same page with .proof-visual{display:none}
   (which is the pre-change grid exactly), section height and all six section
   tops identical to four decimals at every width that keeps three columns:
       1920x1080  .proof 903.4375    1440x900  .proof 831.3125
       1280x800   .proof 745.375     1024x768  .proof 592.5313
        900x900   .proof 533.1875
       1440x900 tops, both builds: 0 / 900 / 1731.3125 / 2902.9531 /
                                   4483.4531 / 5988.4219
   `height:0` alongside flex-basis:0 is belt to that braces, and it is honestly
   labelled: dropping it at runtime moved the section by 0.0000px at all fifteen
   viewports measured, so nothing here needs it. It stays because the intrinsic
   contribution of a flex-basis:0 item to an auto grid row is exactly the corner
   engines disagree on, and the cost of being wrong is every boundary on the page.

   SPECIFICITY IS LOAD-BEARING IN TWO PLACES, because .proof already addresses
   its children generically. `.proof>div{grid-column:2}` and `.proof>div p`
   (0,1,1 and 0,1,2) both match this block, so the child selector carries the
   class — `.proof>.proof-visual` is 0,2,0 and 0,2,1 and wins on class count.
   Dropping either `.proof>` prefix silently returns the plate to column 2 and
   the caption to 46ch of body type.

   THE CORNER IS THE SITE'S, SCALED. .matter-visual img takes
   clamp(24px,3vw,44px) on a 608px-wide plate — 7.1% of its width. This plate
   is 279px wide at 1440 and 301px at 1920, so the same 7.1% is 20px and 21px:
   clamp(16px,1.4vw,24px). Same treatment, same proportion, not the same number. */
.proof>.proof-visual{grid-column:3;display:flex;flex-direction:column;min-height:0}
.proof>.proof-visual img{flex:1 1 0;min-height:0;height:0;object-fit:cover;object-position:50% 50%;border-end-end-radius:clamp(16px,1.4vw,24px)}
.proof>.proof-visual p{max-width:none;margin:18px 0 0;font-size:12px}
/* 600-899px: .proof drops to two columns (40px / 1fr), so the plate would have
   nowhere to go but a second ROW — and a row is 300-700px of height. The third
   track is restored instead, at a flat 30% rather than the desktop 3fr because
   the fr share of a 700px grid is a stamp. The rules above then apply unchanged,
   and the stretch still keeps the PLATE from sizing the row; the plate just
   reads squarer here (165x238 at 600, 212x238 at 768, 248x227 at 899) because
   the row it fills is shorter.
   THE ONE PLACE THIS CHANGE COSTS HEIGHT, AND THE EXACT NUMBER. Narrowing the
   text column re-wraps the lead paragraph from two lines to three — 50.375px to
   75.5625px — so .proof gains 25.1875px, once, at 600-895px:
       600x900  487.1875 -> 512.375     768x1024  487.1875 -> 512.375
       820x1180 496.7813 -> 521.9688     895x900  530.7813 -> 555.9688
       896x900  531.3125 -> 531.3125 (+0, and so on to 899)
   THE FREE RANGE IS 896-899, NOT 821-899, which is what this block used to say.
   Swept 1px at a time from 821 up against the reconstructed untouched grid
   (.proof-visual{display:none} plus .proof{grid-template-columns:40px 1fr}),
   the lead holds three lines and the delta holds 25.1875 at every width through
   895; at 896 the text column reaches 447.45px, the lead returns to two lines
   and the delta goes to 0. The old figure read as 75px of free viewport width
   that is not free.
   That is the whole delta, and it is the RE-WRAP, not the plate: swept at 768
   from 30% down to 16%, the section holds 512.375 while the lead is 75.6 and
   drops to 487.1875 — the untouched baseline, plate still in place — the moment
   the lead returns to 50.4. That happens between a 434.3px text column (22%,
   still three lines) and a 448.5px one (20%, two), i.e. only a 141px plate buys
   the 25px back, which is a stamp. 30% is the trade taken. It is also harmless
   where it lands: .proof/svh stays 0.44-0.73 across 600-899, so its lid is still
   a full viewport with room to spare. */
@media(min-width:600px) and (max-width:899px){
  .proof{grid-template-columns:40px minmax(0,1fr) 30%}
}
/* Below 600px there is no honest third column left — a 26px marker, a headline
   at 13vw and a plate will not share 350px — so the plate becomes a second row
   and .proof DOES grow, by 320.8px at 360 to 413.9px at 599. That is a measured
   trade, not an oversight: the height budget here is svh minus a 510-547px
   section, which is 112px at 360x640, and no photograph fits in 112px.
   WHAT THE SIZE BELOW BUYS is that .proof's ratio to svh lands under the ratio
   this stack ALREADY shipped on the same screen, at every size tested. The
   comparison was against .practices, on the grounds that ".practices is a lid
   too, and it is the taller one in the untouched build":
       vp        .proof+plate/svh     .practices/svh (as measured then)
       599x800        1.20                  1.21
       430x932        0.98                  1.12
       412x915        0.97                  1.15
       390x844        1.02                  1.27
       375x667        1.26                  1.70
       360x640        1.33                  1.80
   .practices IS STILL NOT A LID, AND THIS PARAGRAPH HAS NOW BEEN WRONG ONCE IN
   EACH DIRECTION. It read ".practices IS NO LONGER A LID — the chain ends at
   .proof … .proof is today the ONLY lid on the page", which was right at 0/2
   and is half wrong at 0/3 (js/main.js §THE SCOPE CONSTANT). There are TWO
   lids now, .hero and .proof; .practices is the chain's LAST participant, and
   the last participant is never a holder and never lifts, so the right-hand
   column above is still a comparison against a section that does not lift.
   What changed is that .proof's ratio DOES have a sibling to be measured
   against again — .hero's, which is exactly 1.00 by construction because the
   hero is the page's only 100vh section, and a lid at 1.00 is the reference
   this budget is trying to stay near. That does not reopen the decision. The budget
   argument above stands on its own — a lid taller than svh stops being a full
   viewport lift, and .proof+plate is at or under 1.33 here — and these figures
   are kept because they are what was measured at the time, not because
   .practices still means anything to this block.
   The 5/4 crop and the 360px cap are chosen for that budget rather than for
   the composition; cover keeps the three figures, which sit in the middle band
   of the source. */
@media(max-width:599px){
  .proof>.proof-visual{grid-column:2;display:block;width:min(100%,360px);margin-top:clamp(28px,7vw,44px)}
  .proof>.proof-visual img{flex:0 0 auto;height:auto;aspect-ratio:5/4;border-end-end-radius:24px}
}

/* THE PLATE IS TOO SMALL ON A WIDE SCREEN, AND WIDTH IS THE ONLY LEVER.
   The plate is `flex:1 1 0` in a stretched column, so it ALREADY fills the row's
   full height — 374.1 of 428.1 at 1440, the rest being the caption. Making it
   "bigger" therefore cannot mean taller: the row is sized by the text column, and
   growing the row grows .proof, which is slot 1 of the lid chain and would move
   every advance boundary below it. So: widen the third track, leave the row alone.

   HOW MUCH IS FREE. Column 2 is far wider than its own contents at desktop —
   both the h2 (max-width:11ch) and the lead (max-width:46ch) render at their caps
   with slack to spare, and neither re-wraps until the TRACK drops under the larger
   cap. Measured floor = max(h2, lead), and the slack above 1200px:

       vp      col2     h2 cap   lead cap   floor    slack
       1920    802.5    616      506        616      186
       1440    743.2    517      506        517      226
       1280    660.6    451      460        460      200
       1024    528.5    363      460        460       68   <- too tight, left alone
        900    464.5    308      460        460        4   <- none

   8fr/5fr spends about half the available slack and keeps a 60-110px cushion at
   every width in range, so no cap is approached and the row cannot re-wrap.
   GATED AT 1200 because the slack collapses below it: at 1024 a proportional
   change would take col2 to ~407 against a 460 lead cap, re-wrapping it from two
   lines to three and growing .proof — the same re-wrap trap documented for the
   600-899 band above, which is what that whole comment block exists to warn about. */
@media(min-width:1200px){
  .proof{grid-template-columns:12% minmax(0,8fr) 5fr}
}

/* ── THE PRACTICE PLATE ─────────────────────────────────────────────────────
   .practice-intro was a BLOCK holding an eyebrow and an h2, and
   `.practice-intro h2{max-width:10ch}` caps that h2 at about a third of the
   container — so the right two thirds of the section were bare --shelf. This
   is the same problem §THE PROOF PLATE solves one section above, and it is
   deliberately solved the same way rather than a second way.

   ONE GRID, TWO COLUMNS, AND THE PLATE SPANS BOTH ROWS. The intro becomes a
   grid; the eyebrow and h2 stay in column 1 (they are rows 1 and 2 of it) and
   the figure takes column 2 across `grid-row:1/-1`. So the photograph is
   beside the heading, not under it, and the section gains no row.

   WHY IT MAY NOT ADD HEIGHT — A WEAKER REASON THAN .proof'S, AND KEPT ANYWAY.
   .proof holds its height because it is slot 1 of the lid chain and a HOLDER:
   stackMeasure() reads holders' offsetHeight into the sticky top offset and
   into each lid's travel.
   .practices IS IN THE CHAIN AGAIN at STACK_COUNT 3 (js/main.js §THE SCOPE
   CONSTANT) — this line said "no longer in that chain at all", which was true
   only between JS_V 31 and 34 — but it is the LAST participant, and only
   HOLDERS' heights are read, so its own height is still not an input to any
   lift. Its TOP is: it is staticTop[2], which sets .proof's advStart and
   advEnd. That top is fixed by .proof's height, not by its own. It IS still an
   input to every section top below it and to the
   auto-advance's tail windows, so `flex:1 1 0;min-height:0;height:0` on the
   <img> is copied verbatim from .proof's plate: the picture takes the height
   LEFT OVER in a row it did not size, and can only ever shrink.
   NO align-items HERE, and that is not an omission. The default `stretch` is
   what gives the figure the row's full height for its flex box to fill;
   `align-items:start` collapses a flex-basis:0 image to nothing and the plate
   disappears entirely. .proof's grid relies on the same default.

   SPECIFICITY, THE SAME TRAP §THE PROOF PLATE DOCUMENTS. `.practice-intro>p`
   (0,1,1) is the 11px uppercase eyebrow rule and is SHARED with `.work-lead>p`,
   so it would capture any <p> placed as a direct child here. The figure is
   addressed as `.practice-intro>.practice-visual` (0,2,0) and wins on class
   count, exactly as `.proof>.proof-visual` does.

   THE CORNER IS THE SITE'S, SCALED, by the rule §THE PROOF PLATE sets:
   .matter-visual takes clamp(24px,3vw,44px) on a 608px plate, i.e. 7.1% of its
   width. Deliberately a larger clamp than .proof's clamp(16px,1.4vw,24px)
   because this is the wider of the two plates.

   TWO THINGS HERE ARE EASY TO GET WRONG AND BOTH WERE, ON THE FIRST WRITE.
   1. `gap:5vw` SETS THE ROW GAP TOO. The eyebrow and the h2 are two rows of
      column 1, so a shorthand gap put 64px between them at 1280 on top of the
      eyebrow's own 24px margin — 88px where the block layout had 24px, i.e.
      this rule silently restyled the heading it was supposed to sit beside.
      It is `column-gap` alone, and `row-gap:0` is stated rather than left to
      the initial value so that a later `gap:` shorthand cannot reintroduce it.
   2. `grid-row:1/-1` NEEDS EXPLICIT ROW TRACKS. With `grid-template-rows`
      unset the explicit grid has ONE row line, so -1 resolves to line 1 and
      `1/-1` degenerates to a single row: measured, the plate came out 375.9 x
      40.5px — the height of the eyebrow — instead of spanning to the foot of
      the h2. `grid-template-rows:auto auto` is what makes -1 mean line 3.
      That number is the count of column-1 children; a third one would land in
      an implicit row the figure does not span, so the two are coupled and the
      markup in src/build.mjs emits exactly the two.

   THE STACK IS AT 767px, WHICH IS A NEW BREAKPOINT ON THIS SITE, and it is
   justified by measurement rather than convention. The site's own breakpoints
   are 899 and 599 and neither is the right place: the binding constraint is
   the h2's own 10ch floor, and with the figure taking 38% and a 5vw column gap
   the text column reaches that floor at about 630px. 599 would therefore ship
   a squeezed heading, and 899 would throw the plate away while slack remained.
   Below the breakpoint the intro returns to a block and the plate becomes an
   ordinary 3:2 row under the heading, where there is width for it. */
.practice-intro{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,38%);grid-template-rows:auto auto;column-gap:5vw;row-gap:0}
.practice-intro>p,.practice-intro>h2{grid-column:1}
.practice-intro>.practice-visual{grid-column:2;grid-row:1/-1;display:flex;flex-direction:column;min-height:0;margin:0}
.practice-intro>.practice-visual img{flex:1 1 0;min-height:0;height:0;object-fit:cover;object-position:50% 50%;border-end-end-radius:clamp(20px,2.1vw,34px)}
@media(max-width:767px){
  .practice-intro{display:block}
  .practice-intro>.practice-visual{margin-top:clamp(28px,6vw,44px)}
  .practice-intro>.practice-visual img{flex:0 0 auto;height:auto;aspect-ratio:3/2;border-end-end-radius:24px}
}

/* ══════════════════════════════════════════════════════════════════════════
   §THE BENCH — a pinned slot-and-record roster, 2026-08-14, ?v=17
   A PORT of design/landingpages/codegrid/workingstiff-animated-teams-section-
   nextjs/, which is on disk and serving. Every constant below was read out of
   that build's own CSS and page chunk, or measured off the live page in
   Chromium 143.

   ── WHAT THE REFERENCE ACTUALLY DOES, IN ITS OWN NUMBERS ──────────────────
   Three members. Each is a dashed empty SLOT with a huge initial centred in
   it, and the real CARD is a separate box that starts off screen right and
   lands exactly on the slot. The slots never move horizontally, the cards
   never move vertically, and that is the whole illusion. Two GSAP
   ScrollTriggers drive it, and both are expressible as ranges on ONE CSS view
   timeline:

     ST-A  trigger .team, start "top bottom", end "top top", scrub 1.
           ONE viewport height of scroll, BEFORE the pin. Member i rises
           translateY 125% -> 0 over progress [0.15i, 0.15i + 0.7], and its
           initial scales 0 -> 1 over the last 60% of that same window.
     ST-B  trigger .team, start "top top", end "+=3*innerHeight", pin, scrub 1.
           THREE viewport heights of pinned scroll. Card i flies x from
           (300 - 100i)% to -50% and rotates 20deg -> 0 over [0.075i,
           0.075i + 0.4], then scales 0.75 -> 1 over [0.4 + 0.12i, 1].

   ── WHY THERE IS NO JAVASCRIPT HERE, AND WHY THAT IS NOT A COMPROMISE ─────
   The reference needs GSAP + ScrollTrigger + Lenis. This site has none of the
   three. Lenis in particular replaces native scrolling for the WHOLE page,
   and this page's lid stack (js/main.js) computes its advance windows from
   real scrollY — a hijacked scroller underneath it is not a section this
   section gets to decide for. It is also unnecessary, because both triggers
   are exactly named ranges on a view timeline:

     ST-A  "top bottom" -> "top top" is the section's start edge crossing the
           scrollport: one viewport height. No named range is exactly that
           (`entry-crossing` ends when the subject's END edge reaches the
           scrollport end, so on a 400vh subject it runs 3600px, not 900). A
           slice of `cover` IS exact: cover spans H + 100vh, and H is
           (1 + --bench-pin) * 100vh by declaration, so the first 100vh of it
           is 100% / (--bench-pin + 2) — 20% at three people. That is --p1,
           and it is computed from --bench-pin on the adjacent line FOR THAT
           REASON: they are one number and must not be able to disagree.
     ST-B  the pin IS `contain 0% -> contain 100%` for a subject taller than
           the scrollport, by definition — docTop .. docTop + H - 100vh, which
           is exactly the interval the sticky child is stuck for.

   WHAT IS GENUINELY LOST: `scrub: 1`, GSAP's one-second smoothing lag, and
   Lenis's inertia. Both are FEEL, not geometry — the position a record holds
   at a given scroll offset is identical. js/main.js makes the same argument at
   length about not lerping the lid stack.

   ── WHERE IT SITS, AND WHY THAT IS THE ONLY PLACE IT COULD ────────────────
   Slot 5 of 7, between .work and .contact. Two constraints decide it and they
   agree:

   1. THE LID CHAIN. js/main.js runs STACK_FROM = 0, STACK_COUNT = 3, so slots
      0-2 (hero, proof, practices) are participants and every lid advance
      boundary is advStart[i] = staticTop[i+1] - min(stackH[i], svh),
      advEnd[i] = ceil(staticTop[i+1]). Those are all reads of sections ABOVE
      slot 3, so inserting at 5 cannot move one.
      THIS LINE SAID "STACK_COUNT = 4 … slots 0-3 (hero, proof, practices,
      matterline)" FOR TWO CHAIN CHANGES AFTER IT STOPPED BEING TRUE — through
      0/2 and now 0/3 — which is the exact failure mode this file keeps paying
      for: a comment asserting a constant nobody re-read. The structural claim
      it was making is not weakened by the correction, it is STRENGTHENED: the
      chain is shorter than it was when the claim was written, so the reads sit
      even further above slot 5. The tail advance stops below the chain DO read
      this section's top — .bench→.contact is one of them — but they are read
      live from getBoundingClientRect() on every measure, not baked, and
      .bench's own top does not move (see the block at slot 5's own foot).
      Measured after this section
      shipped, 1440x900, one wheel notch: hero @0 -> 900, proof @950 -> 1732,
      mid-matterline @3400 -> 4484 — byte-identical to the pre-bench build.
      THOSE THREE LANDINGS ARE THE ?v=18 TABLE AND THE TABLE HAS MOVED SINCE,
      not because of this section: §THE NEXT-SECTION CONTROL put five in-flow
      buttons on the page at ?v=19 and each one makes its section taller. The
      live table is 0 -> 900, 950 -> 1810, 3400 -> 4684, 5000 -> 6189 at
      1440x900 and 0 -> 844, 950 -> 1773, 3400 -> 4858, 5000 -> 6359 at
      390x844, and src/render.mjs's JS_V changelog owns it. What THIS entry
      claims is the invariant, and it still holds: re-measured either side of
      the multi-paragraph copy, which makes the bench's CARDS taller, all four
      landings and all seven section tops are identical to four decimal places
      at both viewports. A taller card is not a taller section — the pin is
      (1 + n) * 100vh and the deck's rail is (n * 75 + 100)svh, neither of
      which reads the card.
   2. THE TONE PARITY. Adding a section re-alternates every band BELOW it, by
      design (see POSITIONAL TONE BANDS). Inserting last-but-one is the
      minimum: .contact moves light -> shelf and NOTHING else changes. At slot
      4 both .work and .contact would move; at slot 3 the bench would displace
      .matterline out of the lid chain entirely. The bench itself lands on
      slot 5 = light, which is the ground .work's folio corner was already
      previewing, so .work's --next-bg is unchanged too.

   It is also the right place to read. .work is the evidence, the bench is who
   produced it, .contact is the ask — and the alternative, straight after
   .matterline, puts two scroll-driven mechanics back to back with nothing
   between them.

   ── THE BASE LAYOUT IS THE STATIC ONE, AND IT IS NOT A DEGRADED MODE ──────
   Everything down to §THE LANDING is an ordinary responsive grid of records
   that any browser paints, with the empty slot hidden because a slot with its
   record resting on it is invisible anyway. The pin is layered on top inside
   `@supports (animation-timeline: view())` AND `@media
   (prefers-reduced-motion: no-preference) and (min-width: 900px) and
   (min-height: 730px)`, so FIVE audiences get the grid and all five get the
   whole section — every name, face, role and line: reduced-motion visitors,
   browsers without scroll-driven animations, viewports too narrow to stand
   three records side by side, viewports too short to host the stage, and —
   because the pin needs no script — nobody at all for want of JavaScript.
   All four queries are on the ENHANCEMENT. A pinned layout that fell back to
   itself with a dead timeline would be 400vh of frozen stage with three
   records parked off the right edge, which is how this pattern usually fails.

   THE 900px WIDTH GATE IS THIS SITE'S OWN, not a new number: design.md line
   55 puts the desktop upgrade there and the header, the .proof grid, the
   .matterline grid and the drawer all turn over at it. (The reference kills
   its own animation below 1000px and stacks its members in a column, so the
   idea of the gate is its too; only the number is local.)

   THE 730px HEIGHT GATE IS NOT WHERE THE STAGE STOPS FITTING. It is where the
   COPY STOPS RE-WRAPPING, and the difference is the whole point: --bench-fit
   caps the record so the stage keeps fitting as the viewport shortens, but a
   record that keeps shrinking eventually sets `p` narrow enough to take
   another line, and the line is what the stage has no room for.

   MEASURED, and measured again against the shipped `--bench-fit:calc(61vh -
   224px)` — same experiment as before, the gate temporarily lowered to 1px
   and swept at 1024, 1440 and 1920 wide, because that constant moved and the
   old numbers here were its predecessor's. The record's opening `p` re-wraps
   from two lines to three between 723 and 722px tall, where the card crosses
   217px (217.02 at 723, 216.41 at 722, the same at all three widths). That
   third line costs the stage 21px: its leftover falls 49.66 -> 28.53 at 1024,
   33.27 -> 12.19 at 1440 and 29.33 -> 8.25 at 1920. So there is a real cliff,
   and it is at 723px, not at a round number. Lower still it does overflow —
   +10.2px of leftover at 1920x664, -19.3px at 1920x660 — so "it fits at any
   height" is not the claim; it fits at every height this gate admits.

   730 IS 7px ABOVE THAT CLIFF, which is 4.3px of record (221.30px against the
   217 the copy needs) and the margin the budget in src/data.mjs and
   src/build.mjs exists to protect — one longer line anywhere in `p` moves the
   re-wrap up. THE SECOND REASON FOR THE OLD 700 NO LONGER HOLDS, and it is
   not why this number is 730: at the floor the pinned record is 221.30px
   while 262.66px is the SMALLEST record the static layout paints on any gated
   viewport (900px wide, three across), so from 730 up to 798px tall the pin
   does show the roster smaller than the fallback would. 798 is where 61vh-224
   reaches 262.66 and the two treatments paint the identical record; above it
   the width term caps both. That is the price the second paragraph of copy
   set (see §THE PINNED STAGE: the block went 54 -> 108px and the fit went
   61vh-176 -> 61vh-224), and it is what to re-open if this section is ever
   asked for a third paragraph.

   WHAT WOULD MOVE IT. An extra line on `p`, on the h2 or on the standfirst;
   a fourth person (which costs nothing horizontally — the rail grows and
   --bench-card shrinks to hold the row height — but re-times the whole
   section and adds another 100vh of document). Either means re-running this
   sweep and moving this number.

   ── HOW THIS DIFFERS FROM THE REFERENCE ON PURPOSE ────────────────────────
   THE RECORD IS NOT A CARD. design.md prohibits "three equal cards" and
   "four-corner rounded cards", and says practices are "records with hairlines
   ... never cards". So what flies in is .matter-notes' record — graphite top
   rule, 11px uppercase label, h3, one line of --steel copy — with a square
   plate as its first field. No border box, no fill of its own beyond the
   page's own stock, no resting shadow, and the single radius on it is the
   folio corner on the MEDIA, which is the clamp(24px,3vw,44px) that
   .matter-visual img and .featured-matter already carry.

   THE SLOT IS NOT DASHED. There is no dashed border anywhere on this site.
   design.md line 30 gives --line to "rules, indexing, and inactive matter
   paths", and an empty slot is exactly an inactive path, so the slot is the
   site's own hairline: a top and bottom rule with open sides, the same
   treatment .practice-list and .matter-stages use, never a boxed outline —
   which would be the card the record refuses to be.

   THE START OFFSET IS SOLVED, NOT COPIED. The reference starts card i at
   (300 - 100i)% of its OWN width, which only clears the fold because its
   three cards happen to tile the viewport. Here the record is capped by the
   height the stage has left (--bench-fit), so the rail can be NARROWER than
   the viewport and centred in it, and a percentage of the record's own width
   then does not reach the fold. Every record starts at the same screen x
   instead, just past the right edge:

     railw = n*card + (n-1)*gap            the rail's real width
     x0    = (100vw + railw) / 2 + 4vw     record 0's rest-left to off screen
     start_i = x0 - i * pitch              record i sits i pitches right of 0

   100vw INCLUDES THE CLASSIC SCROLLBAR and the centred rail does not, so x0
   over-reaches by half a scrollbar (7.5px in this Chromium, 0 with overlay
   scrollbars). That is the safe direction — further off screen.

   ── THE STAGE DOES NOT CLIP; THE SECTION DOES ─────────────────────────────
   .bench-stage is max-width:var(--max) with gutters, so clipping there would
   cut the records off at 1600px on a wider viewport — mid-flight, in from the
   fold. The clip is on .bench itself, which is full-bleed and already carries
   `overflow:clip` from the folio rule at the top of this file, so the records
   are hidden exactly at the viewport edge and document.scrollWidth is held at
   innerWidth for the whole pass. `clip` and not `hidden`: hidden makes the
   box a SCROLL CONTAINER, and a scroll container containing a focusable
   element scrolls itself when that element takes focus — which on a
   translated record means the browser silently offsetting the effect by
   several hundred px, permanently, with no scroll of its own to correct it.
   Nothing in a record is focusable today (see build.mjs), and `clip` means
   that stays a taste decision rather than the thing holding the page up.

   ── NO will-change ANYWHERE, DELIBERATELY ─────────────────────────────────
   The reference sets will-change:transform on all six boxes. It has to: GSAP
   writes transforms from JS on the main thread. Here the animated properties
   are `translate`, `rotate` and `scale` driven by a view timeline, which
   Chromium already runs off the main thread and already promotes; adding
   will-change would hold six permanent layers for the life of the page to buy
   nothing. js/main.js takes the opposite decision for the lid stack, and the
   difference is exactly that: that one IS a main-thread write.
   ══════════════════════════════════════════════════════════════════════════ */
.bench{--bench-gap:clamp(20px,2.2vw,36px)}
.bench-stage{max-width:var(--max);margin:auto;padding:clamp(100px,12vw,180px) var(--gutter)}
/* 12% is this section's left rail. design.md line 58 forbids two ADJACENT
   sections sharing one, and the neighbours are .work-lead at 16% and
   .contact h2 at 8%. */
.bench-head{width:84%;margin-left:12%}
.bench-head h2{margin:0;max-width:18ch;font-size:clamp(2.8rem,5.4vw,5.8rem);font-weight:500;line-height:.92;letter-spacing:-.055em;text-wrap:balance}
/* .proof's own head shape — the statement, then the standfirst pushed right
   underneath it — rather than .practices' eyebrow-over-heading, because
   design.md prohibits section-by-section eyebrow labels and .proof is the
   section this one answers. Body role, not the 11px utility one: design.md
   line 29 puts --steel's floor at 14px and this paragraph is the one place a
   reader is told the people are invented, so it is set to be read. */
.bench-note{max-width:52ch;margin:52px 0 0 auto;color:var(--steel);font-size:1rem;line-height:1.55}
.bench-rail{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:var(--bench-gap);margin:clamp(40px,5vw,68px) 0 0;padding:0;list-style:none}
/* position:relative in BOTH layouts even though only the pinned one has a slot
   to position: the containing block for .bench-slot is the member's own
   resting box, and that is the definition the effect rests on. display:grid so
   the record stretches to the row rather than to its own content — three
   records of unequal height would otherwise leave the shorter ones' slot rules
   showing under them. THAT IS NO LONGER HYPOTHETICAL: `p` in data.mjs takes
   many paragraphs and the three people ship 108px, 54px and 87px of copy
   block, so the records ARE unequal and this one declaration is what makes
   every slot the height of the TALLEST card. It costs nothing here (the grid
   row is the auto-fit track's own height) and §THE LANDING's flex row and
   §THE DECK's explicit row do the same job by their own mechanisms. */
.bench-member{position:relative;min-width:0;display:grid}
/* Hidden in the static layout because it is invisible there by construction —
   the record sits on it at scale 1 and paints opaque stock over every pixel of
   it. It is not a fallback that was dropped; there is nothing to see. */
.bench-slot{display:none}
.bench-record{display:flex;flex-direction:column;border-top:1px solid var(--graphite);padding-top:clamp(18px,1.6vw,26px)}
/* Square, because the source portraits are square: the crop was done at build
   time in media/, so object-fit has nothing left to cut and a layout change
   cannot crop the face through the eyes. */
.bench-plate{margin:0 0 clamp(16px,1.6vw,24px);aspect-ratio:1;overflow:clip;border-end-end-radius:clamp(24px,3vw,44px);background:var(--mist)}
.bench-plate img{width:100%;height:100%;object-fit:cover}
.bench-record>span{font-size:11px;letter-spacing:.08em;text-transform:uppercase}
.bench-record h3{margin:14px 0 0;font-size:clamp(1.6rem,2.2vw,2.6rem);font-weight:500;line-height:1.02;letter-spacing:-.04em}
.bench-record p{margin:12px 0 0;max-width:34ch;color:var(--steel);font-size:14px;line-height:1.5}
/* A progress rule for a mechanic the static layout does not have would be a
   hairline that means nothing, so it exists only inside the pin. It is the
   Matterline's own device turned on its side, in the same deep stone
   design.md line 32 assigns to "Matterline progress". */
.bench-progress{display:none;margin:clamp(28px,3vw,44px) 0 0}
.bench-progress span{display:block;height:1px;background:var(--accent-deep);transform-origin:0 50%}
/* ONE COLUMN BELOW 900, NOT AN auto-fit GRID, and the reason is that three is
   an odd number. The rail above is `repeat(auto-fit,minmax(240px,1fr))`, and
   swept a pixel at a time that lays down exactly TWO tracks from about 540px
   wide up to and including 826 — 827 is two tracks, 828 is three — which
   leaves the third record alone on a second row under the other two.
   Measured at 768x1024 before this override: two 343px records, then one,
   with 343px of empty ground beside it. A roster of three that renders 2 + 1
   reads as a grid that did not fit rather than as a bench.
   900 is the site's own turnover and not a number invented here: the nav
   becomes a drawer, .proof drops to two columns and .matterline and
   .featured-matter drop to one, all on this line. The 560px cap keeps the
   square plate from becoming a 706px face on a tablet, and leaves the column
   sitting off-centre in the content box, which is design.md line 63's
   "asymmetry survives via 92% width, offset media" rather than a stack. */
@media(max-width:899px){
  .bench-head{width:92%;margin-left:4%}
  .bench-rail{grid-template-columns:minmax(0,1fr);gap:clamp(28px,5vw,48px)}
  .bench-member{max-width:560px}
}
@media(max-width:599px){
  .bench-stage{padding-inline:20px}
  .bench-head{width:94%;margin-left:4%}
  .bench-head h2{font-size:clamp(3rem,13vw,4.2rem);max-width:14ch}
  .bench-note{max-width:none}
  .bench-record{padding-top:20px}
  .bench-record h3{font-size:1.9rem}
}

/* ── §THE REGISTERED LENGTHS, AND THE BUG THAT FORCED THEM ────────────────
   THESE ARE NOT HYGIENE. --bench-pitch in particular is the fix for a
   Chromium calc defect that puts the MIDDLE record of three exactly two gaps
   off its start, and it was reproduced ON THIS PAGE rather than taken on
   trust from the sister site that first hit it — the sister site's write-up
   blames a different half of the expression and does not reproduce here.

   REPRODUCTION, Chromium 143, viewport 1440x900, all four probes injected
   into the shipped page and read back as the computed `translate` of the
   three records at pin progress 0. RE-RUN AT THE TWO-PARAGRAPH CARD rather
   than left standing on the one-paragraph numbers, because the inputs are the
   thing that moved. Inputs: --bench-card 325px (registered), --bench-gap
   clamp(20px,2.2vw,36px) = 31.68px, --bench-pitch 356.68px, --bench-x0
   1296.78px. Correct is x0 - i * 356.68, i.e.

       i=0  1296.78     i=1  940.10     i=2  583.42

     A  shipped: --bench-i * var(--bench-pitch), pitch REGISTERED
        1296.78 / 940.10 / 583.42                                   EXACT
     B  the sum written inline at the use site,
        --bench-i * (var(--bench-card) + var(--bench-gap))
        1296.78 / 940.10 / 583.42                                   EXACT
     C  an UNREGISTERED --probe-pitch holding that same sum
        1296.78 / 1003.46 / 583.42                        WRONG at i=1
     D  an UNREGISTERED --probe-flat holding the literal 356.68px
        1296.78 / 940.10 / 583.42                                   EXACT

   1003.46 - 940.10 is 63.36, which is exactly 2 x --bench-gap — the
   `(n-1)*gap` term of the rail width, distributed with the wrong sign. It is
   the SAME 63.36 the 373px card produced, because the error is the gap term
   and the gap did not move; the defect is indifferent to the card. i=0 and
   i=2 are exact in every variant; ONLY the multiplier 1 is wrong, which is
   why a section built and eyeballed at n=3 can ship the defect and look
   almost right.

   THE TRIGGER IS NARROWER THAN "unregistered". D holds an unregistered
   property and is exact, so it is not the registration of the container. What
   fails is multiplying a <number> by an unregistered property whose value is
   a calc() SUM WITH AN UNRESOLVED TERM in it — and it fails whether the
   operands are registered or not: a variant building the unregistered pitch
   from the REGISTERED --bench-card gave the identical 1027.46. The unresolved
   term here is --bench-gap's clamp(), which survives substitution.

   SO THE RULE THIS FILE FOLLOWS is that --bench-i multiplies ONE REGISTERED
   <length> and nothing else, ever. B happens to be correct in this Chromium
   and is still not used, because it is the same expression shape one
   substitution away from C and nothing in CSS makes that distinction
   visible to the next person editing this block.

   --bench-railw is deliberately NOT registered: it is never multiplied by
   --bench-i, only read into `width`, and it measured exact there (1038.36 =
   3 x 325 + 2 x 31.68 at 1440x900). --bench-x0 IS registered even though it
   is also never multiplied, because it is the value the whole flight starts
   from and a 63px error in it would be the same silent near-miss.

   --bench-i IS NAMESPACED even though build.mjs would happily write `--i`.
   @property registers a name for the whole DOCUMENT, and `--i` is the single
   most likely custom-property name for anything else on this site to claim
   later; a registration that silently coerced someone else's `--i` to a
   <number> would be a very hard afternoon. */
@property --bench-card{syntax:'<length>';inherits:true;initial-value:0px}
@property --bench-pitch{syntax:'<length>';inherits:true;initial-value:0px}
@property --bench-x0{syntax:'<length>';inherits:true;initial-value:0px}
@property --bench-i{syntax:'<number>';inherits:true;initial-value:0}

/* ── §THE LANDING ─────────────────────────────────────────────────────────
   The enhancement, and the LAST word in the cascade so that neither of the
   two width blocks above can reach back into it. Everything before it stands
   on its own; nothing in it is required for the section to be complete, and
   all four gates sit on this block rather than on an unwind of it.

   EVERY CONSTANT IS THE REFERENCE'S, GENERALISED TO n PEOPLE. The four step
   values are stated as the numerator that produces the reference's figure at
   n = 3 — 0.3/(n-1) = 0.15, 0.15/(n-1) = 0.075, 0.24/(n-1) = 0.12 — so a
   fourth person re-times the whole section correctly and no number in this
   file can be falsified by a content edit. max(1, n-1) keeps a one-person
   section painting instead of dividing by zero; build.mjs throws on that case
   rather than leaving it to be discovered here. */
@supports (animation-timeline:view()){
  /* FLOOR 600 -> 780 (2026-11-02). The operator's longer bench copy (heading,
     standfirst, three bios) costs the pinned stage ~110px: measured with the
     old copy every 700-720px-tall window had ~90px to spare, with the new copy
     -17 to -27px (the stage clipped). 740 still clipped wide windows (-26px at
     1600 and 2560 wide, where the height-bound cards make the rail, and so the
     heading's measure, narrow); 780 is the first height with room at every
     width swept (900-2560). Shorter desktop windows get the static
     roster, which shows the same copy at full size. js/main.js (BENCH FALLBACK,
     DESK) and css/content.css (the pinned heading/rail rules) state the same
     floor and must move with it. */
  @media (prefers-reduced-motion:no-preference) and (min-width:900px) and (min-height:600px){
    .bench{
      /* ONE viewport-height of pin per person, which is the reference's
         3*innerHeight at its three. These two lines are the same number and
         must move together: --p1 is the fraction of `cover` that is exactly
         the 100vh BEFORE the pin, and cover spans (1 + pin)*100vh + 100vh. */
      --bench-pin:var(--bench-n,3);
      height:calc((1 + var(--bench-pin)) * 100vh);
      --p1:calc(100% / (var(--bench-pin) + 2));

      /* THE RECORD IS THE SMALLER OF TWO CONSTRAINTS, AND BOTH ARE REAL.
         Width: a third of the stage's content box, which is what a wide, tall
         viewport gets and what the reference uses. Height: the plate is
         SQUARE, so the record's width sets the record's height, and the stage
         only has what the head and the two margins leave it.

         --bench-fit IS THAT SECOND CONSTRAINT, MEASURED, NOT GUESSED, AND IT
         IS FITTED AGAINST THE COPY BLOCK. The record's height is cardW + K,
         where K = recordH - cardW is every row that is not the plate: the top
         padding, the plate's margin, the 11px label, the h3 and its margin —
         all of which follow the viewport — plus THE PARAGRAPH BLOCK, which
         follows the copy. src/data.mjs caps that block at 108px and
         src/build.mjs throws above it (§THE COPY BUDGET IS A HEIGHT), so K is
         a known function of the viewport again:

             K  201-202 at 900 and 1024 wide, 212 at 1280, 219-221 at 1440,
                222-228 at 1600, 223-236 at 1920, 223-237 at 2560, measured
                over the whole sweep below. Subtract the 108px block and the
                remainder is the 93-129px of fixed rows, which is what the old
                one-line copy's 147-182 was measuring.

         224 IS THE NUMBER THE SWEEP PRODUCES, and the sweep is the authority
         rather than any arithmetic here. Read as the STAGE'S LEFTOVER (its
         100vh minus the composition's own top-to-bottom extent) with the
         shipped markup, fonts settled, over 7 widths x 7 heights (900-2560 by
         730-1440, i.e. every cell this gate admits): worst 25.19px at
         2560x800, then 26.08 at 1920x768, 26.17 at 1600x800, 26.58 at
         1920x800, 27.72 at 2560x768. NOT ONE CELL IS NEGATIVE, no record or
         head overflows its own box, and document.scrollWidth never exceeds
         innerWidth. Card sizes it produces: 221.3 at every width at vh 730
         (the gate floor), 244.5 at vh 768, 264 at vh 800, 325 at 1440x900,
         386 at 1440x1000, 420.5 (width-capped) at 1440x1080, 467 at 1600x1440.

         THE CARD IS 48px NARROWER THAN IT WAS AT ONE PARAGRAPH, and that is
         the price of the second one, not a regression: 373 -> 325 at 1440x900.
         The paragraph block went 54px -> 108px and --bench-fit went 61vh-176
         -> 61vh-224, which is the same 48 plus the 6px the tighter floor
         bought back. There is no way to buy it back inside this composition —
         the plate is SQUARE, so every pixel of copy costs a pixel of portrait
         WIDTH, and at three abreast it costs three of them.

         THE GATE FLOOR MOVED WITH IT, 700px -> 730px, AND IT IS THE SAME
         MEASUREMENT. At vh 700 the fit is 61*7 - 224 = 203px, and the opening
         sentence of the copy budget re-wraps from two lines to three at any
         measure below 217px (swept a pixel at a time: 3 lines at 215, 2 at
         217 and at every width up to the 34ch cap). A third line is 21px the
         stage does not have, so the record would grow out of a box that clips.
         730 is the first height whose fit clears 217 with room — 221.3px, 4.3px
         of it — and 900-2560 wide by 700-729 tall now falls to the static
         one-column roster, which has no height budget at all and therefore
         shows the SAME copy at full size. That is the deliberate outcome for a
         window too short for the pin, not a clip. Verified one pixel either
         side: 1440x729 static / 1440x730 the pin, 900x729 static / 900x730 the
         pin, and 899x730 still static because the width term rejects it.

         A 52vh - 150px CAP WAS TRIED FIRST AND WAS WRONG IN ONE DIRECTION
         ONLY: it held the record to 318px at 1440x900 where 404 fits, which
         left a 1017px rail floating inside a 1325px content box with 84px of
         the stage unused. */
      --bench-content:calc(min(var(--max),100vw) - 2 * var(--gutter));
      --bench-fit:calc(61vh - 224px);
      --bench-card:min(calc((var(--bench-content) - (var(--bench-n,3) - 1) * var(--bench-gap)) / var(--bench-n,3)),var(--bench-fit));
      --bench-pitch:calc(var(--bench-card) + var(--bench-gap));
      --bench-railw:calc(var(--bench-n,3) * var(--bench-card) + (var(--bench-n,3) - 1) * var(--bench-gap));
      --bench-x0:calc((100vw + var(--bench-railw)) / 2 + 4vw);

      --rise-step:calc(.3 / max(1,var(--bench-n,3) - 1));
      --rise-win:.7;
      --fly-step:calc(.15 / max(1,var(--bench-n,3) - 1));
      --fly-win:.4;
      --grow-step:calc(.24 / max(1,var(--bench-n,3) - 1));
      --grow-from:.4;

      view-timeline-name:--bench;
      view-timeline-axis:block;
    }
    .bench-stage{position:sticky;top:0;height:100vh;padding-block:0;display:grid;grid-template-rows:auto auto auto;align-content:center}
    /* ── THE PINNED STAGE IS ONE CENTRED COLUMN THE WIDTH OF THE RAIL ──────
       The static layout puts the head on a 12% left rail (design.md line 58,
       and the neighbours are .work-lead at 16% and .contact h2 at 8%). Inside
       the pin the head, the rail and the progress rule are all bound to
       --bench-railw and centred instead, and that is not a second opinion
       about the left rail — it is the only arrangement that cannot drift.
       --bench-railw is n*card + (n-1)*gap and the card is capped by HEIGHT
       (--bench-fit), so the rail is narrower than the content box on most
       viewports and by an amount that changes with the viewport. Anything
       laid out against the content box instead lines up with the rail at
       exactly one card width and is visibly out at every other: at 1440x900
       the stage's content box runs 57.6 to 1382.4 while the rail runs 200.8
       to 1239.2, so a content-box progress rule overhangs the rail it is
       reporting on by 143.2px at each end. It shipped that way for one build.
       Measured after the fix, same viewport: head 200.81-1239.17 (1038.36
       wide), rail 200.83-1239.17 (1038.34), rule 200.81-1239.17 (1038.36) —
       the three agree to two hundredths of a pixel, and that is the grid's own
       rounding. THE OVERHANG GREW WITH THE COPY BUDGET, which is the point:
       the rail is n*card + (n-1)*gap and the card is now 325px rather than
       373px, so the same content-box mistake would now be 143.2px out instead
       of 71.2px. Nothing here had to change for that — the rule is bound to
       --bench-railw, so it tracked. */
    .bench-head,.bench-progress{width:var(--bench-railw);margin-inline:auto}
    .bench-head h2{font-size:min(clamp(2.8rem,5.4vw,5.8rem),7vh)}
    .bench-note{margin-top:min(52px,3.6vh);font-size:min(1rem,2vh)}
    /* ── THE SKIP CONTROL SHARES THE STANDFIRST'S ROW, AND THAT IS A HEIGHT
       DECISION, NOT A TASTE ONE ────────────────────────────────────────────
       Everywhere else on the page §THE NEXT-SECTION CONTROL is ordinary flow
       content and simply makes its section taller. Here it cannot be: the head
       is INSIDE the pinned stage, whose whole budget is one viewport, and
       --bench-fit (61vh - 224px above) is fitted against the head's measured
       height R. RE-MEASURED A/B AT TWO PARAGRAPHS, one browser, fonts settled,
       over the block's own sweep — 7 widths x 7 heights, 900-2560 by 730-1440,
       leftover = stage height minus (last child's bottom - first child's top):
           shipped, control shares the standfirst row   worst  +25.19px
           control absent                               worst  +25.19px
           control as a third row in the head           worst  -36.61px
       (shipped at 2560x800, absent at 1920x800, third row at 1920x800 and
       2560x800) — i.e. a row of its own overflows the pin by 36.6px at 32 of
       the 49 cells and `.bench{overflow:clip}` eats the bottom of the progress
       rule. Shrinking --bench-fit to pay for it was the alternative and it
       costs the section far more than it costs the control: another 36px off
       the fit takes the 1440x900 record from 325px to 289px to buy a 44px
       button a line of its own.
       So the head becomes two columns for its second row and the control sits
       at the left of it, bottom-aligned with the last line of the standfirst.
       Row 2's height is max(note, control) instead of note + control, and the
       note is the taller of the two at every cell in the sweep, so R does not
       move.
       THE CONTROL IS GONE AND THE TWO COLUMNS ARE DELIBERATELY KEPT. The
       operator removed .bench's next-jump (src/build.mjs emits none), so the
       `.bench-head .next-jump` placement rule and the `[hidden]{display:flex}`
       that opted its reserved box back in are both deleted — they match
       nothing now. The grid itself is NOT a leftover: it is what puts
       .bench-note in the right-hand track, which is the shipped composition,
       and collapsing back to plain flow would run the standfirst the full
       width of the head.
       ROW 2 IS NOW THE NOTE ALONE, AND THAT COSTS THIS BLOCK NOTHING, which
       is the max(note, control) sizing above paying off one last time: the
       note was the taller of the two at every cell in the sweep, so dropping
       the control cannot shrink the row. Measured before/after on the served
       build, inside this gate: .bench is 3200px at 1280x800, 3600px at
       1440x900 and 4000px at 1920x1000 in BOTH arms — zero delta at all three
       — and .bench-note's box is identical in both arms too (520x99.2 at
       x=544.2 at 1280x800; 520x99.2 at x=719.2 at 1440x900). R does not move.
       OUTSIDE this gate it does, and that is expected rather than a
       regression: under §THE DECK the head is ordinary flow and the control
       was a row of its own, so .bench loses exactly that row — measured −78px
       at 768x1024 and −66px at 390x844.
       h2 still spans both columns; nothing else in the head is touched. */
    .bench-head{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end}
    .bench-head h2{grid-column:1/-1}
    .bench-note{grid-column:2}
    .bench-rail{display:flex;width:max-content;max-width:none;margin:min(clamp(40px,5vw,68px),5vh) auto 0}
    .bench-member{flex:0 0 var(--bench-card)}
    /* Descending, so the record with the furthest to travel passes OVER the
       ones it crosses rather than under them. The reference does the same with
       three explicit z-indexes; this is the same rule written once. */
    .bench-member{z-index:calc(var(--bench-n,3) - var(--bench-i))}
    .bench-slot{display:grid;place-items:center;position:absolute;inset:0;border-block:1px solid var(--line)}
    /* THE LETTER IS THE ONLY THING IN THE EMPTY SLOT, so it carries the whole
       beat between the slots landing and the records arriving. --line on
       --field is 8% of lightness apart and reads as an empty box in the
       frames; --accent-deep mixed back toward the field is the same restraint
       at a tone that is actually visible. The --line line above it is the
       fallback for engines without color-mix and is the old value, so nothing
       gets worse where the mix is unsupported. */
    .bench-slot span{font-size:calc(var(--bench-card) * .62);line-height:.72;font-weight:500;letter-spacing:-.06em;color:var(--line);color:color-mix(in srgb,var(--accent-deep) 34%,var(--field))}
    /* The record is opaque so that it covers its own slot exactly, and it is
       painted in the band's OWN stock rather than a colour of its own —
       design.md gives --paper to navigation only and --shelf to sections only,
       and a record with a ground the palette does not have is a new tone on
       the page. Both bands are declared because the tone is positional: an
       editor reordering sections must not leave a field-coloured record on a
       shelf ground. */
    main>[data-band="light"] .bench-record{background:var(--field)}
    main>[data-band="shelf"] .bench-record{background:var(--shelf)}
    .bench-record{height:100%}
    /* Bigger than the static layout's, because in the pin the record is the
       whole composition rather than one cell of a grid. */
    .bench-record h3{font-size:min(clamp(1.7rem,2.4vw,2.9rem),4.4vh)}
    .bench-progress{display:block;margin-top:min(clamp(28px,3vw,44px),4vh)}

    /* ST-A. Member i rises over [i*step, i*step + win] of the 100vh BEFORE the
       pin, and its initial scales over the last 60% of its own window. */
    .bench-member,.bench-slot span{
      animation-name:bench-rise;animation-duration:auto;animation-timing-function:linear;
      animation-fill-mode:both;animation-timeline:--bench;
      animation-range:cover calc(var(--bench-i) * var(--rise-step) * var(--p1))
                      cover calc((var(--bench-i) * var(--rise-step) + var(--rise-win)) * var(--p1));
    }
    .bench-slot span{animation-name:bench-letter}

    /* ST-B. Two animations on one record, on two different slices of the pin,
       kept apart by using the INDEPENDENT transform properties: the fly-in
       owns `translate` and `rotate`, the settle owns `scale`. One @keyframes
       block writing `transform` could not do this — the later animation would
       replace the earlier one's whole matrix instead of composing with it.
       CSS applies them translate -> rotate -> scale, which is also the order
       GSAP composes, so the translate is not scaled at either end. */
    .bench-record{
      animation-name:bench-fly,bench-grow;animation-duration:auto,auto;
      animation-timing-function:linear,linear;animation-fill-mode:both,both;
      animation-timeline:--bench,--bench;
      animation-range:contain calc(var(--bench-i) * var(--fly-step) * 100%)
                      contain calc((var(--bench-i) * var(--fly-step) + var(--fly-win)) * 100%),
                      contain calc((var(--grow-from) + var(--bench-i) * var(--grow-step)) * 100%)
                      contain 100%;
    }
    .bench-progress span{animation-name:bench-fill;animation-duration:auto;animation-timing-function:linear;animation-fill-mode:both;animation-timeline:--bench;animation-range:contain 0% contain 100%}

    @keyframes bench-rise{from{translate:0 125%}to{translate:0 0}}
    @keyframes bench-letter{from{scale:0}40%{scale:0}to{scale:1}}
    @keyframes bench-fly{
      from{translate:calc(var(--bench-x0) - var(--bench-i) * var(--bench-pitch)) 0;rotate:20deg}
      to{translate:0 0;rotate:0deg}
    }
    @keyframes bench-grow{from{scale:.75}to{scale:1}}
    @keyframes bench-fill{from{transform:scaleX(.04)}to{transform:scaleX(1)}}
  }
}

/* ── §THE DECK — THE SAME SECTION HELD UPRIGHT, ONE PERSON AT A TIME ───────
   Below 900px wide the section above hands over to the static one-column
   grid, which is complete but is not the effect. This block is the effect for
   that shape of screen: the same slot-and-record vocabulary, re-staged as a
   HANDOVER — one record at a time flies in from the right, holds, and leaves
   to the left as the next arrives, with the empty ruled frame and the next
   person's initial showing in the gap between them.

   IT USED TO STOP AT 599px WIDE, and 600-899 got neither this nor §THE
   LANDING: a portrait tablet fell all the way through to the static roster.
   The operator asked for the deck there too. What that took was a real
   re-tuning rather than a wider gate — see THE TABLET BAND below — because
   at 768 wide every number in this block had been sized for a 390px screen.

   THERE IS NOTHING TO PORT. The codegrid reference this section came from
   gives up below 1000px: its only media query is `@media (max-width:1000px)`
   and it kills its own animation with !important —
     .team{height:250svh;flex-direction:column}
     .team-member{max-width:400px;transform:translateY(0)!important}
     .team-member .team-member-card{transform:translate(-50%,-50%) scale(1)
                                    rotate(0deg)!important}
   a plain stacked column, no flight and no stagger. So every number below is
   this site's, measured here; none of it is copied.

   ── WHY IT CANNOT BE THE DESKTOP LAYOUT, SHRUNK ───────────────────────────
   Three records side by side needs three widths plus two gaps across 350px of
   content, which is 98px each and a 98px portrait. Stacked instead — which is
   what the static layout already does — three records at the size this block
   gives them are 3 x 556.5 + 2 x 28 = 1725.5px against an 844px pane, and
   1725.5 against the 659px pane the URL-bar test below uses for the same
   handset. Impossible, not a taste call. A PILE was rejected on content
   rather than geometry: the section exists to introduce three people, and a
   stack that overlaps them hides the two it has just introduced.
   AT 768 WIDE THE SAME ARITHMETIC STILL SAYS ONE AT A TIME, and it is not a
   restatement of the phone case: the content box there is 768 - 2 x 30.72 =
   706.6px and --bench-gap is clamp(20px,2.2vw,36px) = 20px, so three abreast
   is (706.6 - 40) / 3 = 222.2px each — a 222px square portrait of a person,
   under the 240px minmax floor the static rail itself declares. The static
   block above independently reached the same conclusion from the other end
   and its comment records the sweep: `repeat(auto-fit,minmax(240px,1fr))`
   lays down exactly TWO tracks from ~540px wide to 827 inclusive, so a tablet
   gets 2 + 1, "a grid that did not fit rather than a bench". 900 is where
   three abreast becomes honest, which is exactly where §THE LANDING starts.

   ── THE HEAD LEAVES THE PIN, AND THAT IS WHAT PAYS FOR THE RECORD ─────────
   .bench-head is 407.2px tall at 390 wide, measured (h2 at
   clamp(3rem,13vw,4.2rem) over three lines, plus .bench-note, which is the
   sentence disclosing that the people are invented and therefore cannot be
   dropped, plus the skip control's own row — this figure was 340.7px before
   §THE NEXT-SECTION CONTROL put a button at the end of the head, and 66.5px is
   what that button and its 28px margin cost at this width). Inside the pin, on
   a 659px pane, that would leave 659 - 407.2 - 2*40 = 171.8px for the record;
   the record's height is its width + 206.5, so on a 390px screen there is no
   positive width at all — the copy block alone outspends the pane before the
   portrait gets a pixel. So the head is ordinary flow content ABOVE the pin
   and the whole pane belongs to one person.
   THAT IS ALSO WHY THIS BLOCK NEEDED NO SPECIAL CASE FOR THE SKIP CONTROL
   while §THE LANDING did. PAST TENSE NOW: .bench ships no control at all
   (src/build.mjs emits none for it, and the two `[hidden]{display:flex}`
   opt-ins that used to sit in this block and in §THE LANDING are deleted), so
   there is no button here to make room for in either treatment. The asymmetry
   is recorded because it is the reason the two blocks still differ: here the
   head was outside the pin, so the button was simply a row of its own and the
   section grew by it — measured on removal, .bench loses 78px at 768x1024 and
   66px at 390x844, which is that row. In the desktop pin the head is INSIDE a
   one-viewport budget and the same button overflowed it by 45.4px, which is
   why §THE LANDING folded it into a two-column second row instead; that grid
   survives the removal because it is also what places .bench-note.

   ── CONSEQUENCE: THE TIMELINE SUBJECT IS .bench-rail, NOT .bench ──────────
   The desktop block names the timeline on .bench and sticks .bench-stage.
   Here .bench contains the head, so the subject has to be the rail alone. The
   rail takes an explicit height and ONE grid area (grid-template-rows:1fr is
   load-bearing: it makes that area, not the rail's content, the sticky
   containing block), and every member is placed in it, so the n members are n
   identical `top:0; height:100svh` panes sharing one flow position and one
   stick interval. That interval is exactly `contain 0%` -> `contain 100%` for
   a subject taller than the scrollport, which is what the animations are
   ranged against. Measured at 390x844 by binary search on scrollY, tolerance
   +-0.5px: rail top 6667.42, rail height 2743, the pane's own top first
   reaches 0 at 6666.5 and last holds 0 at 8566.5; the contain range runs
   6667.42 -> 8566.42. Same interval at both ends, inside the search's own
   tolerance.

   ── THE BEAT ─────────────────────────────────────────────────────────────
   75svh per person, so the pin is n*75svh and the rail is (n*75 + 100)svh —
   the trailing 100 is the pane's own height, not dead air. Each person's
   window is 1.5 beats, [i/n, (i+1.5)/n] of the pin, with ONE animation over
   it and the three landmarks written as keyframe offsets:
       0%      off right, tilted   |  arrive
       33.33%  home                |  hold
       66.67%  home                |  leave
       100%    off left, tilted
   so record i lands at (i+0.5)/n and record i-1 finishes leaving at exactly
   the same offset. At n=3 that is 16.7 / 50 / 83.3% of the pin. It is ONE
   animation and not two because animation-fill-mode:both makes a second
   animation on the same property override the first OUTSIDE its own range —
   the same trap the desktop block dodges by splitting `translate` from
   `scale`, which is not available here because the arrive and the leave both
   want `translate`.

   THE LAST PERSON HOLDS, AND THE CLAMP IS ARITHMETIC, NOT A SPECIAL CASE.
   Member n-1's window ends at (n+0.5)/n — past the end of the pin — so its
   leave would otherwise play in the 100svh of rail that is still scrolling
   after the pin releases, and it would play IN FULL and ON SCREEN: at 390x844
   the leave is 316.5px of scroll and the record's bottom edge needs 673.25px
   to clear the top of the viewport. So the leave's distance is multiplied by
   --bench-go = min(1, max(0, n - 1 - i)), which is 1 for every member except
   the last and 0 for the last. Its `to` keyframe then equals its `hold`
   keyframe and it holds through the end of the section. Checked on real touch
   passes at 390x844: the last record is still at x 20 and width 350 at every
   sample from pin 89.6% out past 131.9%, and after a 3.2k flick at 165.6%.

   WHAT THE BEAT CANNOT DO is outrun a fling. Touch-dragged through CDP so the
   compositor generates the coast, hand-sized drags settle 76-193px each and
   put 12 samples inside the pin, which reads. One hard flick from the top of
   the pin coasted 3166px — the whole 1899px pin and past it — and the section
   is correct at the other end (last record home, scrollWidth still 390) but
   not read. That is reported, not fixed: no beat anyone would choose is
   longer than a fling.

   ── THE TWO MOVING RECORDS NEVER OVERLAP ─────────────────────────────────
   Both travel --bench-in over the same window in opposite directions, so
   their centres stay exactly --bench-in apart for the whole handover and the
   only thing that varies is how wide each one's rotated box is. RE-SWEPT AT
   THE TWO-PARAGRAPH RECORD, 300 steps across the whole pin, the closest the
   two axis-aligned boxes ever come, over the thirteen viewports the widened
   gate has to cover (the one-paragraph figure in brackets):
       320x600  121.40 @75.3% [130.20]   600x600  381.66 @75.3% [358.69]
       390x659  150.18 @41.7% [159.04]   600x1200 241.43 @74.3% [250.01]
       390x844  150.21 @41.3% [158.97]   768x1024 404.72 @41.0% [413.32]
       430x932  166.60 @41.7% [175.42]   800x1000 436.48 @42.0% [445.53]
       450x600  231.66 @75.0% [208.67]   820x1180 456.51 @74.7% [465.30]
       599x900  236.18 @42.7% [244.99]   899x899  535.51 @75.7% [544.32]
                                         899x1200 535.48 @42.0% [544.32]
   — and over the same sweep that is also the only moment two records are on
   screen together at all, so nothing overlaps that a reader can see. THE GAP
   MOVED IN BOTH DIRECTIONS AND BOTH ARE THE SAME ARITHMETIC: where the card
   width did not change (the phones, and the tablets on the 560 cap) the taller
   record makes the rotated box wider, so the gap loses 8-9px; where the HEIGHT
   term binds (450x600, 600x600) the taller record makes the card NARROWER, so
   the gap gains 23px. The worst case is still the narrowest phone at 121.40px
   and the margin still only widens with the viewport.
   --bench-in is 100vw + half a record, which is also what puts the record
   fully off screen at both ends of its travel: at rest the record is centred,
   so the unrotated clearance is exactly 100vw/2 and the rotated box gives
   back (rotated half-width - card/2). Measured at the leave extreme, 141.54px
   at 390x844 and 317.11px at 768x1024, against 111.02px at 320x600 which is
   still the worst — each about 8px down on the one-paragraph record for the
   same reason the gaps are, and none of them near zero.

   ── THE FRAME EMPTIES AND REFILLS; THE INITIAL IS THE COUNTER ────────────
   All n slots sit in the same cell at the same size, so their hairlines
   coincide and read as ONE frame that never moves. What changes inside it is
   the initial. Three things make that work and each is doing a job:

   1. THE SLOT IS EXACTLY THE RECORD'S BOX, by a different mechanism than the
      desktop's. There the slot is position:absolute inset:0 of the member;
      here the member IS a 100svh pane, so instead the slot and the record are
      two items in ONE grid cell.
      THE CELL IS DECLARED, NOT SIZED BY THE RECORD, and that changed when `p`
      became many paragraphs. It used to be an implicit row that the taller of
      the two items sized — which is fine while every person carries the same
      copy and WRONG the moment they do not: the three shipped blocks are
      108px, 54px and 87px, so an implicit row would give each person a frame
      of a different height and the hairlines would jump on every handover,
      destroying the one thing this treatment claims ("ONE frame that never
      moves"). `grid-template-rows:calc(var(--bench-card) + var(--bench-k))` is
      the fix, and it is the same statement as "the slot is the TALLEST card":
      --bench-k is the fixed rows plus the 108px ceiling build.mjs enforces, so
      the row is exactly the tallest record a copy edit is allowed to produce
      and every shorter record stretches into it (grid items are align-self:
      stretch, and .bench-record is a flex column, so the slack lands after the
      last paragraph).
      Sampled in the middle of each person's hold, where the record's computed
      translate/rotate/scale are exactly 0px/0deg/1: slot and record agree on
      x, y, width and height to 0.000px, for all three people, at all fourteen
      gate viewports (320x600, 390x659, 390x844, 430x932, 450x600, 599x600,
      599x900, 600x600, 600x1200, 768x1024, 800x1000, 820x1180, 899x899,
      899x1200); all three slots are the same height at every one of them; and
      the tallest record's last line sits exactly on the row's bottom edge
      (0.00px) while the other two end 54.0px and 21.0px above it.
   2. DESCENDING z-index IS A BUG FIX, not decoration. position:sticky makes
      every member a stacking context, and without a z-index DOM order paints
      the LATER member on top — so the next person's slot hairlines cross the
      resting record. Rendered with the z-index removed and diffed against the
      shipped build at 390x844: exactly two rows differ, y = 171 and y = 672,
      350px each, worst channel delta 182 — the record's own graphite top rule
      overpainted in --line, and a --line rule added along its bottom edge.
      Descending order also makes the resting record MASK the next slot, which
      is what lets the next initial grow hidden and be revealed as the record
      slides off. That reveal is the whole handover cue.
   3. THE SLOT FADES OUT UNDER ITS OWN RECORD. Member i's hairlines are above
      member i+1's record, so if they stayed lit they would paint a --line
      rule over the arriving record's own graphite top rule. They cannot fade
      during the leave either — the record uncovers the frame from the right
      as it goes, and a fading 1px stub would be visible. So the fade runs
      over local 40% -> 60%, which is inside the HOLD, where an opaque record
      covers every pixel of the slot. Nothing is visible at either edge of it.

   THE INITIAL grows over the first quarter of its own window, which is the
   gap: at local 0% the previous record has only just started to move off it.

   ── EXIT TILT IS +20deg, THE SAME SIGN AS THE ARRIVAL ────────────────────
   The desktop fly-in is rotate 20deg -> 0. Leaving with 0 -> +20deg means
   both records are tilted the same way at the ends of the handover and read
   as one deck being dealt. Both signs were RENDERED at 390x844 at pin 41.7%,
   the moment the gap is at its narrowest: at +20 the departing and arriving
   edges are parallel; at -20 they splay outward and the gap between them is a
   wedge. The 12% scale dip at each end is the reference's own settle (scale
   .75 -> 1), reduced: .75 was rendered at the same offset and puts a 262.5px
   record beside a 350px one, an 87.5px size step between two planes the
   reader is asked to read as the same object, on a screen the record already
   fills 90% of. .12 makes that step 42px.

   ── SIZES: --bench-k IS THE FRAME, AND IT IS DECLARED ────────────────────
   The record's height is its own width (the plate is square) plus K, the rows
   that are not the plate. Every one of those rows is a fixed px value inside
   THIS block — padding-top 20 and h3 at 1.9rem are declared here now, not
   inherited (see the note on those two lines), plate margin
   clamp(16px,1.6vw,24px) which is 16 across the whole gate, an 11px label,
   `p` at 14px/1.5 capped at 34ch — and the fixed part of that measures 98.5px
   flat across the gate. K = 98.5 + the paragraph BLOCK.

   --bench-k IS NOW A CEILING RATHER THAN AN OBSERVATION, and that is the
   change multi-paragraph copy forced. 206.5 = 98.5 fixed rows + the 108px
   block src/build.mjs refuses to exceed, so it is the tallest record the copy
   budget can produce; `.bench-member{grid-template-rows:calc(card + k)}` makes
   it the frame for EVERY person, and a person carrying less copy stretches
   into it instead of shrinking the frame. The three shipped blocks are 108px,
   54px and 87px, so two of the three do exactly that.
   THE WRAP CLIFF, re-measured a pixel at a time on the shipped sentences: the
   block is flat at 108px from a 217px measure all the way up (above 306px the
   34ch cap holds the measure there), and at 215px the opening sentence takes a
   third line and the block becomes 129px. The record is
   min(100vw - 2*pad, 100svh - 2*40 - 206.5, 560), whose smallest value
   anywhere in this gate is 280px at 320x600 — 63px clear of the cliff, and the
   height term cannot go below 313.5px because the gate floor is 600px tall.
   WHAT WOULD MOVE IT: a longer name, a bigger h3, or a copy block over 108px —
   the last of which build.mjs throws on, at the same 108 this number is built
   from.
   --bench-air 40px is the clearance above and below the record.
   MEASURED OVER THE WHOLE WIDENED GATE, reading the record's untransformed
   offsetWidth/offsetHeight (getBoundingClientRect is the ROTATED box — the
   records carry rotate 20deg and scale .88 at the ends of their windows, and
   a first pass of this table read 361.71px for a 280px record because of it):
       viewport   pad     card    K       air        h3
       320x600    20      280     206.5   56.75      30.4px
       390x659    20      350     206.5   51.25      30.4px
       390x844    20      350     206.5   143.75     30.4px
       430x932    20      390     206.5   167.75     30.4px
       450x600    20      313.5   206.5   40.00      30.4px
       599x600    20      313.5   206.5   40.00      30.4px
       599x900    20      559     206.5   67.25      30.4px
       600x600    24      313.5   206.5   40.00      30.4px
       600x1200   24      552     206.5   220.75     30.4px
       768x1024   30.72   560     206.5   128.75     30.4px
       800x1000   32      560     206.5   116.75     30.4px
       820x1180   32.8    560     206.5   206.75     30.4px
       899x899    35.96   560     206.5   66.25      30.4px
       899x1200   35.96   560     206.5   216.75     30.4px
   K is 206.5 at every one BY DECLARATION now, and the narrowest record is
   still 280px at 320x600. --bench-air is a FLOOR and it is exactly met, not
   merely respected: 40.00px is the worst case, and it is worst exactly where
   the HEIGHT term is the binding one, because there card = h - 286.5 and the
   air is (286.5 - 206.5)/2 by construction whatever h is. That needs h - 286.5
   to be at or below both 100vw - 2*pad and 560, so it needs h <= 846.5 AND a
   viewport at least h - 286.5 + 2*pad wide. Where the WIDTH term binds instead
   the record is narrower than the pane can afford and the air grows (56.75 at
   320x600, 220.75 at 600x1200); where the 560 CAP binds the air is
   (h-766.5)/2, which is the 66-217px in the tablet rows. Nothing overflows its
   own box — the tallest record's last line lands on the row's bottom edge to
   0.00px at every one of the fourteen — and document.scrollWidth equals
   innerWidth at all fourteen and at 3,913 samples of a full scroll pass over
   thirteen of them.
   THE TABLET CARDS DID NOT MOVE AND THE SHORT-PANE ONES DID: 560 is still 560
   wherever the cap binds, and 367.5 became 313.5 wherever the pane's height
   binds, which is exactly the 54px the second paragraph added to K. That is
   the deck's answer to a taller card — it spends the pane's own slack first
   and only then narrows the record, and --bench-air never drops below its
   floor at any point in the trade.

   ── EVERY LENGTH IS svh. NOT ONE IS dvh, AND THAT IS THE POINT ───────────
   design.md line 91 says "no pinned `dvh` elements", and dvh is precisely the
   unit that re-lays-out under the reader when a phone's URL bar collapses
   mid-gesture. svh is the smallest, stable value; a section sized in it is
   the same size before and after the bar retracts, and the extra scrollport
   the reader gains is absorbed by the last person's half-beat hold: `contain
   100%` is railBottom - scrollportHeight, so a taller scrollport ends the pin
   EARLIER, never later, and the last person holds for 16.7% of the pin, which
   is 247.6px of scroll at 390x659 against the 86px the bar is worth.
   HELD-OFFSET TEST, because headless has no retractable bar and resizing it
   moves svh, lvh and dvh together: the whole stylesheet is frozen to px first
   — vh/lvh at the large viewport 745, svh at the small one 659, which is what
   a device does when the bar retracts, nothing changes size — and then the
   scrollport ALONE is grown 659 -> 745 at a held scroll offset. Shipped
   (svh) against a control arm whose deck lengths are re-declared at their 745
   values, which is what dvh would do:
       record 0 at rest              0.0px      dvh control  43.0px
       record 1 at rest              0.0px                   43.0px
       record 2 landed, final hold   0.0px                   43.0px
       mid-handover, worst          93.5px                  184.2px
   The mid-handover figure is not a jump in the layout — nothing resized — it
   is the timeline reaching further along its own range because the range got
   shorter, and it applies to a record that is already in flight.

   ── WHAT THE GATE IS, AND WHY EACH OF THE FOUR TERMS IS THERE ────────────
   (prefers-reduced-motion:no-preference) and (max-width:899px) and
   (min-height:600px) and (max-aspect-ratio:1/1)

   899 is the site's own turnover and not a number invented here: the nav
   becomes a drawer, .proof drops to two columns, .matterline and
   .featured-matter drop to one, and .bench itself drops to one column with a
   560px cap, all on this line — and 900 is where §THE LANDING begins, so the
   two gates ABUT and there is no width a portrait screen can fall between.
   600 tall is where --bench-air stops being satisfiable: at 600 tall the
   record is h - 286.5 = 313.5px whatever the width, with exactly 40.0px above
   and below, and one pixel shorter takes it under the floor. 286.5 is
   2 x --bench-air + --bench-k, so the second paragraph moved this arithmetic
   without moving the gate — the record at the floor went 367.5px -> 313.5px
   and the 40.0px air is still exactly met rather than merely respected.

   THE ASPECT TERM WAS 3/4 AND THAT IS WHAT HAD TO CHANGE, not the width.
   max-aspect-ratio:3/4 means width/height <= 0.75, which is NOT "portrait" —
   it is "portrait and then some", and it throws away most of the tablets this
   change exists for. Worked through with real viewport sizes rather than
   device sizes, because the media query reads the VIEWPORT and browser chrome
   eats height:
       768x1024  0.7500  admitted by 3/4 by exactly nothing
       810x1080  0.7500  admitted by 3/4 by exactly nothing
       820x1180  0.6949  admitted
       834x1194  0.6985  admitted
       744x1133  0.6566  admitted
       800x1000  0.8000  EXCLUDED by 3/4
       768x954   0.8050  EXCLUDED by 3/4 — and this is the ordinary case, not
                         an odd one: it is a 768x1024 tablet with ~70px of
                         browser chrome. The two rows that 3/4 admits "by
                         exactly nothing" are the ones no real browser
                         actually reports.
   So 3/4 does not deliver portrait tablets at all; it delivers a slide rule.
   1/1 is the honest reading of "portrait or square": width <= height. It
   admits every row above, and it keeps every exclusion the 3/4 term was
   there for:
       844x390   2.1641  EXCLUDED, and twice over — the height term rejects it
                         as well, because a phone held sideways is 390 tall.
                         This is the named case and it is not close.
       896x414   2.1643  EXCLUDED (iPhone 11 landscape), height too
       915x412   2.2209  EXCLUDED by width; §THE LANDING then rejects it on
                         min-height:730, so a landscape phone gets the static
                         grid whichever side of 900 it lands on
       899x600   1.4983  EXCLUDED — a short wide window is not a tablet held
                         upright, and the aspect term is the only thing that
                         says so now that the width term reaches 899
       601x600   1.0017  EXCLUDED by 1.7 thousandths, which is the term doing
                         its job at the boundary rather than near it
   WHAT 1/1 ADMITS THAT 3/4 DID NOT, inside the old width range, is 451-599
   wide at 600-798 tall (a resized desktop window, no handset is that shape)
   and 599x600 itself. Geometry there is the same as everywhere else in the
   gate — 367.5px record, 40.0px air, no overflow — so it is admitted rather
   than specially excluded.

   ── THE TABLET BAND: 560px, AND WHY THE RECORD IS NOT JUST BIGGER ────────
   A tablet is not a big phone, and the arithmetic scaling to 706px does not
   make 706px right. Three things are tuned for 600-899 and each is measured:
     1. --bench-cap 560px. Not new — it is `@media(max-width:899px){
        .bench-member{max-width:560px}}` above, whose own comment gives the
        reason ("keeps the square plate from becoming a 706px face on a
        tablet"). Reusing it means a person is the SAME SIZE in the deck and
        in the static grid at the same width, which is the only way the two
        treatments can agree. Uncapped, 768x1024 gives a 706.6px record.
     2. --bench-pad follows the stage instead of restating it, so the width
        budget is right in both bands (20px below 600, var(--gutter) = 24 to
        35.96px above) rather than 40px everywhere, which would have been
        11.9px wrong per side at 899.
     3. The record's own type is declared in this block rather than inherited
        from the 599 block, which is what keeps the FIXED part of K flat at
        98.5 — see the note on those two lines for the 6.4px step that appears
        without it. That flatness is what lets --bench-k be one declared
        number for the whole gate rather than a per-band one.
   The head is NOT re-tuned: it is ordinary flow content above the pin at
   every width this gate covers, it keeps the site's own responsive type, and
   design.md line 63's offset (92% width on a 4% rail at <=899) is what keeps
   the asymmetry the centred record does not carry.

   BOUNDARIES VERIFIED ONE PIXEL EITHER SIDE, by reading which layout is live
   (deck = .bench-member sticky AND the rail gridded AND .bench-record with a
   live animation-name; desktop = .bench-stage sticky; neither = static):
     390x599 static / 390x600 deck        320x599 static / 320x600 deck
     599x599 static / 599x600 deck        600x600 deck   / 601x600 static
     600x600 deck   / 600x601 deck        899x900 deck   / 900x900 desktop
     899x899 deck (aspect exactly 1.0)    899x699 static / 900x699 static
     844x390 static    896x414 static     915x412 static    1024x600 static
     768x1024, 768x954, 800x1000, 810x1080, 820x1180, 834x1194, 744x1133,
     700x1000, 600x1200, 899x1200 all DECK — every one of them static before.
     1024x768 and 1024x1366 both §THE LANDING, unchanged: 1024 was never in
     the gap, and 1024x1366 is a portrait tablet that fits three abreast at
     299px each, which is what the desktop block is for.
   No viewport in that table is in two treatments and none is in neither by
   accident; the seven that are in neither — 844x390, 896x414, 915x412,
   899x699, 900x699, 1024x600 and 601x600 — are all landscape or
   short-and-wide, and the static grid is the correct answer for every one.
   A HOLE STILL EXISTS AND IT IS NOT THIS CHANGE'S: 900-2560 wide by under
   730 tall (1024x600, 900x699, 1440x729) takes neither gate, because §THE
   LANDING's min-height is a real geometric floor — its own note derives 730
   from the record's opening sentence re-wrapping below a 217px measure — and
   this block's aspect term correctly refuses a landscape window. Both gates
   say no for a reason; nothing about extending the deck sideways touches it.
   THE HOLE GREW BY 30px OF HEIGHT when the copy budget doubled, from "under
   700" to "under 730", and every viewport in that strip gets the static
   one-column roster with the same two paragraphs at full size.

   ── THE LANDSCAPE TABLET GOES TO §THE LANDING, DELIBERATELY ──────────────
   1024x768 is 900+ wide and 730+ tall, so it takes the desktop pin, and that
   is right rather than incidental: at that viewport --bench-fit is 61vh -
   224px = 244.48px and the width constraint is 300.7px, so the pin lays down
   three 244px records — inside the sweep §THE LANDING already ran (900-2560 x
   730-1440), measured leftover 49.4px, and 4.5px clear of the 240px minmax
   floor the static rail declares. THAT CLEARANCE USED TO BE 52px: the second
   paragraph spent it, and 1024x768 is now the cell where the pin's record is
   closest to the width the static grid would refuse to lay down. It is still
   on the right side of it, and the number to watch if the copy budget ever
   rises again. A screen that is wider than it is tall is a desktop-shaped
   screen; the deck's whole premise is that three records do not fit across,
   and across 1024px they do.

   ── THE FALLBACK IS THE STATIC GRID, AND IT IS NOT A DEGRADED MODE ───────
   Both gates are on the ENHANCEMENT, exactly as the desktop block's four are.
   An engine without scroll-driven animations, or a reader who asked for less
   motion, gets the complete one-column roster with every name, face, role and
   line, and no script is involved in either path. Both were exercised again
   over the widened gate: with the @supports test rewritten to something no
   engine satisfies, 390x844, 768x1024 and 820x1180 fall back to the static
   grid with `animation-name:none`, real records and laid-out text, and every
   section top above .contact unchanged to four decimals; under
   prefers-reduced-motion:reduce, 1440x900, 390x844 and 768x1024 all give the
   static roster with three real records, `animation-name:none`, the desktop
   stage back to position:static, ZERO skip controls (stackBuild returns before
   building) and scrollWidth == innerWidth.
   iOS Safari and Firefox Android were NOT measured — Chromium 143 only —
   which is exactly why the @supports test is the thing that carries them.

   ── THE SECTION GETS TALLER AND THE LID CHAIN DOES NOT MOVE ──────────────
   js/main.js runs STACK_FROM = 0, STACK_COUNT = 3, so every LID advance
   boundary is advStart[i] = staticTop[i+1] - min(stackH[i], svh) and advEnd[i]
   = ceil(staticTop[i+1]) over slots 0-2, all of them ABOVE this section — and
   .bench's own TOP does not move either, only its bottom.
   THE "STACK_COUNT = 4 … slots 0-3" THIS LINE CARRIED WAS STALE THROUGH TWO
   CHAIN CHANGES (0/4 -> 0/2 -> 0/3). Corrected against the shipped constant,
   and the claim survives with more room than it was written with. The tail
   advance stops are the part that does read down here — .bench→.contact is a
   stop — but they are re-read live on every stackMeasure(), so a taller
   .bench moves that stop's END and nothing above it.
   MEASURED AS AN A/B IN ONE BROWSER at ?v=19: this build against the same
   build with the LAST @supports condition rewritten to something no engine
   satisfies, so the only difference between the arms is this block.
       viewport   .bench height   top delta, deck ON minus deck OFF
       390x844    2144 -> 3390    hero, proof, practices, matterline, work
       768x1024   2667 -> 3880    and bench: 0.0000px at all three viewports.
       820x1180   2673 -> 4388    contact: +1246.00 / +1212.44 / +1714.22
   Six of the seven section tops are identical to four decimal places at every
   viewport, and the seventh moves by exactly what .bench grew. 768x1024 and
   820x1180 are viewports that took the static grid before this change, so
   that A/B is also the direct measurement of what extending the gate costs
   the chain: nothing. Stated plainly, the deck-OFF arm also has no skip
   control in .bench-head — §THE NEXT-SECTION CONTROL asks the stylesheet
   whether a section is animated and gets "no" there — so ~78px of each height
   delta is the control rather than the pin. It does not touch the claim being
   made, which is about the six tops that did not move at all.
   THE NUMBERS IN THE ADVANCE TABLE DID MOVE AT ?v=19, AND NOT BECAUSE OF THIS
   BLOCK. §THE NEXT-SECTION CONTROL became five in-flow buttons at the same
   revision and each one makes its section taller, which moves every section
   top below the hero. That is the control's entry in the JS_V changelog to
   account for, and it is accounted for there; what this block is responsible
   for is that .bench's top is not one of the things that moved.

   ── .bench-progress STAYS HIDDEN, AND THAT IS A DECISION ─────────────────
   The desktop pin turns the progress rule on; this one does not, and the base
   `display:none` is left to stand. It is a sibling of the rail, not of a
   member, so inside this layout it would sit in flow BELOW a 325svh rail and
   be off screen for the entire pin — there is nowhere to put it that is not
   inside somebody's pane. It is also redundant here in a way it is not on
   desktop: three records arriving side by side have no other counter, whereas
   here the initial in the frame names which of the three the reader is on.

   ── IT DEPENDS ON `overflow:clip` FOR A SECOND REASON NOW ────────────────
   The folio rule at the top of this file gives .bench `overflow:clip`, and
   the desktop block's note explains why `clip` rather than `hidden` (hidden
   makes the box a scroll container, and a scroll container scrolls itself
   when a focusable descendant takes focus). `hidden` would now break more
   than that: a scroll container ancestor is the scrollport a view timeline
   resolves against, so the whole pin would range against a box that never
   scrolls. Same declaration, second load-bearing reason.

   ── WHAT WAS NOT TESTED, PLAINLY ─────────────────────────────────────────
   1. A REAL RETRACTABLE URL BAR. No browser here has one, and resizing a
      headless viewport moves svh, lvh and dvh together, so it cannot tell
      them apart. The table above is a reconstruction with the layout frozen
      by hand; it is the right shape of test and it is not a handset.
   2. iOS SAFARI AND FIREFOX ANDROID. Chromium 143 only. Where they lack
      animation-timeline the @supports test hands them the static grid, and
      THAT path was verified by making the condition unsatisfiable — but
      whether they support it and then behave differently was not measured.
   3. WHETHER MEDIA-QUERY `height` RESOLVES AGAINST lvh ON SOME ENGINE. It is
      not observable here. It is harmless because the gate only decides
      WHETHER, and every length is svh: an engine that admitted a viewport
      whose svh is 520 while its media-query height reads 600 would compute a
      287.5px record 440px tall in a 520px pane, still 40px clear top and
      bottom.
   4. A REAL DEVICE OF ANY KIND. All of the above is emulation.
   5. THE URL-BAR AND TOUCH PASSES WERE NOT RE-RUN AT TABLET WIDTHS. The
      held-offset reconstruction and the CDP touch pass below are the phone's,
      taken at 390x659 and 390x844, and they are not repeated at 768 or 899.
      The mechanism they test is width-independent — every length in this
      block is still svh and the beat is still 75svh per person — but the
      NUMBERS in those two tables are the phone's and should not be read as
      the tablet's.
   6. A REAL TABLET'S BROWSER CHROME. The 768x954 row in the aspect table is
      an arithmetic model of chrome eating ~70px, not a measurement of any
      particular browser, and it is the row the choice of 1/1 over 3/4 leans
      on hardest.
   ══════════════════════════════════════════════════════════════════════════ */
@property --bench-in{syntax:'<length>';inherits:true;initial-value:0px}
@property --bench-go{syntax:'<number>';inherits:true;initial-value:0}

@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference) and (max-width:899px) and (min-height:600px) and (max-aspect-ratio:1/1){
    .bench{
      /* --bench-pad IS THE STAGE'S PADDING, not a copy of it. This used to be
         a literal 40px with a comment saying "the two must move together";
         the two are now the SAME DECLARATION — `.bench-stage` below reads this
         property, so the width budget cannot drift from the box it is a budget
         for. Both arms are the value the cascade already gives that band, so
         this is zero-pixel on both: 20px is the 599 block's own
         `.bench-stage{padding-inline:20px}`, var(--gutter) is the base
         `.bench-stage` padding, which is what 600-899 already resolves to.
         100vw counts a classic scrollbar the centred rail does not, which is
         the safe direction — a slightly wider start offset, never a shorter
         one. */
      --bench-pad:20px;
      --bench-air:40px;
      --bench-k:206.5px;
      /* 560 IS NOT A NEW NUMBER. It is this section's own tablet cap, from
         `@media(max-width:899px){.bench-member{max-width:560px}}` above, whose
         comment states the reason: it "keeps the square plate from becoming a
         706px face on a tablet". Reusing it means the record is the SAME SIZE
         in the deck and in the static grid at the same width, so the two
         treatments cannot disagree about how big a person is. It is inert
         below 600px wide by arithmetic, not by a gate: the width term there is
         at most 100vw - 40 = 559px at the widest phone this block admits, so
         the cap can never bind on a phone and every phone number in this
         header is unchanged. */
      --bench-cap:560px;
      --bench-card:min(calc(100vw - 2 * var(--bench-pad)),calc(100svh - 2 * var(--bench-air) - var(--bench-k)),var(--bench-cap));
      --bench-in:calc(100vw + var(--bench-card) / 2);
      --bench-dip:.12;
    }
    .bench-stage{padding-inline:var(--bench-pad)}
    /* THE SECOND OPT-IN THAT USED TO SIT HERE IS GONE. It read
       `.bench-head .next-jump[hidden]{display:flex}` and paired with the one in
       §THE LANDING, so .bench's reserved box was opted back in by each of the
       two blocks that make .bench animate. .bench no longer ships a control at
       all (src/build.mjs emits none), so both rules matched nothing; see the
       note at §THE RESERVED BOX. .bench still ANIMATES here — that is what
       this whole block is — it simply no longer offers to be skipped. */
    /* THE RECORD'S OWN TYPE IS DECLARED HERE, NOT INHERITED FROM THE 599
       BLOCK, and that is what keeps --bench-k a single flat number now that
       the gate reaches 899. Measured at the one-paragraph copy with the gate
       widened and these two lines absent, K = recordHeight - cardWidth over
       all three records:
           146.09 at every viewport from 600 to 899 wide
           152.50 at every viewport 599 and below
       — a 6.4px step in the middle of one gate, because `.bench-record
       {padding-top:20px}` (20 -> 18, the clamp) and `.bench-record h3
       {font-size:1.9rem}` (30.4 -> 25.6px) both stop applying at 600. With
       them declared here K was 152.50 at all 15 probe viewports, phone and
       tablet alike. The two paragraphs moved the copy block from 54px to
       108px, so the same measurement now reads 206.50 and that is what
       --bench-k above declares; what these two lines still buy is that it is
       ONE number and not a 6.4px step in the middle of the gate. Both values
       are byte-identical to what the 599 block already gives below 600px, so
       no phone pixel moves; above it they replace an inherited step nobody
       chose. */
    .bench-record{padding-top:20px}
    .bench-record h3{font-size:1.9rem}
    @media (min-width:600px){
      .bench{--bench-pad:var(--gutter)}
    }
    .bench-rail{display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:1fr;gap:0;height:calc(var(--bench-n,3) * 75svh + 100svh);view-timeline-name:--bench;view-timeline-axis:block}
    .bench-member{grid-area:1/1;align-self:start;position:sticky;top:0;height:100svh;max-width:none;display:grid;grid-template-columns:min(100%,var(--bench-card));grid-template-rows:calc(var(--bench-card) + var(--bench-k));place-content:center;z-index:calc(var(--bench-n,3) - var(--bench-i));--bench-go:min(1,max(0,var(--bench-n,3) - 1 - var(--bench-i)))}
    .bench-slot,.bench-record{grid-area:1/1}
    .bench-slot{display:grid;place-items:center;border-block:1px solid var(--line)}
    .bench-slot span{font-size:calc(var(--bench-card) * .62);line-height:.72;font-weight:500;letter-spacing:-.06em;color:var(--line);color:color-mix(in srgb,var(--accent-deep) 34%,var(--field))}
    /* Opaque, and in the band's OWN stock — the record has to cover its slot
       and the initial inside it exactly. Both bands are declared for the same
       reason the desktop block declares both: the tone is positional, so an
       editor reordering sections must not leave a field-coloured record on a
       shelf ground. */
    main>[data-band="light"] .bench-record{background:var(--field)}
    main>[data-band="shelf"] .bench-record{background:var(--shelf)}

    /* One window per person, [i/n, (i+1.5)/n] of the pin, shared by the frame
       and the record so the two can never drift apart. --bench-i is a
       registered <number> and --bench-n substitutes a literal, so the whole
       expression resolves at computed-value time — the shape §THE REGISTERED
       LENGTHS above requires. Verified live rather than assumed: the three
       members' ranges resolve to 0/50, 33.33/83.33 and 66.67/116.67 percent
       of `contain` at 390x844. */
    .bench-member>*,.bench-slot span{
      animation-duration:auto;animation-timing-function:linear;animation-fill-mode:both;
      animation-timeline:--bench;
      animation-range:contain calc(var(--bench-i) * 100% / var(--bench-n,3))
                      contain calc((var(--bench-i) + 1.5) * 100% / var(--bench-n,3));
    }
    .bench-slot{animation-name:bench-frame}
    .bench-record{animation-name:bench-hand}
    .bench-slot span{animation-name:bench-cue}

    @keyframes bench-hand{
      from{translate:var(--bench-in) 0;rotate:20deg;scale:calc(1 - var(--bench-dip))}
      33.3333%{translate:0 0;rotate:0deg;scale:1}
      66.6667%{translate:0 0;rotate:0deg;scale:1}
      to{translate:calc(-1 * var(--bench-in) * var(--bench-go)) 0;rotate:calc(20deg * var(--bench-go));scale:calc(1 - var(--bench-dip) * var(--bench-go))}
    }
    @keyframes bench-frame{from{opacity:1}40%{opacity:1}60%{opacity:0}to{opacity:0}}
    @keyframes bench-cue{from{scale:0}25%{scale:1}to{scale:1}}
  }
}
