/* ═══════════════════════════════════════════════════════════════════════════
   OCTOBER26 — THE CONTENT LAYER.

   css/main.css is lawyer2's stylesheet, carried VERBATIM so it can be re-synced
   from the reference without a merge. This file is the second half: it dresses
   codex's content classes (.band, .pull, .stat, .steps, .sources, .hub-list,
   .qa, .prose, .job-terms) and this site's own home sections (.family) in the Matterline Folio system defined there.

   It introduces NO new colour, NO new type size and NO new breakpoint. Every
   value below is a token from main.css:2 or a clamp lifted from design.md's
   type scale. The section frame and the folio corner are the same three rules
   main.css applies to .page-head/.index-list/.page-body — repeated here for the
   new class names rather than re-declared with different numbers, because two
   near-identical paddings is exactly how a system drifts.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── THE SECTION FRAME ──────────────────────────────────────────────────── */
.manifesto,.family,.reviews,.process,.contact,
.prose,.band,.pull,.stat,.steps,.sources,.hub-list,.qa,.cta,.job-terms{
  position:relative;--folio-radius:clamp(28px,3.35vw,48px);overflow:clip;
  padding:clamp(70px,9vw,130px) var(--gutter)}

.manifesto:after,.family:after,.reviews:after,
.process:after,.contact:after,.prose:after,.band:after,.pull:after,
.stat:after,.steps:after,.sources:after,.hub-list:after,.qa:after,.cta:after,
.job-terms: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))}

/* Consecutive prose-ish bands are one reading column, not a stack of rooms:
   halve the seam so a four-band article does not read as four sections. */
.band+.band,.band+.pull,.pull+.band,.band+.sources,.steps+.sources{
  padding-top:clamp(18px,2.4vw,34px)}
.band+.band{padding-top:clamp(10px,1.6vw,22px)}

@media(max-width:599px){
  .manifesto,.family,.reviews,.process,.contact,
  .prose,.band,.pull,.stat,.steps,.sources,.hub-list,.qa,.cta,.job-terms{
    --folio-radius:28px}
}

/* ── TYPE, from design.md's scale ───────────────────────────────────────── */
.manifesto h2,.family h2,.reviews h2,
.process h2,.contact h2,.cta h2{
  margin:0;font-size:clamp(2.8rem,5.4vw,5.8rem);font-weight:500;
  line-height:.92;letter-spacing:-.055em;text-wrap:balance}

.band h2,.pull h2,.steps h2,.sources h2,.qa h2,.hub-list h2,.prose h2,.job-terms h2{
  margin:0 0 .5em;font-size:clamp(1.7rem,3vw,3.25rem);font-weight:500;
  line-height:1;letter-spacing:-.035em;text-wrap:balance}

.band h3,.steps h3,.qa h3,.job-terms h3{
  margin:0 0 .4em;font-size:clamp(1.12rem,1.5vw,1.4rem);font-weight:500;
  line-height:1.22;letter-spacing:-.02em}

.section-label{
  margin:0 0 1.4em;font-size:11px;line-height:1.2;letter-spacing:.08em;
  text-transform:uppercase;color:var(--steel)}

.lede{
  margin:1.1em 0 0;max-width:52ch;
  font-size:clamp(1.08rem,1.5vw,1.35rem);line-height:1.42;color:var(--steel)}

.band p,.pull p,.steps p,.sources p,.qa p,.prose p,.col-copy p,.job-terms p{
  margin:0 0 1.05em;max-width:65ch;font-size:1rem;line-height:1.55}
.band p:last-child,.col-copy p:last-child,.prose p:last-child{margin-bottom:0}

.prose ul,.band ul,.steps ol,.job-terms ul{margin:0 0 1.05em;padding-left:1.15em;max-width:65ch}
.prose li,.band li,.steps li,.job-terms li{margin:0 0 .45em;line-height:1.55}

/* ── LINKS ──────────────────────────────────────────────────────────────── */
.text-link,.solid-link{
  display:inline-flex;align-items:center;gap:.5em;margin-top:1.4em;
  font-size:11px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--graphite);text-decoration:none;
  border-bottom:1px solid var(--accent-deep);padding-bottom:.42em;
  transition:gap .3s var(--ease),border-color .3s var(--ease)}
.text-link:hover,.text-link:focus-visible,
.solid-link:hover,.solid-link:focus-visible{gap:.95em;border-color:var(--graphite)}
[data-band="plate"] .text-link,[data-band="plate"] .solid-link{color:#fff;border-color:rgba(255,255,255,.55)}

/* ── NO TWO ADJACENT SECTIONS SHARE A LEFT RAIL (design.md) ─────────────── */
.manifesto>div{margin-left:7%;max-width:46ch}
.family .col-copy{margin-left:18%;max-width:52ch}
@media(max-width:899px){
  .manifesto>div,.family .col-copy{margin-left:0;max-width:none}
}

.quote-by{margin:.9em 0 1.4em;font-size:11px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--steel)}


/* ── REVIEWS — codex's data-review-story, ported (2026-11-03) ──────────────
   THE LAYOUT IS UNCONDITIONAL: two columns from 900px, a stack below, static
   hairline records as the base. Without JS or with reduced motion that is what
   the reader gets, all three reviews at full opacity.
   WITH MOTION: the photo sits in a sticky 100svh box and each card takes most of
   a screen and scrolls natively; there is no scripted pin. Each card fades and
   rises through ONE pass of the viewport, from its top reaching the viewport
   bottom to its bottom leaving the viewport top (codex's ViewTimeline entry 0% to
   exit 100%, same keyframes). That range is in PIXELS (--ra/--rb, js/main.js
   REVIEW SCRUB), not a view-timeline, so a phone's URL bar cannot move it. The
   animation itself sits OUTSIDE @supports: Firefox skips those blocks, and its
   JS driver needs a paused animation to find. Only transform and opacity move. */
.reviews-head{max-width:60ch}
.reviews-story{display:grid;grid-template-columns:minmax(0,43fr) minmax(0,57fr);gap:clamp(32px,5vw,88px);align-items:start;margin-top:clamp(30px,4vw,56px)}
.reviews-visual{margin:0;position:relative}
.reviews-visual img{display:block;width:100%;height:auto;
  border-bottom-right-radius:clamp(24px,2.6vw,44px)}
.reviews-visual picture{display:block;width:100%}
.reviews-stream{border-top:1px solid var(--line)}
.review-card{padding:clamp(20px,2.4vw,32px) 0;border-bottom:1px solid var(--line)}
.review-card blockquote{margin:0 0 1em;font-size:clamp(1.5rem,2.5vw,2.75rem);font-weight:500;
  line-height:1.14;letter-spacing:-.03em}
.review-card p{display:flex;flex-direction:column;gap:.22em;margin:0;
  font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--steel)}
.review-card p b{font-weight:500;color:var(--graphite)}
@media(max-width:899px){.reviews-story{grid-template-columns:minmax(0,1fr)}
  .review-card blockquote{font-size:clamp(1.45rem,7vw,2.1rem)}}
@keyframes review-pass{
  0%{opacity:0;transform:translate3d(0,52px,0)}
  28%,72%{opacity:1;transform:none}
  100%{opacity:0;transform:translate3d(0,-42px,0)}}
@media(prefers-reduced-motion:no-preference){
  .js .reviews-stream{border-top:0}
  .js .review-card{min-height:82svh;display:flex;flex-direction:column;justify-content:center;border-bottom:0;
    will-change:transform,opacity;animation:review-pass 1000ms linear both paused}
  @media(min-width:900px){
    .js .reviews-visual{position:sticky;top:0;height:100svh;display:flex;align-items:center}
    .js .reviews-story{margin-top:-8svh}
  }
  @media(max-width:899px){.js .review-card{min-height:72svh}}
  @supports (animation-timeline:scroll()){
    .js .review-card{animation-play-state:running;animation-timeline:scroll(root block);animation-range:var(--ra,0px) var(--rb,1px)}
  }
}

/* ── IMAGE STATEMENT — a plate: its surface is its own media ────────────── */
.image-statement{position:relative;overflow:clip;min-height:clamp(420px,62vh,760px);
  display:grid;align-content:end;gap:.4em;padding:clamp(40px,6vw,90px) var(--gutter)}
.image-statement picture,.image-statement img{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover}
.image-statement:before{content:"";position:absolute;inset:0;
  background:linear-gradient(180deg,rgba(35,35,35,.18) 0%,rgba(35,35,35,.72) 100%)}
.image-statement p,.image-statement span{position:relative;color:#fff;margin:0}
.image-statement p{font-size:clamp(2.4rem,5.4vw,5.4rem);font-weight:500;
  line-height:.92;letter-spacing:-.055em}
.image-statement span{font-size:clamp(1.08rem,1.5vw,1.35rem);line-height:1.42;
  color:rgba(255,255,255,.82)}

/* ── PROCESS / GUIDE DIRECTORY — numbered records, never cards ──────────── */
.process header{max-width:56ch;margin-bottom:clamp(30px,4vw,56px)}
.process header p{margin:0 0 .7em;font-size:clamp(1.08rem,1.5vw,1.35rem);
  line-height:1.42;color:var(--steel);max-width:52ch}
.process-list{display:grid;border-top:1px solid var(--line)}
.process-card{display:grid;grid-template-columns:auto minmax(0,16ch) minmax(0,1fr) auto;
  gap:clamp(14px,2.6vw,44px);align-items:baseline;
  padding:clamp(20px,2.6vw,34px) 0;border-bottom:1px solid var(--line);
  color:inherit;text-decoration:none;transition:transform .35s var(--ease)}
.process-card:hover,.process-card:focus-visible{transform:translateX(6px)}
.process-card span{font-size:11px;letter-spacing:.08em;color:var(--accent-deep)}
.process-card h3{margin:0;font-size:clamp(1.3rem,2.1vw,2rem);font-weight:500;
  line-height:1.05;letter-spacing:-.03em}
.process-card p{margin:0;color:var(--steel);max-width:62ch}
.process-card p b{font-weight:500;color:var(--graphite);white-space:nowrap}
.process-card i{font-style:normal;color:var(--accent-deep)}
@media(max-width:899px){
  .process-card{grid-template-columns:auto minmax(0,1fr);row-gap:.45em}
  .process-card p{grid-column:1/-1}
  .process-card i{display:none}
}

/* ── CONTACT ────────────────────────────────────────────────────────────── */
.contact{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);
  gap:clamp(26px,5vw,80px);align-items:center}
.contact .section-label{max-width:44ch}
.contact-media img{display:block;width:100%;height:auto;
  border-bottom-right-radius:clamp(24px,2.6vw,44px)}
@media(max-width:899px){.contact{grid-template-columns:1fr}
  .contact-media{order:2}}

/* ── INTERIOR CONTENT: hub lists, Q&A, stats, steps, sources, pull ──────── */
.hub-grid{display:grid;border-top:1px solid var(--line);margin-top:clamp(22px,3vw,40px)}
.hub-grid a,.hub-grid article>a{display:grid;grid-template-columns:minmax(0,1fr) auto;
  gap:1.2em;align-items:baseline;padding:clamp(16px,2.1vw,26px) 0;
  border-bottom:1px solid var(--line);color:inherit;text-decoration:none;
  transition:transform .35s var(--ease)}
.hub-grid a:hover,.hub-grid a:focus-visible{transform:translateX(6px)}
.hub-grid h3{margin:0 0 .25em;font-size:clamp(1.18rem,1.8vw,1.6rem);
  font-weight:500;line-height:1.1;letter-spacing:-.028em}
.hub-grid p{margin:0;color:var(--steel);max-width:62ch;font-size:1rem;line-height:1.55}

.qa>div,.qa article{padding:clamp(16px,2.1vw,26px) 0;border-bottom:1px solid var(--line)}
.qa>div:first-of-type,.qa article:first-of-type{border-top:1px solid var(--line)}

.pull{font-size:clamp(1.45rem,2.6vw,2.4rem);line-height:1.2;letter-spacing:-.035em;
  font-weight:500}
/* The 30ch measure belongs to the quote, not the section: on the section it made
   the band half the page wide, and its corner showed mid-page whenever the next
   band was a different colour (does-your-domain-matter, 2026-11-13). */
.pull blockquote,.pull p{max-width:30ch}
.pull blockquote{margin:0}
.pull p{font-size:inherit;line-height:inherit;letter-spacing:inherit}
.pull cite{display:block;margin-top:1em;font-size:clamp(.95rem,1.1vw,1.05rem);font-style:normal;
  font-weight:400;letter-spacing:0;line-height:1.4;color:var(--steel);max-width:60ch}

.stat{display:grid;gap:.3em}
.stat-n{font-size:clamp(3.4rem,8vw,8rem);font-weight:500;line-height:.86;
  letter-spacing:-.06em;color:var(--accent-deep)}
.stat-cap{max-width:42ch;color:var(--steel);font-size:clamp(1.08rem,1.5vw,1.35rem);
  line-height:1.42}

.steps ol{counter-reset:s;list-style:none;padding:0;margin:clamp(18px,2.4vw,32px) 0 0;
  border-top:1px solid var(--line)}
.steps ol>li{counter-increment:s;padding:clamp(16px,2.1vw,26px) 0 clamp(16px,2.1vw,26px) 3.4em;
  border-bottom:1px solid var(--line);position:relative;margin:0}
.steps ol>li:before{content:counter(s,decimal-leading-zero);position:absolute;left:0;top:clamp(16px,2.1vw,26px);
  font-size:11px;letter-spacing:.08em;color:var(--accent-deep)}

.sources{font-size:.92rem}
.sources ol,.sources ul{padding-left:1.2em;max-width:72ch}
.sources li{margin:0 0 .5em;line-height:1.5;color:var(--steel)}
.sources a{color:var(--graphite);text-underline-offset:.22em}
.sources-note{color:var(--steel);max-width:65ch}

.job-terms dl,.job-terms .lvl{display:grid;grid-template-columns:minmax(0,14ch) minmax(0,1fr);
  gap:.6em clamp(14px,2.4vw,40px);margin:0;border-top:1px solid var(--line);padding-top:1.1em}
.job-terms dt{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--steel)}
.job-terms dd{margin:0}
@media(max-width:599px){.job-terms dl,.job-terms .lvl{grid-template-columns:1fr;gap:.2em 0}}

/* ── PAGE PLATE (interior hero image) ───────────────────────────────────── */
.page-plate{margin:clamp(26px,3.4vw,52px) 0 0}
.page-plate img{display:block;width:100%;height:auto;
  border-bottom-right-radius:clamp(24px,2.6vw,44px)}
.page-plate--tall img{max-height:72vh;object-fit:cover}

/* ── ACCESSIBILITY TABLE (the WCAG grid on /accessibility/) ─────────────── */
.table-wrap{overflow-x:auto;margin-top:clamp(20px,2.6vw,36px)}
.table-wrap table{border-collapse:collapse;width:100%;min-width:34rem;font-size:.94rem}
.table-wrap th,.table-wrap td{text-align:left;padding:.66em .9em;
  border-bottom:1px solid var(--line);vertical-align:top}
.table-wrap th{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--steel);font-weight:500}
.status{font-size:11px;letter-spacing:.06em;text-transform:uppercase}
.status--ok{color:var(--accent-deep)}
.status--na{color:var(--steel)}
.sc{font-variant-numeric:tabular-nums;white-space:nowrap}

/* ── MOBILE MENU GROUP HEADS ────────────────────────────────────────────── */
.mobile-group{margin:1.5em 0 .3em;font-size:11px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--steel)}
.mobile-group:first-child{margin-top:0}

/* ── THE PROSE WRAPPER ──────────────────────────────────────────────────── */
.prose--wide p,.prose--wide li{max-width:78ch}
.band-copy{max-width:65ch}
.band-n{font-size:11px;letter-spacing:.08em;color:var(--accent-deep);
  display:block;margin-bottom:.7em}

/* ── THE BENCH PORTRAIT IS A BLANK, ON PURPOSE ──────────────────────────────
   No photograph of Mike Choe was supplied. .bench-plate already renders a
   mist square at aspect-ratio:1, which is the ruled empty frame §THE DECK uses
   between records; this centres an initial in it so the frame reads as
   deliberate rather than as a failed <img>. Replace with a real portrait by
   putting an <img> back inside .bench-plate and dropping the --blank class. */
.bench-plate--blank{display:grid;place-items:center}
.bench-plate--blank span{
  font-size:clamp(2.4rem,5vw,4.2rem);font-weight:500;line-height:1;
  letter-spacing:-.05em;
  color:color-mix(in srgb,var(--accent-deep) 34%,var(--field))}

/* The bench head's note is secondary copy, not a lede. */
.bench-note{max-width:60ch;color:var(--steel);
  font-size:clamp(1.02rem,1.3vw,1.18rem);line-height:1.45;margin:1.1em 0 0}

/* ── PAGE HEAD: codex's class names, lawyer2's values ───────────────────────
   codex calls these .section-label and .lede; lawyer2 calls the same two roles
   .page-eyebrow and .page-lede. The names could not be swapped wholesale
   because .section-label also appears INSIDE the extracted codex content, so
   instead the values are matched to lawyer2's exactly. */
.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-head .section-label{margin:0 0 24px}
.page-head .lede{max-width:52ch;margin:44px 0 0 auto;
  font-size:clamp(1.05rem,1.4vw,1.25rem);color:var(--steel)}
@media(max-width:599px){.page-head h1{font-size:clamp(2.9rem,13vw,4.1rem)}}

/* The WCAG grid's last column is prose and must wrap, or the table measures
   ~4400px and the reader scrolls a long way for every row. It still scrolls
   (.table-wrap is overflow-x:auto and the section's overflow:clip does not
   trap it — verified scrollLeft moves), this just stops it needing to. */
.table-wrap table{table-layout:fixed}
.table-wrap th:nth-child(1),.table-wrap td:nth-child(1){width:4.5rem}
.table-wrap th:nth-child(3),.table-wrap td:nth-child(3){width:4rem}
.table-wrap th:nth-child(4),.table-wrap td:nth-child(4){width:9rem}
.table-wrap td{overflow-wrap:anywhere}

/* main.css sets white-space:nowrap on table cells for lawyer2's own compact
   numeric tables. This grid's last column is a sentence, so nowrap made every
   cell overflow its box and pushed the scroll width to 5524px while the TABLE
   itself measured a correct 1310px — the row looked fine and the text ran off
   the side. Re-enable wrapping; .sc keeps nowrap because an SC number
   ("1.4.11") must not break. */
.table-wrap th,.table-wrap td{white-space:normal}
.table-wrap .sc,.table-wrap td:first-child{white-space:nowrap}

/* The statement h2 is set at clamp(2.8rem,5.4vw,5.8rem) with margin:0, so any
   copy that follows it needs its own leading or the two collide optically at
   desktop sizes. .process-list already carries a
   margin-top; .col-copy and the reviews head did not. */
.col-copy{margin-top:clamp(26px,3.2vw,48px)}
.manifesto>div>h2+.quote-by{margin-top:1.1em}


/* ── THE PULLED SECTION'S CORNER STAYS ON THE SEAM ─────────────────────────
   js/main.js publishes --stack-pull (px) on a stack participant ONLY while it
   is being pulled up under the lid, and only when it is not lifting itself;
   see "THE PULLED SECTION'S FOLIO CORNER STAYS ON THE SEAM IT DESCRIBES"
   there. The section moves up by that amount and the section below it does
   not, so the folio corner is counter-translated by the same amount to stay
   on the real boundary instead of floating above it.
   `translate`, not `bottom`: bottom is layout and would re-raster the whole
   section every frame of the window; translate on its own promoted layer is
   compositor-only. The selector matches only while the property is present,
   so at rest — and in every section that is never pulled — nothing here
   applies and the page is unchanged.
   overflow-clip-margin lets the corner paint below the section's clipped box
   by exactly the pull and no more, so nothing else inside the section can
   spill. Where it is unsupported the corner is clipped during the pull
   (absent for those frames) rather than floating, which is the lesser
   failure. Lengths are the pull itself, so the widening is zero at rest. */
[data-wb-motion][style*="--stack-pull"]{overflow-clip-margin:var(--stack-pull)}
[data-wb-motion][style*="--stack-pull"]:after{translate:0 var(--stack-pull);will-change:translate}

/* ── THE LINK MARK IS THE FOLIO CORNER, NOT AN ARROW ───────────────────────
   Every "↗" glyph on the site — the hero button, the Contact button in the
   mobile menu, the "All of …" link in each nav dropdown, the four practice
   rows and the email link — was replaced on 2026-11-02 with a drawn hook: one
   stroke bent round the folio corner's radius, ending in a small arrowhead
   (FOLIO_MARK / FOLIO_MARK_DOWN in _build/site.mjs, which says which link gets
   which). The operator asked for it because the stock arrow read as a generic
   site, then asked for the heads because the bare corner read as a line
   curving up. It is currentColor, so it
   inherits whatever colour the glyph had in each context (the accent inside
   .button and .practice-row), and 1.15em, so it scales with the text it
   follows rather than being one fixed size in a 10px menu and a 13px button.
   Hover or keyboard focus nudges it 2px the way its head points — right for
   the forward mark, down for .folio-mark--down; reduced motion keeps it
   still. */
.folio-mark{display:inline-grid;place-items:center;line-height:0;vertical-align:-.1em}
.folio-mark svg{display:block;width:1.15em;height:1.15em;transition:translate .25s var(--ease)}
a:hover .folio-mark svg,a:focus-visible .folio-mark svg{translate:2px 0}
a:hover .folio-mark--down svg,a:focus-visible .folio-mark--down svg{translate:0 2px}
@media(prefers-reduced-motion:reduce){.folio-mark svg{transition:none}a:hover .folio-mark svg,a:focus-visible .folio-mark svg,a:hover .folio-mark--down svg,a:focus-visible .folio-mark--down svg{translate:none}}

/* ── THE PHONE HEADLINE'S MEASURE FOLLOWS THE COPY ─────────────────────────
   main.css caps the hero h1 at 8ch below 600px, sized for the three-word
   "Look the part.". The headline became "Customers don't trust old websites."
   on 2026-11-02, and at 8ch "don't" stood alone on its own line at every phone
   width (Customers / don't / trust old / websites.). 11ch lets text-wrap:
   balance set it as three even lines. Same specificity as main.css's rule;
   this file loads after it, so source order decides. */
@media(max-width:599px){.hero h1{max-width:11ch}}

/* ══ §THE STORY ═════════════════════════════════════════════════════════════
   A customer on a review app, told on a phone, inside .manifesto (2026-11-02).
   INSIDE, not a new section: data-band parity is stamped by position
   (stampTones in js/main.js), so a new section would flip the ground of every
   section after it.

   THE BASE IS THE STATIC SEQUENCE: four captioned phone frames in a row and the
   end line as plain text. That is what reduced-motion visitors, short
   viewports and no-script visitors get, and it is complete on its own.

   THE PINNED, SCRUBBED VERSION IS GATED ON `.js` + MEDIA, NEVER ON @supports.
   That is the lesson of the bench (js/main.js, BENCH FALLBACK): its layout sat
   inside @supports (animation-timeline:view()), so Firefox got neither pin nor
   animations and the fallback had nothing to drive. Here the layout and the
   keyframes apply in every engine; only animation-timeline/animation-range are
   Chrome-only, and an engine that drops them gets 1ms animations that
   js/main.js (STORY FALLBACK) pauses and scrubs from the same progress the
   timeline would give. Without script there is no `.js` class, so no engine
   ever shows a frozen pin.

   ONE TIMELINE, ONE RANGE — IN PIXELS. Every animation below runs on the
   ROOT SCROLL timeline over [--story-a, --story-b] (the stage's whole pinned
   travel), both set in px by js/main.js (STORY RANGE). It used to be a
   view-timeline with `contain 0% → contain 100%`, which is measured against the
   SCROLLPORT: on a phone the URL bar slides in and out while you scroll, the
   scrollport height changes on those frames, the mapping from scroll to story
   progress moved under the finger, and the beats micro-jittered (operator, real
   phone, 2026-11-02; desktop has no dynamic toolbar, so it never showed there).
   The px range is computed from the svh geometry (story height, the 100svh
   stage) and recomputed only when that geometry really changes and encodes its
   beat in its keyframe percentages, so the fallback needs one progress number,
   not a per-animation range table, and cannot drift from this file.
   Beats (v2, 2026-11-02, operator's sequence): 0–15 the listing, "No website"
   emphasised, thumb hovering; 15–21 back; 21–31 results, tap Summit; 31–45 its
   listing, tap Website; 45–69 the browser slides up over the app and its site's
   hero settles in (a static capture, no scrolling: see THE SITE below); 69–80
   back to the listing, tap Call; 80–91
   the call screen; 91–100 the end card. No beat gets less than ~5% of the
   scrub (~120px at 390x844, ~140px at 1280x800).
   THE "NO WEBSITE" PULSE IS TIME-BASED ON PURPOSE. Its visibility follows the
   scrub, but the breathe itself (nosite-breathe, 1.6s) runs on the document
   clock, so a fast scroll cannot turn it into rapid flashing — it stays well
   under 3 flashes a second whatever the reader does. It is deliberately NOT
   named story-*, so the Firefox fallback leaves it running on its own clock.
   Reduced motion and no-script get the ring and the bold label, static.

   MOTION IS transform/opacity ONLY (translate, scale, opacity), on boxes no
   bigger than the phone screen, so the compositor never has a large layer to
   re-raster mid-scrub (memory: scroll jitter = compositor stale animation).
   Each screen is a FIXED, CLIPPED window (.story-screen) and only its content
   (.scr) moves, so a sliding screen never shows outside the bezel and the
   captions, which live outside the screen, are never clipped with it.

   BRAND: the review app is generic — amber stars, neutral chips, this site's
   ink — with no third-party logo, wordmark or trade dress, and the businesses
   are fictional with no domain shown. */
.story{margin:clamp(40px,6vw,72px) 0 0}
.story-stage{display:grid;gap:18px}
.story-frames{list-style:none;margin:0;padding:0 0 10px;display:grid;grid-auto-flow:column;
  grid-auto-columns:clamp(150px,40vw,190px);gap:16px;overflow-x:auto;scroll-snap-type:x mandatory}
.story-frame{scroll-snap-align:start;container-type:inline-size}
.story-screen{position:relative;aspect-ratio:9/19;border:5px solid #0b0b0c;border-radius:22px;overflow:clip;background:#fff;
  margin:6px;box-shadow:0 0 0 1px #8c8f94,0 0 0 4px #c9cbcf,0 0 0 5px #8f9297,0 8px 18px -6px rgba(20,20,22,.28)}
/* the island, on each static frame (the animated phone draws its own) */
.story-screen::after{content:"";position:absolute;z-index:3;top:2.4%;left:50%;width:30%;height:3.3%;transform:translateX(-50%);border-radius:1em;background:#050506}
.scr{position:absolute;inset:0;background:#fff;color:var(--graphite);font-size:4.4cqw;line-height:1.3;overflow:clip}
.story-cap{margin:.7em 0 0;font-size:12px;line-height:1.4;color:var(--steel)}
.story-thumb{display:none}
.story-end{margin:0;max-width:34ch;text-wrap:balance;font-size:clamp(1.1rem,1.6vw,1.35rem);line-height:1.35}

/* the screens' own UI, all in em of the 4.4cqw base so it scales with the phone.
   THE APP (yp-*): a review app laid out the way people know review apps — photo
   header, square rating tiles, Claimed/price/category, hours, a round action row,
   numbered results — in this site's own warm terracotta (--app), never a third
   party's name, logo, burst or exact red. THE SITE (.br-shot, inside br-*
   browser chrome): a real screenshot of one of OUR designs (plumbing2's mobile
   hero), because the operator found the earlier hand-built mock read as a generic
   site. Its fictional brand was swapped for "Summit" at capture time only (the
   design's own name, "Alder Plumbing", is also a real business). No reviews on
   it: the sites we build do not have them yet. */
.story{--app:#c4583c;--app-soft:#f6e6e0;--sp-copper:#d9793a}
.scr svg{width:1.15em;height:1.15em;flex:none}
.yp-photo{position:relative;height:12.5em;background:linear-gradient(160deg,#9aa7a1,#5c6d68 55%,#3c4744)}
.yp-photo::before{content:"";position:absolute;inset:auto 8% 18% 12%;height:38%;border-radius:.4em;background:linear-gradient(180deg,rgba(255,255,255,.18),rgba(255,255,255,.05))}
.yp-photo-bar{position:absolute;top:2.3em;left:.8em;right:.8em;display:flex;justify-content:space-between}
.yp-photo-bar i,.yp-photo-bar span i{display:grid;place-items:center;width:2em;height:2em;border-radius:50%;background:rgba(255,255,255,.92);color:var(--graphite)}
.yp-photo-bar span{display:flex;gap:.4em}
.yp-count{position:absolute;right:.8em;bottom:.7em;display:flex;align-items:center;gap:.3em;padding:.2em .55em;border-radius:1em;background:rgba(0,0,0,.55);color:#fff;font-size:.8em}
.yp-body{display:grid;gap:.5em;padding:.9em .9em 0}
.yp-name{font-size:1.65em;font-weight:600;letter-spacing:-.02em;line-height:1.05}
.yp-rate{display:flex;align-items:center;gap:.45em;color:var(--steel);font-size:.85em}
.yp-tiles{display:flex;gap:.12em}
.yp-tiles i{width:1.15em;height:1.15em;border-radius:.18em;background:#e4ddd8}
.yp-tiles i.on{background:var(--app)}
.yp-tiles i.half{background:linear-gradient(90deg,var(--app) 50%,#e4ddd8 50%)}
.yp-meta{display:flex;flex-wrap:wrap;align-items:center;gap:.35em;font-size:.85em;color:var(--steel)}
.yp-claimed{display:inline-flex;align-items:center;gap:.25em;color:#2f6b52;font-weight:500}
.yp-hours{font-size:.85em;color:var(--steel)}.yp-hours b{color:#2f6b52;font-weight:600}
.yp-actions{display:grid;grid-template-columns:repeat(5,1fr);gap:.2em;margin:.3em 0 .1em;padding:.7em 0;border-block:1px solid #efe8e3}
.yp-act{position:relative;display:grid;justify-items:center;gap:.3em;font-size:.72em;text-align:center;line-height:1.15}
.yp-act i{display:grid;place-items:center;width:2.9em;height:2.9em;border-radius:50%;border:1px solid #e3dad3;color:var(--graphite)}
.yp-act i svg{width:1.4em;height:1.4em}
.yp-call i,.yp-site i{background:var(--app);border-color:var(--app);color:#fff}
.yp-nosite{color:var(--app);font-weight:600}
.yp-nosite i{background:#f1ece8;border-style:dashed;border-color:#c9bdb4;color:#b3a69c}
/* "No website": always emphasised (bold, the app colour, a ring) */
.yp-nosite::before{content:"";position:absolute;inset:-.45em -.1em;border:1.5px solid var(--app);border-radius:1em}
.yp-site::after,.yp-call::after{content:"";position:absolute;inset:-.4em 0;border-radius:1em;background:rgba(196,88,60,.16);opacity:0}
.yp-review{display:flex;align-items:center;gap:.5em;padding:.55em .7em;border-radius:.6em;background:#f6f2ef;font-size:.82em;color:var(--steel)}
.yp-review>i{width:1.8em;height:1.8em;border-radius:50%;background:#e4ddd8}
.yp-sec{margin-top:.2em;font-size:1em;font-weight:600}
.yp-high{display:flex;flex-wrap:wrap;gap:.35em}
.yp-high span{display:inline-flex;align-items:center;gap:.3em;padding:.3em .6em;border-radius:1em;border:1px solid #e3dad3;font-size:.75em}
.yp-search{position:relative;z-index:2;display:flex;align-items:center;gap:.5em;padding:2.4em .9em .5em;background:#fff}
.yp-search span{flex:1;padding:.45em .8em;border-radius:2em;background:#f1ece8;color:var(--graphite);font-size:.9em}
.yp-filters{position:relative;z-index:2;display:flex;gap:.35em;padding:0 .9em .6em;white-space:nowrap;overflow:clip;background:#fff}
.yp-filters span{padding:.3em .65em;border-radius:2em;border:1px solid #e3dad3;font-size:.75em}
.yp-list{display:grid;border-top:1px solid #efe8e3}
.yp-card{position:relative;display:flex;gap:.7em;padding:.8em .9em;border-bottom:1px solid #efe8e3}
.yp-thumb{flex:none;width:4.4em;height:4.4em;border-radius:.5em;background:linear-gradient(140deg,#c9b6a6,#8a7464)}
.yp-pick .yp-thumb{background:linear-gradient(160deg,#a9b4ac,#566b66)}
.yp-cbody{display:grid;gap:.22em;font-size:.88em;align-content:start;min-width:0}
.yp-cbody b{font-size:1.05em}
.yp-cmeta{color:var(--steel);font-size:.9em}
.yp-cbody em{color:var(--steel);font-size:.88em;line-height:1.3}
.yp-tag{display:inline-flex;align-items:center;gap:.3em;color:#2f6b52;font-size:.82em}
.yp-web{display:inline-flex;align-items:center;gap:.3em;justify-self:start;margin-top:.15em;padding:.2em .6em;border-radius:2em;background:var(--app);color:#fff;font-size:.82em;font-weight:500}
.yp-pick::after{content:"";position:absolute;inset:.25em .4em;border:2px solid var(--app);border-radius:.7em;opacity:0}
.yp-blur{filter:blur(1.5px);opacity:.4}
/* the business's own site, in browser chrome */
.sc-browser .scr{background:#fff;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
.br-bar{position:relative;z-index:3;padding:2.3em .9em .45em;background:#f2f2f4}
.br-url{display:flex;align-items:center;justify-content:center;gap:.35em;padding:.4em;border-radius:.6em;background:#fff;color:#333;font-size:.85em}
.br-url svg{width:1em;height:1em;color:#3a7d57}
.br-load{position:relative;z-index:3;display:block;height:2px;background:var(--sp-copper);transform-origin:0 50%}
.br-view{position:absolute;top:5.1em;left:0;right:0;bottom:3.4em;overflow:clip;background:#fff}
.br-tools{position:absolute;left:0;right:0;bottom:0;z-index:3;display:flex;justify-content:space-around;align-items:center;height:3.4em;padding-bottom:.5em;background:#f2f2f4;color:#1a73e8}
.br-tools i{display:grid;place-items:center}.br-tools svg{width:1.3em;height:1.3em}
.br-shot img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:50% 0}
.sc-call .scr{background:linear-gradient(180deg,#1d2321,#2c3532);color:#fff;display:grid;justify-items:center;align-content:start;padding-top:24%}
.cl-avatar{display:grid;place-items:center;width:5.4em;height:5.4em;border-radius:50%;background:var(--accent-deep);font-size:1.1em;font-weight:500}
.cl-rings{position:absolute;top:24%;left:50%;width:5.94em;height:5.94em;margin-left:-2.97em}
.cl-rings i{position:absolute;inset:0;border-radius:50%;border:1px solid rgba(255,255,255,.55);opacity:0}
.cl-name{margin-top:1em;font-size:1.3em;font-weight:500}
.cl-state{margin-top:.3em;font-size:.9em;opacity:.7}
.cl-keys{position:absolute;bottom:24%;left:0;right:0;display:flex;justify-content:center;gap:1.4em}
.cl-keys i{width:3em;height:3em;border-radius:50%;background:rgba(255,255,255,.14)}
.cl-end{position:absolute;bottom:8%;left:50%;width:3.6em;height:3.6em;margin-left:-1.8em;border-radius:50%;background:#c4483e}

@media (prefers-reduced-motion:no-preference) and (min-height:560px){
  .js .story{position:relative;height:513svh}
  .js .story-stage{position:sticky;top:0;height:100svh;display:grid;place-items:center;gap:0}
  .js .story-phone{position:relative;height:min(64svh,560px);aspect-ratio:9/19;container-type:size}
  .js .story-frames{position:absolute;inset:0;display:block;padding:0;overflow:visible;
    background:#0b0b0c;border-radius:13cqw}
  .js .story-frames::before{content:"";position:absolute;z-index:5;top:4.4cqw;left:50%;width:22cqw;height:4.6cqw;margin-left:-11cqw;border-radius:3cqw;background:#050506;box-shadow:inset 0 0 0 1px rgba(255,255,255,.04)}
  /* ── A REAL PHONE, NOT AN OUTLINE (2026-11-02) ─────────────────────────────
     Everything below is drawn AROUND the existing phone box, never inside it:
     .story-phone, .story-frames and the screens keep their exact size and
     position, so every measured tap target (--t-*) is unchanged. The frame is a
     titanium ring painted behind the black bezel (::before, 1.7cqw outside the
     box, radius = bezel radius + 1.7), the side buttons and the contact shadow
     sit on ::after, and the glass sheen is a static ~3% band over the screens.
     All of it is static: no animated filter or shadow, so the scrub never
     re-rasters it (memory: scroll jitter = compositor stale animation).
     Generic device: no logo, no branding. */
  .js .story-phone::before{content:"";position:absolute;inset:-1.7cqw;border-radius:14.7cqw;pointer-events:none;
    background:linear-gradient(145deg,#e6e7ea 0%,#a7aaaf 22%,#d6d7da 45%,#8c8f94 70%,#c6c8cc 88%,#9fa2a7 100%);
    box-shadow:inset 0 0 0 1px rgba(255,255,255,.55),inset 0 0 0 2px rgba(0,0,0,.18),
      0 1.4cqw 2.6cqw -.6cqw rgba(20,20,22,.28),0 5cqw 9cqw -3cqw rgba(20,20,22,.30)}
  .js .story-phone::after{content:"";position:absolute;top:0;bottom:-3.6cqw;left:-2.25cqw;right:-2.25cqw;pointer-events:none;
    background:
      linear-gradient(90deg,#8c8f94,#dadbde 55%,#9a9da2) 0 15% / .6cqw 5% no-repeat,
      linear-gradient(90deg,#8c8f94,#dadbde 55%,#9a9da2) 0 23% / .6cqw 9% no-repeat,
      linear-gradient(90deg,#8c8f94,#dadbde 55%,#9a9da2) 0 34% / .6cqw 9% no-repeat,
      linear-gradient(90deg,#9a9da2,#dadbde 45%,#8c8f94) 100% 26% / .6cqw 13% no-repeat,
      radial-gradient(ellipse 40% 1.5cqw at 50% calc(100% - 1.6cqw),rgba(20,20,22,.2),transparent 72%)}
  .js .story-frames::after{content:"";position:absolute;z-index:4;inset:2.6cqw;inset:round(2.6cqw,1px);border-radius:10.5cqw;pointer-events:none;
    background:linear-gradient(118deg,rgba(255,255,255,.045) 0%,rgba(255,255,255,.03) 34%,rgba(255,255,255,0) 34.5%)}
  .js .story-frame{position:absolute;inset:2.6cqw;inset:round(2.6cqw,1px)}
  .js .story-screen{position:absolute;inset:0;margin:0;aspect-ratio:auto;border:0;border-radius:10.5cqw;background:transparent;box-shadow:none}
  .js .story-screen::after{content:none}
  .js .story-frame{overflow:clip;border-radius:10.5cqw}
  /* THE CAPTION IS THE MESSAGE (2026-11-13): no line under the phone; the same
     <p> becomes a card centred on the screen, shown in a pause after its slide
     (story-c1..c5). Its huge box-shadow is the scrim that dims the slide; the
     frame's clip keeps it on the screen. */
  .js .story-cap{position:absolute;z-index:6;left:9%;right:9%;top:50%;transform:translateY(-50%);margin:0;
    padding:5cqw 5.5cqw;border-radius:5cqw;background:var(--graphite);color:#fff;text-align:center;
    font-size:max(12px,5.4cqw);line-height:1.3;text-wrap:balance;box-shadow:inset 0 0 0 1px rgba(255,255,255,.14),0 0 0 100vmax rgba(16,17,15,.42)}
  .js .story-thumb{display:block;position:absolute;z-index:6;left:0;top:0;width:12cqw;height:12cqw;margin:-6cqw 0 0 -6cqw;
    border-radius:50%;background:rgba(16,17,15,.22);border:2px solid rgba(255,255,255,.95);box-shadow:0 2px 8px rgba(0,0,0,.18)}
  .js .story-end{position:absolute;z-index:7;left:7%;right:7%;top:34%;margin:0;max-width:none;padding:6cqw;border-radius:5cqw;
    background:var(--graphite);color:#fff;font-size:max(12.5px,6.2cqw);line-height:1.25;text-wrap:balance}

  /* desktop: copy pinned on the left, the phone stage beside it */
  @media (min-width:900px){
    .js .manifesto{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);column-gap:clamp(24px,4vw,72px);align-items:start}
    .js .manifesto>.section-label{grid-column:1;grid-row:1}
    .js .manifesto>div{grid-column:1;grid-row:2;position:sticky;top:clamp(80px,14vh,140px)}
    .js .manifesto>.story{grid-column:2;grid-row:2;margin-top:0;height:580vh}
    .js .story-phone{height:min(70vh,600px)}
  }

  .js .story :is(.story-frames,.scr,.br-shot,.yp-list,.br-load,.story-cap,.story-thumb,.story-end,.cl-rings i),
  .js .story :is(.yp-pick,.yp-site,.yp-call,.yp-photo-bar>i,.br-back)::after{
    animation-duration:1ms;animation-duration:auto;animation-timing-function:linear;animation-fill-mode:both;
    animation-timeline:scroll(root block);animation-range:var(--story-a,0px) var(--story-b,1px)}
  .js .story :is(.yp-photo-bar>i,.br-back){position:relative}
  .js .story :is(.yp-photo-bar>i,.br-back)::after{content:"";position:absolute;inset:-.3em;border-radius:50%;background:rgba(196,88,60,.28);opacity:0}
  .js .sf1 .scr{animation-name:story-s1}
  .js .sf2 .scr{animation-name:story-s2}
  .js .sf3 .scr{animation-name:story-s3}
  .js .sf4 .scr{animation-name:story-s4}
  .js .sf5 .scr{animation-name:story-s5}
  .js .sf2 .yp-list{animation-name:story-list}
  .js .sf4 .br-shot{animation-name:story-settle}
  .js .sf4 .br-load{animation-name:story-load}
  .js .sf1 .story-cap{animation-name:story-c1}
  .js .sf2 .story-cap{animation-name:story-c2}
  .js .sf3 .story-cap{animation-name:story-c3}
  .js .sf4 .story-cap{animation-name:story-c4}
  .js .sf5 .story-cap{animation-name:story-c5}
  .js .story-thumb{animation-name:story-thumb}
  .js .story-frames{animation-name:story-dim}
  .js .story-end{animation-name:story-end}
  .js .sf1 .yp-photo-bar>i::after{animation-name:story-press-back}
  .js .story .yp-pick::after{animation-name:story-press-pick}
  .js .story .yp-site::after{animation-name:story-press-site}
  .js .story .br-back::after{animation-name:story-press-brback}
  .js .story .sf3 .yp-call::after{animation-name:story-press-call}
  .js .cl-rings i:first-child{animation-name:story-ring1}
  .js .cl-rings i:last-child{animation-name:story-ring2}
  /* the "No website" ring: scrubbed in, then a slow time-based breathe */
  .js .story .yp-nosite::before{animation-name:story-nosite,nosite-breathe;
    animation-duration:1ms,1.6s;animation-duration:auto,1.6s;animation-timing-function:linear,ease-in-out;
    animation-iteration-count:1,infinite;animation-fill-mode:both,none;
    animation-timeline:scroll(root block),auto;animation-range:var(--story-a,0px) var(--story-b,1px),normal}
}
/* ── THE TAPS ARE HONEST ──────────────────────────────────────────────────
   Every target is MEASURED, not guessed: js/main.js (STORY TAP TARGETS)
   reads each target's centre from its layout box inside the phone and writes
   it to .story-phone as --t-* (px), re-measuring on resize and after fonts
   load, so the thumb lands on the element at every phone size and after any
   copy or layout change. The results list's scroll is a measured offset too
   (--list-shift), and the Summit tap adds it. Each tap is
       arrive -> press (thumb scales down AND the target lights) -> release
   and only then does the screen it causes start to move:
       Back       arrive 13.5  press 14.6-15.6  transition 16-21
       Summit     arrive 27    press 27.6-28.6  transition 31-36
       Website    arrive 40    press 40.8-41.8  transition 45-50
       Back (br)  arrive 67.2  press 67.8-68.6  transition 69-74
       Call       arrive 76.6  press 77.2-78.2  call screen 80-85
   (all % of the scrub). The fallback values in var() are only for the first
   frame before the measurement lands. */
@keyframes story-s1{0%,17.04%{translate:0 0;animation-timing-function:cubic-bezier(.5,0,.15,1)}20.74%,100%{translate:100% 0}}
@keyframes story-s2{0%,17.04%{translate:-100% 0;animation-timing-function:cubic-bezier(.5,0,.15,1)}20.74%,33.33%{translate:0 0;animation-timing-function:cubic-bezier(.5,0,.15,1)}37.04%,100%{translate:-30% 0}}
@keyframes story-s3{0%,33.33%{translate:100% 0;scale:1;opacity:1;animation-timing-function:cubic-bezier(.5,0,.15,1)}37.04%,48.89%{translate:0 0;scale:1;opacity:1;animation-timing-function:cubic-bezier(.5,0,.15,1)}52.59%,71.85%{translate:0 0;scale:.94;opacity:.55;animation-timing-function:cubic-bezier(.5,0,.15,1)}75.56%,100%{translate:0 0;scale:1;opacity:1}}
@keyframes story-s4{0%,48.89%{translate:0 100%;animation-timing-function:cubic-bezier(.5,0,.15,1)}52.59%,71.85%{translate:0 0;animation-timing-function:cubic-bezier(.5,0,.15,1)}75.56%,100%{translate:0 100%}}
@keyframes story-s5{0%,80%{opacity:0;scale:.96}83.7%,100%{opacity:1;scale:1}}
@keyframes story-list{0%,21.11%{translate:0 0;animation-timing-function:cubic-bezier(.5,0,.15,1)}23.33%,100%{translate:0 var(--list-shift,-3em)}}
@keyframes story-load{0%,51.85%{scale:0 1;opacity:1}54.81%{scale:1 1;opacity:1}56.3%,100%{scale:1 1;opacity:0}}
@keyframes story-settle{0%,51.85%{opacity:0;scale:1.04}54.81%{opacity:1}58.52%,100%{opacity:1;scale:1}}





/* MESSAGES IN THE PHONE (2026-11-13): each slide's caption is shown as a card over
   the screen in a pause after its slide; every story keyframe was remapped so
   the five pauses (old % 13, 30, 44, 62, 91; 7 points each) hold the whole scene. */
@keyframes story-c1{0%,9.63%{opacity:0;scale:.94}10.36%,14.09%{opacity:1;scale:1}14.81%,100%{opacity:0;scale:.97}}
@keyframes story-c2{0%,27.41%{opacity:0;scale:.94}28.13%,31.87%{opacity:1;scale:1}32.59%,100%{opacity:0;scale:.97}}
@keyframes story-c3{0%,42.96%{opacity:0;scale:.94}43.69%,47.42%{opacity:1;scale:1}48.15%,100%{opacity:0;scale:.97}}
@keyframes story-c4{0%,61.48%{opacity:0;scale:.94}62.21%,65.94%{opacity:1;scale:1}66.67%,100%{opacity:0;scale:.97}}
@keyframes story-c5{0%,88.15%{opacity:0;scale:.94}88.87%,92.61%{opacity:1;scale:1}93.33%,100%{opacity:0;scale:.97}}
@keyframes story-dim{0%,88.15%,93.33%{opacity:1}96.3%,100%{opacity:.28}}
@keyframes story-end{0%,88.15%,93.33%{opacity:0;translate:0 10px}96.3%,100%{opacity:1;translate:0 0}}
@keyframes story-press-back{0%,16%{opacity:0}16.15%,16.74%{opacity:1}17.33%,100%{opacity:0}}
@keyframes story-press-pick{0%,25.63%{opacity:0}25.78%,100%{opacity:1}}
@keyframes story-press-site{0%,40.59%{opacity:0}40.74%,41.33%{opacity:1}42.22%,100%{opacity:0}}
@keyframes story-press-brback{0%,70.96%{opacity:0}71.11%,71.56%{opacity:1}72.15%,100%{opacity:0}}
@keyframes story-press-call{0%,77.93%{opacity:0}78.07%,78.67%{opacity:1}79.56%,100%{opacity:0}}
@keyframes story-ring1{0%,83.7%{opacity:0;scale:1}84.44%{opacity:.55;scale:1}86.67%,100%{opacity:0;scale:1.7}}
@keyframes story-ring2{0%,85.56%{opacity:0;scale:1}86.3%{opacity:.55;scale:1}88.15%,93.33%,100%{opacity:0;scale:1.7}}
@keyframes story-nosite{0%,1.11%{opacity:0}2.59%,100%{opacity:1}}
@keyframes nosite-breathe{0%,100%{scale:1}50%{scale:1.07}}
@keyframes story-thumb{
  10.15%,14.3%{opacity:0}
  27.93%,32.07%{opacity:0}
  43.48%,47.63%{opacity:0}
  62%,66.15%{opacity:0}
  88.67%,92.81%{opacity:0}
  0%{opacity:0;translate:var(--t-nosite,40cqw 60cqh);scale:1}
  2.22%{opacity:1;translate:var(--t-nosite,40cqw 60cqh)}
  9.63%,14.81%{opacity:1;translate:var(--t-nosite,40cqw 60cqh);scale:1}
  15.19%,16%{translate:var(--t-back1,8cqw 8cqh);scale:1}
  16.15%,16.74%{translate:var(--t-back1,8cqw 8cqh);scale:.78}
  17.04%,20.74%{translate:var(--t-back1,8cqw 8cqh);scale:1;opacity:1}
  21.11%{translate:var(--t-swipe-lo,60cqw 72cqh);opacity:1}
  23.33%{translate:var(--t-swipe-hi,60cqw 48cqh)}
  25.19%,25.63%{translate:var(--t-pick,50cqw 40cqh);scale:1}
  25.78%,26.37%{translate:var(--t-pick,50cqw 40cqh);scale:.78}
  26.96%,37.04%{translate:var(--t-pick,50cqw 40cqh);scale:1}
  40%,40.59%{translate:var(--t-site,60cqw 52cqh);scale:1}
  40.74%,41.33%{translate:var(--t-site,60cqw 52cqh);scale:.78}
  42.22%,50.37%{translate:var(--t-site,60cqw 52cqh);scale:1;opacity:1}
  51.85%,69.63%{translate:var(--t-site,60cqw 52cqh);opacity:0}
  70.07%{translate:var(--t-brback,10cqw 94cqh);opacity:1}
  70.52%,70.96%{translate:var(--t-brback,10cqw 94cqh);scale:1}
  71.11%,71.56%{translate:var(--t-brback,10cqw 94cqh);scale:.78}
  72.15%,74.07%{translate:var(--t-brback,10cqw 94cqh);scale:1;opacity:1}
  77.48%,77.93%{translate:var(--t-call,10cqw 52cqh);scale:1}
  78.07%,78.67%{translate:var(--t-call,10cqw 52cqh);scale:.78}
  79.56%{translate:var(--t-call,10cqw 52cqh);scale:1;opacity:1}
  81.48%,100%{translate:var(--t-call,10cqw 52cqh);scale:1;opacity:0}}

/* ── THE BENCH HEADING'S MEASURE IN THE PIN ───────────────────────────────
   The heading became "The founders who keep things running while you focus
   on your business." (2026-11-02), four times the old line. At main.css's
   18ch it set in THREE lines inside the pinned stage, whose card size is
   fitted to the head's height (--bench-fit in main.css): measured leftover
   fell from 91–149px to 12–52px, 12.2px at 2560x800. The pinned head already
   spans the whole rail (--bench-railw), so the heading is let use it and
   text-wrap:balance sets it as two even lines; on viewports ~800px tall the
   rail is still too narrow for two at main.css's 7vh ceiling, so the ceiling
   becomes 6.2vh here (56px -> 49.6px at 800 tall; tall viewports are
   width-bound and unchanged).
   `.bench .bench-head h2`, ONE CLASS HEAVIER than main.css's rule, on purpose:
   in Firefox the pinned bench rules are re-inserted at the END of <head> by
   js/main.js (BENCH FALLBACK), after this file, so equal specificity would
   lose on source order there and win in Chrome. Measured: it did exactly
   that until the extra class. Same media as §THE LANDING's
   pin, deliberately WITHOUT its @supports (the measure suits the unpinned
   desktop roster too, and an @supports gate is what stranded Firefox once). */
@media (prefers-reduced-motion:no-preference) and (min-width:900px) and (min-height:600px){
  .bench .bench-head h2{max-width:none;font-size:min(clamp(2.8rem,5.4vw,5.8rem),6.2vh)}
}
/* Phones: the bench head is flow content ABOVE the deck's pin (main.css §THE
   DECK), so its size is a reading choice, not a height budget. At main.css's
   13vw / 14ch the new heading set in six lines at 390 and eight at 320 —
   a wall before the first person. 10vw with the rail's full width brings it
   to four balanced lines. One class heavier for the same Firefox reason as
   the rule above. */
@media (max-width:599px){.bench .bench-head h2{font-size:clamp(2.2rem,10vw,3.2rem);max-width:none}}
/* THE PINNED RAIL IS SIZED, NOT max-content, ONCE THE PLATES HOLD PHOTOS.
   main.css sets .bench-rail{width:max-content;margin:auto} and centres it.
   With the headshots in (960px-intrinsic <img>s, 2026-11-02) Firefox takes the
   rail's max-content from the images' natural width rather than the members'
   flex-basis, so the rail grew wider than the viewport, its auto margins fell
   to 0 and the three records sat ~180px left of the head at 1280x800 — while
   Chromium, which uses the flex-basis, stayed centred. --bench-railw is the
   rail's real width, computed by the pinned block itself, so this is the same
   number Chromium already lands on. Outside the pin the variable does not
   exist and the declaration falls back to auto. One class heavier for the
   Firefox re-insertion reason above. */
@media (prefers-reduced-motion:no-preference) and (min-width:900px) and (min-height:600px){
  .bench .bench-rail{width:var(--bench-railw)}
  /* THE COPY BLOCK GREW AGAIN (Sarah's bio, 2026-11-02): main.css fits the
     square plate as 61vh - 224px against the record's text rows. With the
     operator's longer bios Firefox at 900x730 set Sarah's in eight lines and
     the stage overflowed by 3.7px. 12px more for the copy, i.e. a 12px
     smaller portrait; re-measured below. main>.bench outweighs the .bench
     rule Firefox re-inserts after this file. */
  main>.bench{--bench-fit:calc(61vh - 236px)}

  /* ── SHRINK TO FIT, DON'T DROP (operator, 2026-11-02) ──────────────────────
     The pin runs from 600px tall again (the floor briefly went to 780 to dodge
     the longer copy). What made short windows overflow was a coupling in
     main.css: the record's WIDTH is --bench-card = min(width share, --bench-fit),
     so a short window shrank the portrait AND narrowed the card, the bio rewrapped
     into more lines, and the record got TALLER exactly when height ran out.
     Here the two are decoupled on short windows only: the card keeps a readable
     width (never narrower than 340px while the width share allows it), and the
     square portrait alone takes the height budget (plate width = --bench-fit).
     Where --bench-fit is already >= 340px (taller windows) the card is
     min(width share, --bench-fit) exactly as before, so nothing changes there.
     The text then scales with height within readable limits: the bio between
     13px and 14px (it never goes below 13), its line box 17.5-21px, and the
     paddings and margins between 60% and 100% of their designed values. The
     bio's 34ch cap is lifted in the pin so it uses the card's width.
     main>.bench / .bench …: heavier than the rules Firefox re-inserts after this
     file (js/main.js, BENCH FALLBACK). */
  main>.bench{--bench-card:min(calc((var(--bench-content) - (var(--bench-n,3) - 1) * var(--bench-gap)) / var(--bench-n,3)),max(var(--bench-fit),340px))}
  main>.bench .bench-plate{width:min(100%,var(--bench-fit));margin-bottom:min(clamp(16px,1.6vw,24px),2.2vh)}
  main>.bench .bench-record{padding-top:min(clamp(18px,1.6vw,26px),2.6vh)}
  main>.bench .bench-record h3{margin-top:min(14px,1.7vh);font-size:min(clamp(1.7rem,2.4vw,2.9rem),4vh)}
  main>.bench .bench-record p{margin-top:min(12px,1.5vh);max-width:none;font-size:clamp(13px,1.75vh,14px);line-height:clamp(17.5px,2.6vh,21px)}
}

/* The statement's clip (js/main.js, LAZY VIDEO): over its poster, fading in on
   its own 'playing' event. Hidden under reduced motion, where it never loads. */
.image-statement video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .5s ease}
.image-statement video.is-playing{opacity:1}
/* A decorative clip sitting exactly on its poster (contact block, guide plates):
   the frame takes the poster's box, whatever max-height or crop that has. */
.clip-frame{position:relative;display:block}
.clip-frame picture{display:block}
.clip-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .5s ease;border-bottom-right-radius:clamp(24px,2.6vw,44px);pointer-events:none}
.clip-frame video.is-playing{opacity:1}
@media(prefers-reduced-motion:reduce){.clip-frame video{display:none}}
@media(prefers-reduced-motion:reduce){.image-statement video{display:none}}
/* The second sentence of the contact heading's message (2026-11-02), set as a
   paragraph under it; it also takes the place the email link left. */
.contact-note{margin:clamp(16px,1.8vw,26px) 0 0;max-width:44ch;font-size:clamp(1.05rem,1.4vw,1.25rem);line-height:1.45}
/* The two listings' header photos (2026-11-02): our own plumbing-site imagery
   (design/landingpages/sites/plumbing*, generated for this project; no brand,
   text or phone number in frame). Summit gets the polished on-the-job shot,
   Ridgeline a plainer one, so the contrast between the businesses still reads.
   js/main.js (STORY/BENCH PRELOAD) loads and decodes them before the story is
   on screen, so neither pops in mid-scrub. */
.yp-shot img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.yp-photo:has(.yp-shot)::before{display:none}

/* ── MOBILE MENU MOTION (js/main.js, MOBILE MENU) ──────────────────────────
   Only under .js: without script the menu is exactly what it was. The panel
   slides down from behind the top edge on open (360ms, ease-out) and back up on
   close (240ms); its items fade and rise 35ms apart (capped at the 10th). The
   underline cascade is the site's 1px text underline drawn under each link's
   TEXT (--uw), left to right, then retracted right to left. */
.js .mobile-menu{transform:translateY(-100%);transition:transform .36s cubic-bezier(.2,.8,.2,1)}
.js .mobile-menu.is-open{transform:none}
.js .mobile-menu.is-closing{transition:transform .24s cubic-bezier(.4,0,1,1)}
.js .mobile-menu>*{opacity:0;transform:translateY(-6px);transition:opacity .26s ease,transform .3s cubic-bezier(.2,.8,.2,1);transition-delay:calc(var(--i,0) * 35ms + 60ms)}
.js .mobile-menu.is-open>*{opacity:1;transform:none}
.js .mobile-menu.is-closing>*{transition-delay:0s;transition-duration:.14s}
.js .mobile-menu>a{position:relative}
.js .mobile-menu>a::after{content:"";position:absolute;left:0;bottom:9px;width:var(--uw,0);height:1px;background:currentColor;transform:scaleX(0);transform-origin:left;pointer-events:none}
.js .mobile-menu.is-cascade>a.in-cascade::after{animation:mm-underline .56s var(--d,0ms) backwards}
/* A tapped link draws its underline and holds it while the page changes (main.js, THE TAP UNDERLINE). */
.js .mobile-menu>a.is-tapped::after{transform:scaleX(1);transition:transform .22s cubic-bezier(.33,0,.2,1)}
/* The underline IS the tap feedback, so the mobile browsers' grey/blue tap box would only compete with it. */
.js .mobile-menu>a{-webkit-tap-highlight-color:transparent}
@keyframes mm-underline{0%{transform:scaleX(0);transform-origin:left;animation-timing-function:cubic-bezier(.3,.7,.2,1)}46%{transform:scaleX(1);transform-origin:left}54%{transform:scaleX(1);transform-origin:right;animation-timing-function:cubic-bezier(.6,0,.8,.3)}100%{transform:scaleX(0);transform-origin:right}}
@media(prefers-reduced-motion:reduce){
  .js .mobile-menu,.js .mobile-menu.is-closing{transform:none;opacity:0;transition:opacity .12s linear}
  .js .mobile-menu.is-open{opacity:1}
  .js .mobile-menu>*{transform:none;opacity:1;transition:none}
  .js .mobile-menu.is-cascade>a.in-cascade::after{animation:none}
}

/* CLIENT PORTAL (2026-11-09). The way back in for existing clients, on every
   page's header and as the hero's second action. The header link borrows the
   menu button's outline and currentColor, so it reads on the white-on-video
   header and on the graphite-on-paper one alike. */
.site-header{justify-content:flex-start;gap:16px}
.site-header>.brand{margin-right:auto}
.header-portal{display:inline-flex;align-items:center;min-height:40px;padding:0 14px;border:1px solid currentColor;color:inherit;font-size:12px;font-weight:500;text-decoration:none;white-space:nowrap;transition:background .2s,color .2s}
.header-portal:hover{background:rgba(128,128,128,.16)}
.header-portal:focus-visible{outline:3px solid var(--accent-deep);outline-offset:3px}
.hero-actions{display:flex;flex-wrap:wrap;gap:12px}
.hero-portal{background:transparent;border-color:#fff;color:#fff}
.hero-portal span{color:#fff}
.hero-portal:hover{background:rgba(255,255,255,.12)}
@media(max-width:599px){.header-portal{min-height:36px;padding:0 10px}.site-header{gap:10px}}

/* GUIDE VIDEO (2026-11-12). A 16:9 click-to-play plate: the thumbnail and a
   play mark until pressed, then the YouTube player in the same box. */
.page-video{position:relative}
.page-video .page-video-play,.page-video iframe{display:block;width:100%;aspect-ratio:16/9;border:0;padding:0;margin:0;background:#111}
.page-video-play{position:relative;cursor:pointer;overflow:hidden}
.page-video-play img{display:block;width:100%;height:100%;object-fit:cover;transition:opacity .2s}
.page-video-play:hover img{opacity:.88}
.page-video-icon{position:absolute;left:50%;top:50%;translate:-50% -50%;opacity:.92;transition:opacity .2s}
.page-video-play:hover .page-video-icon,.page-video-play:focus-visible .page-video-icon{opacity:1}
.page-video-play:focus-visible{outline:3px solid var(--accent-deep);outline-offset:4px}
.page-video figcaption{margin-top:10px;font-size:13px;opacity:.75}
@media (prefers-reduced-motion:reduce){.page-video-play img,.page-video-icon{transition:none}}
