/* ============================================================================
   OROVELL / CONCEPT C  "THE PROCESSION"
   Structure, motion and spatial feel modelled on venteru's home-v2:
   dark/light banding, a sticky stacking deck, three long scrub stages, an
   odometer. The hero is "the frame": one full-bleed still that never moves,
   with four projects dissolving inside it. The site chrome (tokens, nav,
   drawer, buttons, footer) is css/chrome.css, loaded after this sheet on
   every page including the home; the :root tokens below are the same names
   at the same values, so the two sheets never compete.
   Brand is unchanged: 4 colours, Aboreto + Figtree, sharp geometry.

   APPROVED DEVIATIONS (client signed off, everything else in the V3 contract
   still binds):
     1. radius token --r: 5px on cards/images/panels/inputs. Buttons stay
        rectangles at 5px. No pills anywhere.
     2. gold is used as a FILL under obsidian text (buttons, active deck card,
        step cards, tags, hover wipes). Wine is the second accent for large
        quiet surfaces. Neither is ever a body or label text colour.
     3. negative tracking on display type only, -0.02em to -0.045em, tuned per
        size. Aboreto is inscriptional and collapses if pushed further.
   ========================================================================= */

/* ------------------------------------------------------------------ tokens */
:root{
  --obsidian:#050505;
  --gold:#E0B87E;
  --wine:#4B1928;
  --linen:#F5F5F4;

  /* linen at graded alphas: the only "greys" on the dark */
  --l90:rgba(245,245,244,.90);
  --l72:rgba(245,245,244,.72);
  --l55:rgba(245,245,244,.55);
  --l45:rgba(245,245,244,.45);
  --l30:rgba(245,245,244,.30);
  --l20:rgba(245,245,244,.20);
  --l12:rgba(245,245,244,.12);
  --l06:rgba(245,245,244,.06);

  /* obsidian at graded alphas: the same job on the linen bands */
  --o72:rgba(5,5,5,.72);
  --o55:rgba(5,5,5,.55);
  --o45:rgba(5,5,5,.45);
  --o20:rgba(5,5,5,.20);
  --o12:rgba(5,5,5,.12);

  /* contract rule 7: ONE hairline alpha governs every rule on the page.
     Dashed versus solid is the only distinction a border is allowed to carry. */
  --hair:rgba(245,245,244,.16);
  --hair-d:rgba(5,5,5,.16);

  /* deviation 1: one house radius, everywhere */
  --r:5px;

  --shell:1500px;
  --rail:1240px;
  --gut:clamp(20px,4.2vw,56px);
  --pad:clamp(76px,9.4vw,148px);
  /* Daylight's section breath, 140 to 180: the pass-2 chapters open on it */
  --pad-xl:clamp(120px,12.5vw,180px);
  /* the craft run's outer column, Daylight's span-w-6 (335 of 1440) */
  --run-col:clamp(260px,23.3vw,360px);

  --ease:cubic-bezier(.19,1,.22,1);
  --circ:cubic-bezier(.075,.82,.165,1);
  --odo:cubic-bezier(.649,.328,.257,.662);
  /* the dissolve curve: a symmetric ease in and out, the CSS equivalent of
     GSAP's power2.inOut. Nothing else on the page uses it, because nothing else
     hands one full-bleed layer over to another. */
  --diss:cubic-bezier(.45,.05,.55,.95);
}

/* ------------------------------------------------------------------ reset */
*,*::before,*::after{ box-sizing:border-box; }
/* NOTE: no `scroll-behavior:smooth` here. ScrollTrigger jumps the scroller to 0
   and back while it measures, and a CSS smooth scroll turns that instant jump
   into an animation, so every trigger records the wrong start. Smooth anchor
   scrolling is done per click in vc.js instead. */
html{ -webkit-text-size-adjust:100%; }
body{
  margin:0;
  /* rule 16: never leave large areas pure obsidian */
  background:linear-gradient(rgba(75,25,40,.06), rgba(75,25,40,.02)) var(--obsidian);
  color:var(--l90);
  font:300 15px/1.75 Figtree, system-ui, sans-serif;
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
}
body.pre-lock{ overflow:hidden; }
body.menu-open{ overflow:hidden; }
/* height:auto is load-bearing: every <img> carries width/height attributes for
   aspect ratio, and without it max-width:100% shrinks the width while the
   presentational height sticks, which stretched the Standard band photograph. */
img,video,svg{ display:block; max-width:100%; height:auto; }
a{ color:inherit; text-decoration:none; }
button,input{ font:inherit; color:inherit; background:none; border:0; padding:0; }
button{ cursor:pointer; }
ol,ul{ margin:0; padding:0; list-style:none; }
h1,h2,h3,h4,p,figure,blockquote,dl,dd{ margin:0; }
::selection{ background:var(--gold); color:var(--obsidian); }
:focus-visible{ outline:1px solid var(--gold); outline-offset:3px; }
/* .sr-only and the .dm / .ds type ranks below are currently unused by this page.
   They stay: .sr-only is the a11y net any new visually-hidden label needs, and
   .dm / .ds are two of the four declared display ranks. */
.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

/* --------------------------------------------------------------- type ranks */
/* register 1: Aboreto display, uppercase. Deviation 3 lives here and only here,
   on the two hero ranks. Client ruling 2026-09-16: the whole scale is about
   22% smaller than the first build (the site read text-heavy); the ratios
   between the four ranks are unchanged. The .dm and .ds ranks keep
   letter-spacing 0, because this face pinches at small sizes long before it
   does at display sizes. */
.dh,.dl,.dm,.ds{
  font-family:Aboreto, Georgia, serif;
  font-weight:400;
  text-transform:uppercase;
  color:var(--linen);
  line-height:1.04;
}
.dh{ font-size:clamp(32px, min(5.9vw, 8.3svh), 80px); letter-spacing:-.032em; }
.dl{ font-size:clamp(32px, 4.2vw, 60px);  letter-spacing:-.028em; }
.dm{ font-size:clamp(22px, 2.65vw, 38px); letter-spacing:0; line-height:1.1; }
.ds{ font-size:clamp(17px, 1.5vw, 22px);  letter-spacing:0; line-height:1.18; }
.on-linen .dh,.on-linen .dl,.on-linen .dm,.on-linen .ds{ color:var(--obsidian); }
/* the masked line, contract rule 19: one overflow:hidden mask per authored
   line, the inner span is what moves. Pre-authored in markup, never split at
   runtime. The hero declares its own copy of this pair and is untouched; the
   pass-2 chapters (the craft run titles, the anatomy) share this one. */
.hl{ display:block; overflow:hidden; }
.hl > span{ display:block; }

/* register 2: tiny Figtree plate caps. The ONLY positively tracked text. */
.lbl{
  font-family:Figtree, system-ui, sans-serif;
  font-size:11px; font-weight:400; line-height:1.6;
  letter-spacing:.12em; text-transform:uppercase;
  color:var(--l45);
}
.on-linen .lbl{ color:var(--o45); }

.prose{ max-width:58ch; color:var(--l72); font-size:15px; line-height:1.75; }
.on-linen .prose{ color:var(--o72); }

/* ------------------------------------------------------------------ layout */
.shell{ width:100%; max-width:var(--shell); margin-inline:auto; padding-inline:var(--gut); }
.sec{ position:relative; padding-block:var(--pad); }
.band-linen{ background:var(--linen); color:var(--o72); }
/* .band-wine is gone with the statement band it dressed. Wine's "large quiet
   surface" role (deviation 2) is carried by the anatomy chapters and the
   Standard Test, both on the oxblood wash. */

/* --------------------------------------------------------- grade: grain + vignette */
/* the keyframes translate by at most 2% of this box, so a 6% bleed is all the
   cover it needs. inset:-50% was painting four viewports of tiled noise. */
.grain{
  position:fixed; inset:-6%; z-index:60; pointer-events:none; opacity:.045;
  will-change:transform;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/><feColorMatrix type='saturate' values='0'/></filter><rect width='140' height='140' filter='url(%23n)' opacity='.55'/></svg>");
  animation:grainshift 7s steps(6) infinite;
}
@keyframes grainshift{
  0%{transform:translate(0,0)} 20%{transform:translate(-2%,1%)}
  40%{transform:translate(1%,-2%)} 60%{transform:translate(-1%,-1%)}
  80%{transform:translate(2%,1%)} 100%{transform:translate(0,0)}
}
.vignette{
  position:fixed; inset:0; z-index:59; pointer-events:none;
  background:radial-gradient(122% 92% at 50% 45%, rgba(5,5,5,0) 44%, rgba(5,5,5,.32) 100%);
}

/* ---- the button family (.btn, .btn-sm, .wipe, .btn-gold, .btn-wine,
   .btn-line, .roll and the surface-scoped primaries) lives in css/chrome.css,
   which index.html loads after this sheet. One definition for every page. */


/* --------------------------------------------------------------- media slots */
.fig{ position:relative; overflow:hidden; border-radius:var(--r); background:rgba(75,25,40,.10);
  outline:1px solid var(--hair-d); outline-offset:-1px; }
/* No .hovzoom here any more. It scaled project images to 1.06 on hover, which
   rule 6 ("NO hover scale/zoom") and rule 20 ("NO image zooms") both forbid, and
   it was not one of the three signed deviations in the header block. The project
   cards keep the hover language the contract does allow: the .pc-name roll-up
   and the tag fill, both of which fire on the same hover. Nothing on this page
   transforms a .fig child now, so the transition that fed the zoom is gone too. */

/* small translucent tag, 5px, never a pill */
.tag{
  display:inline-block; border-radius:var(--r);
  padding:6px 11px 5px;
  font-family:Figtree, system-ui, sans-serif;
  font-size:10px; letter-spacing:.12em; text-transform:uppercase; line-height:1;
  background:var(--l12); color:var(--l90);
  transition:background-color .3s ease, color .3s ease;
}
/* deviation 2 hands the FILL role to gold and names tags among its uses. It
   grants wine "large quiet surfaces" only, and rule 9 adds "Never colored
   chips", so .tag-wine is gone: a 10px chip is the opposite of a large quiet
   surface. One gold chip per card now means "this is the headline attribute". */
.tag-gold{ background:var(--gold); color:var(--obsidian); }

/* ---------------------------------------------------------- motion A: reveal */
/* venteru's reveal, minus the blur: opacity 0 + 10% -> settled. Rule 13 and
   the 2026-09-16 ruling allow blur on the nav only, so the reveal is opacity
   and transform alone (same 1.15s timing and stagger).
   CRITICAL: the hidden state is armed only on wide viewports AND only when
   vc.js has put .js-on on <html>. A dead script or a phone can never hide
   content. */
/* rules 18 and 21: 1.1-1.4s on the expo family, stagger .15-.18. */
.rv{ transition:opacity 1.15s var(--ease), transform 1.15s var(--ease); }
@media (min-width:992px){
  html.js-on .rv{ opacity:0; transform:translateY(10%); }
  html.js-on .rv.in{ opacity:1; transform:none;
    transition-delay:calc(var(--d,0) * .15s); }
}

/* ================================================================= PRELOADER */
.curtain{
  position:fixed; inset:0; z-index:100;
  display:flex; align-items:center; justify-content:center;
  background:linear-gradient(rgba(75,25,40,.07), rgba(75,25,40,.02)) var(--obsidian);
  cursor:pointer;
  transition:opacity .8s ease, visibility .8s;
}
/* pointer-events goes with .done, not .gone: the fade runs for 900ms after a
   skip and an invisible z-index:100 sheet would eat the first click on the hero. */
.curtain.done{ opacity:0; pointer-events:none; }
.curtain.gone{ visibility:hidden; }
/* the intro plays once per session (client ruling). The head script stamps
   html.intro-seen before first paint on every later load, and the curtain is
   then never rendered at all: no flash, no fade, the hero is simply there. The
   body lock goes with it, so the page scrolls before vc.js has even arrived. */
html.intro-seen .curtain{ display:none; }
html.intro-seen body.pre-lock{ overflow:auto; }

/* ---- unified preloader beats (shared across all versions, copied from the retired concept sheet) */
.curtain .pre-beat{position:absolute;display:flex;flex-direction:column;align-items:center;gap:16px;
  opacity:0;transform:translateY(8px);
  transition:opacity .6s cubic-bezier(.16,1,.3,1),transform .6s cubic-bezier(.16,1,.3,1)}
.curtain .pre-beat.on{opacity:1;transform:none}
.curtain .pre-beat.off{opacity:0;transform:translateY(-10px)}
.curtain .pre-vmb img{height:clamp(72px,11vh,110px);width:auto}
/* rule 3: tracked caps live in the 10-11.5px band and nowhere else */
.curtain .pre-vmb .pre-vm-word{font-family:'Figtree',sans-serif;font-weight:400;font-size:11px;
  letter-spacing:.14em;text-transform:uppercase;color:rgba(245,245,244,.85)}
/* one tune against gate 4: tracking above .05em is a label-only privilege, so
   this beat sheds it as it grows past 12px.
   Deviation 2 closes with "Neither is ever a body or label text colour", and
   rule 9 makes gold a SURFACE and a SIGNAL. This word used to be 17-24px of
   gold type. The signal is now a gold rule under linen type, the same move
   as the section labels. (This comment was left unclosed once, which
   swallowed every rule down to .pre-skip: the Orovell beat never rendered and
   "Click to enter" sat on top of the beats. Closed 2026-09-16.) */
.curtain .pre-now span{display:block;font-family:Aboreto,Georgia,serif;font-size:clamp(17px,2vw,24px);
  letter-spacing:0;text-transform:uppercase;color:var(--linen);padding-bottom:10px;position:relative}
.curtain .pre-now span::after{content:"";position:absolute;left:50%;bottom:0;width:24px;height:1px;
  background:var(--gold);transform:translateX(-50%)}
.curtain .pre-oro .pre-mask{width:min(320px,62vw);aspect-ratio:1998/438;
  background:linear-gradient(110deg, rgba(245,245,244,0) 44%, rgba(245,245,244,.95) 50%, rgba(245,245,244,0) 56%),
             linear-gradient(var(--gold),var(--gold));
  background-size:260% 100%,100% 100%;background-position:220% 0,0 0;background-repeat:no-repeat;
  -webkit-mask:url("../assets/logo-primary-gold.svg") center/contain no-repeat;
  mask:url("../assets/logo-primary-gold.svg") center/contain no-repeat}
.curtain .pre-oro.on .pre-mask{animation:orv-sheen 1.5s cubic-bezier(.65,0,.35,1) .25s forwards}
@keyframes orv-sheen{from{background-position:220% 0,0 0}to{background-position:-140% 0,0 0}}
@supports not ((-webkit-mask:url("")) or (mask:url(""))){
  .curtain .pre-oro .pre-mask{background:none}
  .curtain .pre-oro .pre-mask::after{content:"";display:block;width:100%;height:100%;
    background:url("../assets/logo-primary-gold.svg") center/contain no-repeat}}
.curtain .pre-skip{position:absolute;bottom:26px;left:0;right:0;text-align:center;
  font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:rgba(245,245,244,.45)}

/* ---- the nav, burger, veil and drawer live in css/chrome.css, loaded after
   this sheet on the home too. This sheet keeps only the home's hooks on the
   bar: the load-time slide-in below the hero (parked above the viewport under
   the hero's three locks) and its reduced-motion and no-gsap mirrors. */


/* ============================================================ 2 HERO, "THE FRAME"
   One full-bleed frame that fills the fold and never moves. Four subjects are
   stacked inside it and cross dissolve in place. Everything here is opacity and
   a very slow scale drift; no layout property is ever animated, so the frame
   cannot slide, rotate or re-height.
   No border-radius on the hero media. --r is for the inset cards elsewhere; a
   full-bleed frame with rounded corners is a widget, not a window. */
/* STAGE 2: .hero is the runway, .hero-stage is the box the Frame used to be.
   Static by default (100svh outer, stage in flow, no clip motion). The
   200svh runway and the sticky stage are armed further down, only on the wide
   viewport, only with motion allowed, and only while vc.js has said GSAP is
   really running: the same three locks the headline masks carry. No overflow
   on the outer: an overflow:hidden parent is a scroll container, and a sticky
   child sticks to it instead of the viewport. The stage clips instead. */
.hero{
  position:relative; isolation:isolate;
  min-height:640px; height:100svh;
}
.hero-stage{
  position:relative; isolation:isolate;
  display:flex; align-items:flex-end;
  min-height:640px; height:100svh;
  /* clears the fixed nav at the top and leaves the credit row its air at the
     bottom. Both terms are viewport-aware so a 1280x700 laptop keeps the label,
     all three headline lines, the support line and the buttons inside the fold. */
  padding-top:clamp(96px,14svh,150px);
  padding-bottom:clamp(76px,11svh,118px);
  overflow:hidden; contain:paint;
}

/* ---------------------------------------------------------------- the frame */
/* rule 16: the surface the window retreats into is obsidian warmed with a 5%
   wine wash, never a flat #050505 */
.hero-frame{
  position:absolute; inset:0; z-index:0;
  background:linear-gradient(rgba(75,25,40,.05), rgba(75,25,40,.05)) var(--obsidian);
}
/* the window. clip-path only: the box never changes size, the visible region
   does. vc.js writes the inset in px from the stage's own measurements so the
   hairline, computed from the same numbers, lands exactly on the edge. The
   round term is the one place the film gets --r: at full bleed it is 0 and it
   only arrives with the card. will-change is applied by vc.js inside the
   matchMedia enter and removed in its cleanup, never declared here. */
.hero-clip{ position:absolute; inset:0; clip-path:inset(0 0 0 0 round 0px); }
/* the 1 to 1.06 push. Origin sits on the card's centre line (46vw + 25vw), so
   the picture pushes into the window rather than sliding out of one side. */
.hero-zoom{ position:absolute; inset:0; transform-origin:71% 50%; }
.hsub{
  position:absolute; inset:0;
  opacity:0;
  /* the dissolve. 1.4s on a symmetric ease, the CSS reading of power2.inOut. */
  transition:opacity 1.4s var(--diss);
}
.hsub.is-on{ opacity:1; }
/* the subject itself. object-position is biased above centre on the stills:
   these masters are 3:2 into a ~16:9 frame, and the quarter this drops is the
   bottom one, which is where the burned-in wordmark sits. */
.hm{
  width:100%; height:100%; object-fit:cover; object-position:50% 34%;
  /* the Ken Burns breath: 1 to 1.045 across the whole hold, then an INSTANT
     rewind once the outgoing layer is already invisible. Transitioning back
     over the same 8.4s would drift the wrong way through the dissolve. */
  transform:scale(1); transform-origin:50% 50%;
  transition:transform 0s linear 1.7s;
}
.hsub.is-on .hm{ transform:scale(1.045); transition:transform 8.4s linear; }
/* the film is a vertical 9:16 master in a landscape frame, so cover keeps a
   narrow band of it. 58% is where the seating and the table sit. */
.hm-film{ object-position:50% 58%; }

/* ---------------------------------------------------------------- the grade
   A directional scrim so the copy column always sits on darkness whichever
   subject is in the frame, plus a bottom wash under the credit row and a corner
   vignette. Obsidian alphas only, and the right two thirds stay light enough
   that the room still reads. */
.hero-scrim{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  background:
    linear-gradient(96deg,
      rgba(5,5,5,.76) 0%, rgba(5,5,5,.62) 27%, rgba(5,5,5,.30) 53%,
      rgba(5,5,5,.08) 78%, rgba(5,5,5,.14) 100%),
    linear-gradient(rgba(5,5,5,.30) 0%, rgba(5,5,5,.04) 26%,
      rgba(5,5,5,.08) 54%, rgba(5,5,5,.52) 100%),
    radial-gradient(128% 96% at 50% 46%, rgba(5,5,5,0) 44%, rgba(5,5,5,.36) 100%);
}

/* the gold hairline. Static form first: one 16px inset frame at gold 22%,
   which is what phones, reduced motion and the no-GSAP path all get. The four
   spans inside are only rendered on the desktop scrub (below), where vc.js
   translates them onto the window's edges. Gold as a signal, rule 9: a 1px
   line, never a surface here. */
.hero-hair{
  position:absolute; inset:16px; z-index:2; pointer-events:none;
  border:1px solid rgba(224,184,126,.22); border-radius:var(--r);
}
.hero-hair span{ display:none; }

/* ----------------------------------------------------------------- the copy
   A narrow column against the left, about a third of the shell, never centred
   across the viewport. */
.hero-copy{ position:relative; z-index:3; width:100%; }
/* the column ends before the retreated card begins (vc.js END.l = 46vw):
   gutter + 38vw at 1024 to 1368 (432px vs the card's 471 at 1024, 540 vs 589
   at 1280), gutter + 520px from 1369 up (576 vs 662 at 1440, 266 + 520 = 786
   vs 883 at 1920 once the shell centres). The narrowest clearance is 39px. */
.hero-col{ max-width:clamp(300px, 38vw, 520px); }
/* the four home labels that sit brighter than the label rank's --l45 carry
   their section as a second class: css/chrome.css (loaded after this sheet)
   sets .lbl's colour at the same specificity a bare class would have */
.hero .hero-eyebrow{ color:var(--l55); }
/* its own size ramp rather than .dh's, tuned to the narrow column: the longest
   line is eight Aboreto capitals and it has to stay inside 520px at 1920 and
   inside the gutters at 375. The floor is 40px, so deviation 3's negative
   tracking still only lands at 40px and above. The svh term keeps three lines
   plus the label, the support line and the link inside a 700px fold. */
.hero-h1{
  margin-top:clamp(15px,2.2svh,24px);
  font-size:clamp(32px, min(4.2vw, 7.3svh), 68px);
  letter-spacing:-.03em; line-height:1.06;
}
/* the masks. One per line, pre-authored in the markup. */
.hero-h1 .hl{ display:block; overflow:hidden; }
.hero-h1 .hl > span{ display:block; }
.hero-sup{
  margin-top:clamp(15px,2.2svh,24px); color:var(--l72);
  font-size:15px; line-height:1.75; max-width:32ch;
}
/* client ruling 2026-09-16: a sub-heading under a display heading sits on ONE
   line at desktop. The 44ch cap is the safety net; a line that does not fit
   it is shortened in copy, never wrapped. */
@media (min-width:1024px){
  .hero-sup,.run-sub,.dh + .prose:not(.stat-prose),.dl + .prose:not(.hand-prose){ max-width:44ch; white-space:nowrap; }
  /* the two display lines under the same rule: the craft head's pre-authored
     pair (the <br> is the only break) and the close's question */
  .run-head .run-h2,.close-q{ white-space:nowrap; }
}
/* the secondary block: support line, the two buttons, the trust line. The
   wrapper exists for the desktop scrub, which fades it as one unit (autoAlpha,
   so the buttons also leave the tab order while hidden). */
.hero-sec{ position:relative; }
/* the action row: the primary gold plate and the hairline button, the same two buttons
   the CTA band closes the page with. Contract rule 11. */
.hero-act{
  margin-top:clamp(20px,3svh,32px);
  display:flex; flex-wrap:wrap; align-items:center; gap:12px;
}
/* the trust line under the buttons. A label row: small caps, tracked like
   every other .lbl, separated by 3px gold squares (no circles on this site). */
.hero .hero-caps{
  display:flex; flex-wrap:wrap; align-items:center; gap:10px 14px;
  margin-top:clamp(18px,2.2vw,26px); color:var(--l55);
}
.hero-caps i{ display:block; width:3px; height:3px; background:var(--gold); flex:none; }
/* --------------------------------------------------------- the credit corner
   The opposite bottom corner names whatever is in the frame right now, and
   cross fades with it. This is how the hero says "many projects" without a
   single tile. flex-wrap is the 375px guard: the longest credit plus the
   control is a hair wider than a phone gutter-to-gutter, so the control drops
   to its own line rather than widening the document. */
.hero-meta{
  position:absolute; z-index:3; right:var(--gut); bottom:clamp(20px,3.4svh,34px);
  max-width:calc(100% - var(--gut) * 2);
  display:flex; align-items:center; justify-content:flex-end;
  gap:14px; flex-wrap:wrap;
}
.hero-credit-w{ min-width:0; }
.hero .hero-credit{ color:var(--l55); text-align:right; transition:opacity .55s ease; }
.hero-credit.swap{ opacity:0; }

/* the film's own control, following the AP precedent: visibly tiny, keyboard
   reachable, and only present while the film is the subject. visibility rather
   than display, so the credit beside it never jumps. */
.hero-vid{
  flex:none; width:28px; height:28px;
  border:1px solid var(--l30); border-radius:var(--r);
  display:grid; place-items:center; color:var(--l72);
  visibility:hidden; opacity:0;
  transition:color .2s ease, border-color .2s ease, opacity .3s ease;
}
.hero.is-film .hero-vid{ visibility:visible; opacity:1; }
.hero-vid:hover,.hero-vid:focus-visible{ color:var(--linen); border-color:var(--gold); }
.hv-pause{ width:9px; height:11px; border-left:2px solid currentColor; border-right:2px solid currentColor; }
.hv-play{
  display:none; width:0; height:0; margin-left:2px;
  border-left:9px solid currentColor;
  border-top:6px solid transparent; border-bottom:6px solid transparent;
}
.hero-vid.is-paused .hv-pause{ display:none; }
.hero-vid.is-paused .hv-play{ display:block; }

/* the masked line rise, armed only where it can be un-armed again: vc.js must
   have said GSAP is really running, and the visitor must not have asked for
   less motion. Under reduce, with JS off, or with the CDN blocked, the three
   lines are simply there. */
@media (prefers-reduced-motion:no-preference){
  html.js-on:not(.no-gsap) .hero-h1 .hl > span{ transform:translateY(110%); }
  /* the nav arrives after the first frame, Daylight's move: parked above the
     viewport under the same three locks, and vc.js slides it down once the
     curtain lifts. Not scroll driven. The same yPercent + y:0 gotcha applies. */
  html.js-on:not(.no-gsap) .nav{ transform:translateY(-140%); }
}

/* ------------------------------------------------ the runway, desktop only
   The 200svh outer and the sticky stage exist only where the scrub does:
   min-width 1024 (the matchMedia vc.js builds it in), motion allowed, GSAP
   proven. Any of those failing leaves the static Frame above untouched, so a
   throw inside the timeline can never strand a 100svh runway of dead scroll.
   The four hairline spans are rendered here and nowhere else. Only transform,
   opacity and clip-path move on this path; the spans' size and place are set
   once, at the stage's edges, and reached by transform alone. */
@media (min-width:1024px) and (prefers-reduced-motion:no-preference){
  html.js-on:not(.no-gsap) .hero{ height:200svh; min-height:1280px; }
  /* the 640px floor is for the static Frame; while sticky the stage is exactly
     100svh, or a short desktop viewport would overhang the fold for the whole runway */
  html.js-on:not(.no-gsap) .hero-stage{ position:sticky; top:0; min-height:0; }
  html.js-on:not(.no-gsap) .hero-hair{ inset:0; border:0; border-radius:0; }
  html.js-on:not(.no-gsap) .hero-hair span{
    display:block; position:absolute; background:var(--gold); opacity:0;
  }
  /* each span starts 1px OUTSIDE the stage on its own side, full length, and
     is translated in by the inset plus that 1px, then scaled along its length
     to the window's edge minus the corner radius */
  html.js-on:not(.no-gsap) .hero-hair span:nth-child(1){ top:-1px; left:0; width:100%; height:1px; transform-origin:left center; }
  html.js-on:not(.no-gsap) .hero-hair span:nth-child(2){ bottom:-1px; left:0; width:100%; height:1px; transform-origin:left center; }
  html.js-on:not(.no-gsap) .hero-hair span:nth-child(3){ left:-1px; top:0; width:1px; height:100%; transform-origin:center top; }
  html.js-on:not(.no-gsap) .hero-hair span:nth-child(4){ right:-1px; top:0; width:1px; height:100%; transform-origin:center top; }
}

/* ============================================================ 3 PROOF STRIP */
.proof{
  border-top:1px solid var(--hair);
  border-bottom:1px solid var(--hair);
  overflow:hidden; padding-block:16px;
}
/* three sets, cycling by a third. Two sets at 60s ran at about 23px/s, which
   reads as a still image, and at any viewport wider than one set the strip also
   ran out of content before the loop point. vc.js pauses it off screen. */
.proof-track{ display:flex; width:max-content; animation:marq 21s linear infinite; }
.proof-set{ display:flex; }
.proof-set span{
  display:block; padding-inline:clamp(20px,3vw,48px);
  border-right:1px solid var(--hair);
  white-space:nowrap;
}
@keyframes marq{ from{ transform:translateX(0);} to{ transform:translateX(-33.3333%);} }
/* the ribbon rides on the top rule of the craft run's frame, on linen: one
   solid dark hairline below it, the dividers in the same token */
.craft .proof{ border-top:0; border-bottom:1px solid var(--hair-d); }
.craft .proof-set span{ border-right-color:var(--hair-d); }

/* ========================================================== 4 THE STANDARD
   The statement, blueprint section 4, on a true wine band (rule 16): the
   near-black oxblood chapters precede it, the linen slider follows, so no two
   neighbours share a surface. Client, 2026-09-16: the centred column carried
   too much type, so it is now a split. Left, the lead: the two-line claim,
   the four promises it names as a two-by-two of plate caps, the action.
   Right, the ledger: vmmodular's two differences, indexed. Under them the
   kitchen frame runs the full shell and the two counters sit on its left
   edge. The section carries .on-wine so its primary button is the gold fill.
   The masked headline lines rise once (vc.js); everything else is .rv. No
   scrub, no sticky, no rule lines. */
.stat{
  position:relative;
  padding:var(--pad-xl) 0 clamp(60px,7vw,100px);
  background:var(--wine);
}
.stat-grid{ display:grid; grid-template-columns:minmax(0,1fr); gap:clamp(32px,4.4vw,56px); margin-top:clamp(18px,2.2vw,24px); }
/* its own ramp inside .dh: "TO KEEP THE PROMISES" is twenty Aboreto capitals
   and has to sit on one line inside a half shell from 1024 up (about 380px
   there, 560 at 1280, 670 at 1500) */
.stat-h2{ font-size:clamp(26px, min(2.7vw, 5svh), 42px); }
/* the four promises (client, 2026-09-16): the hero's trust-line grammar, a
   3px gold square before each, two columns so the pairs never break mid-line.
   Never outlined: rule 7. Plate caps on wine, so --l72 as in the close. */
.stat .stat-caps{
  display:grid; grid-template-columns:repeat(2, max-content); gap:12px clamp(24px,3vw,40px);
  margin-top:clamp(24px,3vw,36px); color:var(--l72);
}
.stat-caps span{ display:flex; align-items:center; gap:10px; }
.stat-caps span::before{ content:''; display:block; width:3px; height:3px; background:var(--gold); flex:none; }
.stat-act{ margin-top:clamp(24px,3vw,36px); }
/* the ledger: an index in gold, the difference's name as a label, its lines
   as prose inside the 44ch measure. The index and the gap separate the two;
   no hairline. */
.stat-ledger{ display:flex; flex-direction:column; gap:clamp(26px,3.4vw,44px); }
.stat .stat-diff-l{ display:flex; align-items:baseline; gap:14px; color:var(--l72); }
.stat-idx{ color:var(--gold); }
.stat .stat-prose{ margin-top:10px; max-width:44ch; }
/* Daylight's span-w-14 canvas becomes the kitchen photograph: a 2:1 band the
   width of the shell, cut from the 2:3 portrait master a little below centre
   so the counter and the fronts are the band and the wordmark in the
   bottom-left of the source stays outside it. */
.stat-fig{
  width:100%; margin-top:clamp(44px,5vw,72px);
  aspect-ratio:2 / 1; outline-color:var(--hair); background:rgba(75,25,40,.16);
}
.stat-fig img{ width:100%; height:100%; object-fit:cover; object-position:50% 55%; }
/* the wardrobe film over the still, desktop only (vc.js attaches its source) */
.stat-fig{ position:relative; }
.stat-fig video{ display:none; position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.stat-fig video.is-film{ display:block; }
.std-nums{ display:flex; gap:clamp(30px,5vw,72px); flex-wrap:wrap; justify-content:center; }
.stat .std-nums{ margin-top:clamp(40px,4.4vw,64px); justify-content:flex-start; }
.std-num{ text-align:center; }
.std-num .lbl{ margin-top:12px; }
.stat .std-num{ text-align:left; }
.stat .std-num .odo{ justify-content:flex-start; }
@media (min-width:1024px){
  .stat-grid{ grid-template-columns:minmax(0,1fr) minmax(0,1fr); column-gap:clamp(48px,6vw,96px); align-items:start; }
  /* the first index sits on the headline's cap line */
  .stat-ledger{ padding-top:.35em; }
}
@media (max-width:519px){
  .stat .stat-caps{ grid-template-columns:minmax(0,1fr); }
}

/* odometer: fixed-height windows, digit strips sliding under them.
   --odc is the cell height; Aboreto's numerals overshoot a flat 1em box, so the
   cell is a little taller and the glyph is centred inside it. */
.odo{ --odc:1.18em;
  display:flex; align-items:flex-start; justify-content:center; color:var(--gold);
  font-family:Aboreto, Georgia, serif; font-weight:400;
  font-size:clamp(41px,4.7vw,68px); line-height:1; letter-spacing:-.03em; }
.odo-d{ height:var(--odc); overflow:hidden; }
.odo-strip{ display:block; transition:transform 2.5s var(--odo); transform:translateY(0); }
.odo-strip b{ display:flex; align-items:center; justify-content:center;
  height:var(--odc); font-weight:400; line-height:1; }
.odo-plus{ height:var(--odc); display:flex; align-items:center; }

/* ============================================================== 5 THE CRAFT
   Daylight's USP run. The static layout is declared first and is what phones,
   reduced motion, a blocked CDN and JavaScript-off all get: each row is copy in
   flow above its film, nothing fixed, nothing clipped. The framed three-column
   run with the fixed, clip-masked copy is armed further down under the same
   three locks the hero's runway carries (1024px up, motion allowed, GSAP
   proven), so a throw in vc.js can never strand a fixed layer. */
.craft{ position:relative; }
/* the questions section still opens on this class (inline-styled); the run
   itself uses .run-head */
.craft-head{ position:static; padding-bottom:clamp(28px,4vw,52px); }
.run-head{ padding-block:var(--pad-xl) clamp(40px,5vw,72px); text-align:center; }
/* the head's own ramp inside .dl (client ruling 2026-09-16: two pre-authored
   lines, "CRAFTED WITH PRECISION." the longer at thirteen ems, so 3.3vw keeps
   it at 43vw and each line sits on one line from 1024 up; the 24px floor
   keeps it inside a 375px gutter to gutter). No measure cap: the <br> is the
   only break. */
.run-head .run-h2{ max-width:none; margin:18px auto 0; font-size:clamp(24px, 3.3vw, 46px); letter-spacing:-.024em; }

.run{ position:relative; margin-inline:var(--gut); padding-block:clamp(40px,6vw,80px); }
/* the two vertical hairlines, the width of the outer columns, full height of
   the run including its breath above and below the rows. Rendered on the
   desktop path only. */
.run::before,.run::after{
  content:''; display:none; position:absolute; top:0; bottom:0;
  width:var(--run-col); pointer-events:none;
}
.run::before{ left:0; border-right:1px solid var(--hair-d); }
.run::after{ right:0; border-left:1px solid var(--hair-d); }
.run-row{
  display:grid; grid-template-columns:minmax(0,1fr);
  border-bottom:1px solid var(--hair-d);
}
.run-row:first-child{ border-top:1px solid var(--hair-d); }
.run-cell{ min-width:0; }
/* the copy. In flow here; fixed and masked on the desktop path. Register 2
   label, a run-sized display line, a one-sentence sub. */
.run-copy{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:28px; text-align:center; padding-block:64px;
}
.run-copy > *{ max-width:min(88vw,572px); }
/* its own size ramp inside .dm: the longest title is nineteen Aboreto
   capitals and it has to sit on one line inside the centre column at 1024 */
.run-title{ font-size:clamp(21px,2.25vw,33px); letter-spacing:0; line-height:1.1; }
.run-sub{ font-size:15px; line-height:1.75; color:var(--o72); }
.run-fig{ display:flex; justify-content:center; padding-bottom:48px; }
/* 9:16, the outer column's own width on desktop: the film fills its cell edge
   to edge so the row's rules and the column's hairlines frame it exactly */
.run-media{
  position:relative; width:min(60vw,300px); aspect-ratio:9 / 16;
  margin-inline:auto; overflow:clip; border-radius:var(--r);
  outline:1px solid var(--hair-d); outline-offset:-1px;
  background:rgba(75,25,40,.10);
}
.run-media > video,.run-media > img{ width:100%; height:100%; object-fit:cover; }
/* the still: a third taller than its frame and anchored top, the .std-fig
   crop, so the frame's bottom never reaches the burned-in wordmark */
.run-media-still img{ height:133%; object-position:50% 0%; }
/* the turnkey row's call to action sits inside its copy, so it rides the
   fixed layer with the label and the title on the desktop path; the copy's
   own gap spaces it, the margin is for the stacked form */
.run-act{ margin-top:4px; display:flex; align-items:center; justify-content:center; gap:22px; flex-wrap:wrap; }
/* the rows' "Learn more" text link to the matching /interiors page (SEO Phase
   1A, tools/SEO-PHASE1A-SERVICES-SPEC-2026-09-22.md): label rank, the 1px
   gold rule on hover, never a button; it rides the fixed copy like the rest */
.run-more{ position:relative; }
.on-linen .run-more{ color:var(--o72); }
.run-more::after{ content:''; position:absolute; left:0; right:0; bottom:-4px; height:1px;
  background:var(--gold); transform:scaleX(0); transform-origin:left; transition:transform .25s var(--ease); }
.run-more:hover::after,.run-more:focus-visible::after{ transform:scaleX(1); }

/* ---- the framed run, desktop only, under the hero's three locks.
   The centre cell carries clip-path:inset(0) and the copy inside it is
   position:fixed; inset:0: centred in the viewport, visible only through its
   own cell as the cell scrolls past. NOTHING between .run-cell and <body> may
   carry transform, filter, contain, will-change or .rv: a transformed ancestor
   demotes fixed to absolute and the copy would scroll with its row. The fixed
   copy paints under the nav (z 70). Media is not animated. */
@media (min-width:1024px) and (prefers-reduced-motion:no-preference){
  html.js-on:not(.no-gsap) .run{ padding-block:var(--pad-xl); }
  html.js-on:not(.no-gsap) .run::before,
  html.js-on:not(.no-gsap) .run::after{ display:block; }
  html.js-on:not(.no-gsap) .run-row{
    grid-template-columns:var(--run-col) minmax(0,1fr) var(--run-col);
    grid-template-rows:auto;
  }
  html.js-on:not(.no-gsap) .run-cell{ grid-column:2; grid-row:1; clip-path:inset(0); }
  html.js-on:not(.no-gsap) .run-copy{ position:fixed; inset:0; padding:0; }
  html.js-on:not(.no-gsap) .run-copy > *{ max-width:min(40vw,572px); }
  html.js-on:not(.no-gsap) .run-fig{ grid-row:1; grid-column:3; padding:0; }
  html.js-on:not(.no-gsap) .run-row[data-side="left"] .run-fig{ grid-column:1; }
  html.js-on:not(.no-gsap) .run-media{ width:var(--run-col); }
}

/* ========================================================= 6 HOW WE BUILD
   Daylight's three-step, carrying The Standard's six steps. Static first: a
   70svh film band with the chapter's copy on it, then six linen cards in
   flow, copy then tile. The sticky film intro, the fixed linen curtain, the
   sticky 110svh items and the tile mask are armed further down under the
   hero's three locks. The section is linen underneath everything so no seam
   ever shows obsidian. Nothing here counts the cards: any number stacks. */
.hiw{ position:relative; background:var(--linen); }
.hiw-intro{ position:relative; isolation:isolate; min-height:70svh; display:grid; overflow:clip; }
.hiw-intro > video,.hiw-intro > img{ position:absolute; inset:0; z-index:0; width:100%; height:100%; object-fit:cover; }
/* the grade: a directional obsidian scrim so the copy column lands on
   darkness, warmed with wine at 6%. Rule 16. The film is a bright one. */
.hiw-scrim{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  background:
    linear-gradient(rgba(75,25,40,.06), rgba(75,25,40,.06)),
    linear-gradient(96deg, rgba(5,5,5,.62) 0%, rgba(5,5,5,.42) 40%, rgba(5,5,5,.22) 100%);
}
.hiw-intro-copy{
  position:relative; z-index:2; width:100%; max-width:var(--shell); margin-inline:auto;
  padding:clamp(120px,16svh,160px) var(--gut) clamp(64px,9svh,96px);
  display:flex; flex-direction:column; justify-content:center; align-items:flex-start;
}
/* its own ramp inside .dh, the close's: the type ruling of 2026-09-16 took
   the page's chapter headlines under the hero's */
.hiw-h2{ margin-top:clamp(15px,2.2svh,24px); font-size:clamp(30px, min(4.3vw, 7svh), 64px); }
.hiw-intro-copy .prose{ margin-top:clamp(20px,3svh,28px); }
.hiw-act{ margin-top:clamp(24px,3.4svh,34px); }
/* the footnote every asterisk in the six steps points to: plate caps on the
   film's dark wash, quieter than the eyebrow */
.hiw-note{ margin-top:clamp(18px,2.6svh,26px); color:var(--l45); max-width:60ch; }

.hiw-wrap{ position:relative; overflow:clip; }
/* the curtain exists on the desktop path only */
.hiw-curtain{ display:none; }
.hiw-list{ position:relative; z-index:3; display:flex; flex-direction:column; row-gap:20px; padding-block:20px; }
.hiw-item{ padding-inline:20px; }
/* opaque linen, a dark hairline, the house radius. Rule 13: nothing shows
   through to the film. */
.hiw-card{
  position:relative; background:var(--linen); border:1px solid var(--hair-d);
  border-radius:var(--r); overflow:clip;
  display:flex; flex-direction:column; gap:32px; padding:32px 20px;
}
.hiw-copy{ display:flex; flex-direction:column; align-items:flex-start; }
.hiw-copy .dm{ margin-top:16px; max-width:12ch; }
.hiw-sub{ margin-top:14px; }
.hiw-copy .prose{ margin-top:22px; white-space:nowrap; max-width:none; }
.hiw-card-act{ margin-top:32px; }
.hiw-tile-wrap{ display:flex; justify-content:center; }
.hiw-tile{
  position:relative; width:min(60vw,300px); aspect-ratio:9 / 16; max-height:60svh;
  overflow:clip; border-radius:var(--r);
  outline:1px solid var(--hair-d); outline-offset:-1px;
  background:rgba(75,25,40,.10);
}
.hiw-tile > video,.hiw-tile > img{ width:100%; height:100%; object-fit:cover; }
/* the renders are landscape; the wardrobe niches sit left of centre, the
   foyer's feature wall sits dead centre */
.hiw-tile > img{ object-position:32% 50%; }
.hiw-tile > img.hiw-img-c{ object-position:50% 50%; }
/* a bottom wash under the caption, obsidian alpha only */
/* .on-linen .lbl would paint this obsidian; the caption sits on the film's
   dark wash, so it takes linen at the same specificity, declared later */

/* ---- the three-step, desktop only, under the hero's three locks.
   .hiw carries clip-path:inset(0): the curtain inside it is position:fixed
   and overflow cannot clip a fixed descendant, clip-path can, so the linen
   sheet is masked to this section and can never sit over the anatomy below.
   The intro is sticky (never pinned) for the section's whole height; each
   item is sticky at --hiw-run (110svh: the 100svh card plus a 10svh dwell
   before the next card's 100svh crossing; six cards at the old 160svh would
   add four viewports) with the card at its top; the list rides above the
   curtain, the curtain above the film. Only transform, opacity and clip-path
   move here; will-change is applied by vc.js in the matchMedia enter and
   removed in its cleanup, never declared here. */
@media (min-width:1024px) and (prefers-reduced-motion:no-preference){
  html.js-on:not(.no-gsap) .hiw{ clip-path:inset(0); --hiw-run:110svh; }
  html.js-on:not(.no-gsap) .hiw-intro{ position:sticky; top:0; z-index:1; height:100svh; min-height:0; }
  html.js-on:not(.no-gsap) .hiw-curtain{
    display:block; position:fixed; inset:0; z-index:2; background:var(--linen); opacity:0;
    pointer-events:none;
  }
  html.js-on:not(.no-gsap) .hiw-list{ row-gap:160px; padding-block:0; }
  /* the last item can never stick (nothing below it), so its runway leaves
     (--hiw-run - 100svh) of bare curtain before the anatomy; pull the
     section's end up to the last card's bottom. The list's own box is
     unchanged, so the sticky holds for every item but the last and itemTop()
     in vc.js is unaffected; .hiw-wrap's overflow:clip hides the tail. */
  html.js-on:not(.no-gsap) .hiw-list{ margin-bottom:calc(100svh - var(--hiw-run)); }
  html.js-on:not(.no-gsap) .hiw-item{ position:sticky; top:0; height:var(--hiw-run); padding:20px; transform-origin:50% 0; }
  html.js-on:not(.no-gsap) .hiw-card{
    height:calc(100svh - 40px); justify-content:center; gap:0;
    padding:0 0 0 clamp(40px,4.1vw,60px); width:min(100%, 1280px); margin-inline:auto; }
  html.js-on:not(.no-gsap) .hiw-copy{ width:calc(52% - clamp(40px,4.1vw,60px)); max-width:660px; }
  html.js-on:not(.no-gsap) .hiw-tile-wrap{
    position:absolute; top:0; left:52%; width:48%; height:100%; align-items:center;
  }
  html.js-on:not(.no-gsap) .hiw-tile{ width:clamp(300px,26vw,400px); max-height:84svh; }
}

/* ========================================================== 6a AFTER HANDOVER
   vmmodular.com's handover counters (client, 2026-09-16), between the linen
   three-step and the oxblood anatomy: obsidian, warmed with wine at 4% (rule
   16), so no two neighbours share a surface. Editorial, left-aligned: the
   head on the left of the shell, then a hairline-topped row of three counters
   drawn with single-side borders (rule 7), the numerals gold at the
   Standard's odometer ramp (rule 9). No scrub, no sticky; everything is .rv
   and vc.js module 6 rolls the digits. */
.hand{
  padding-block:var(--pad-xl) var(--pad);
  background:linear-gradient(rgba(75,25,40,.04), rgba(75,25,40,.04)) var(--obsidian);
  color:var(--l72);
}
.hand-head{ display:flex; flex-direction:column; align-items:flex-start; }
/* its own ramp inside .dl: "THE HANDOVER IS JUST" is twenty Aboreto capitals
   on one line at 1024, the same constraint the Standard's head carries */
.hand-h2{ margin-top:18px; font-size:clamp(28px, min(3.9vw, 6.2svh), 56px); }
/* a paragraph, not a sub-heading: it is excluded from the one-line rule above
   and wraps inside a 60ch measure (contract rule 2), ranged left with the head */
.hand-prose{ margin-top:clamp(20px,2.4vw,28px); max-width:60ch; white-space:normal; text-align:left; }
.hand-act{ margin-top:clamp(24px,3vw,34px); }
.hand-nums{
  margin-top:clamp(56px,7vw,96px);
  display:grid; grid-template-columns:repeat(3, minmax(0,1fr));
  border-top:1px solid var(--hair);
}
.hand-num{ padding:clamp(28px,3vw,40px) clamp(20px,2.6vw,36px) 0 0; }
.hand-num + .hand-num{ border-left:1px solid var(--hair); padding-left:clamp(20px,2.6vw,36px); }
/* the Standard's odometer, ranged left here rather than centred */
.hand-num .odo{ justify-content:flex-start; }
.hand-num .lbl{ margin-top:14px; }
@media (max-width:700px){
  .hand-nums{ grid-template-columns:minmax(0,1fr); border-top:0; }
  .hand-num{ padding:clamp(20px,4vw,28px) 0; border-top:1px solid var(--hair); }
  .hand-num + .hand-num{ border-left:0; padding-left:0; }
}

/* =========================================================== 6b OUR FACTORY
   Daylight's video chapters, blueprint section 3. Static first: a head, then
   three 70svh bands in flow, each a one-cell grid of media, scrim and copy,
   which is what phones, reduced motion, a blocked CDN and JavaScript-off get.
   The sticky stacking (each chapter sticky at the top of the viewport at
   100svh, the next rising over it) is armed further down under the hero's
   three locks. Oxblood: wine under a heavy obsidian wash, between the
   obsidian #handover band (wine at 4%) and the true wine Standard band.
   The chapters tell the factory story (our own factory, every material named,
   finished like a maison) on the three macro stills. */
.chapters{
  position:relative; isolation:isolate;
  background:linear-gradient(rgba(5,5,5,.66), rgba(5,5,5,.88)) var(--wine);
  color:var(--l72);
}
.chap-head{ padding-block:var(--pad-xl) clamp(40px,5vw,72px); text-align:center; }
/* the head's own ramp inside .dl: "FIFTEEN YEARS. ONE FACTORY." is 27
   Aboreto capitals (about 25ch at this tracking) and has to sit on one line
   inside the shell at 1024, so 3.9vw holds it at about 750px against a 938px
   shell; the <br> is the only break, never a third line */
.chap-head .chap-h2{ max-width:none; margin:18px auto 0; font-size:clamp(28px, min(3.9vw, 6.2svh), 56px); }
/* the sub sits on one line at desktop (the sub-heading ruling, the nowrap is
   set with the other sub lines above): 68 characters of Figtree 300 at 15px
   is about 510px against a 938px shell at 1024, so the 44ch net is released */
.chap-head .prose{ margin:20px auto 0; }
.chap-head .dl + .chap-sub{ max-width:none; }
.chap-list{ position:relative; }
.chap{
  position:relative; display:grid; grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(0,1fr);
  min-height:70svh; overflow:clip;
}
.chap-media{ grid-area:1 / 1; position:relative; overflow:clip; background:rgba(5,5,5,.3); }
/* the parallax handle: 28% taller than its frame so the +-10% of travel the
   scrub gives it never uncovers an edge. The macro frames carry no burned-in
   wordmark, so the picture is centred in the handle. */
.chap-par{ position:absolute; left:0; right:0; top:-14%; bottom:-14%; }
.chap-par > img,.chap-par > video{ width:100%; height:100%; object-fit:cover; object-position:50% 50%; }
/* the grade: wine at .32 over obsidian at .5, so the type lands on oxblood
   whichever frame is under it */
.chap-scrim{
  grid-area:1 / 1; z-index:1; pointer-events:none;
  background:linear-gradient(rgba(75,25,40,.32), rgba(75,25,40,.32)), linear-gradient(rgba(5,5,5,.5), rgba(5,5,5,.5));
}
.chap-copy{
  grid-area:1 / 1; position:relative; z-index:2;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:clamp(20px,3svh,32px); text-align:center;
  padding-block:clamp(56px,8svh,96px);
}
.chap-copy .lbl{ color:var(--l55); }
/* one ramp for all three chapter titles: under the type ruling of
   2026-09-16 a title on a 100svh chapter sits at 46px, not 60 */
.chap-title{ color:var(--linen); max-width:24ch; font-size:clamp(26px, 3.4vw, 46px); letter-spacing:-.02em; line-height:1.06; }
.chap-copy .prose{ max-width:44ch; }
/* the one call to action in the chapters, under chapter 03's prose */
.chap-act{ margin-top:clamp(4px,1svh,10px); }
/* all three chapters share one grammar (client, 2026-09-16: no mixed
   alignments): centred label, one title line, prose inside 44ch */
/* the chapter paragraphs are body copy, not sub-headings: they are released
   from the desktop one-line rule (which .dl + .prose would otherwise catch at
   a higher specificity) and wrap inside the 44ch measure above */
@media (min-width:1024px){
  .chap-copy .dl + .prose{ white-space:normal; max-width:44ch; }
}

/* ---- the chapters, desktop only, under the hero's three locks.
   Each chapter is position:sticky at the top of the viewport for its own
   100svh, constrained by .chap-list, so chapter 1 holds while chapter 2 rises
   over it and chapter 2 holds while chapter 3 does; the last one simply
   scrolls away. No pin. Only transform moves inside (the parallax handle and
   the masked lines); will-change is applied by vc.js in the matchMedia enter
   and removed in its cleanup, never declared here. */
@media (min-width:1024px) and (prefers-reduced-motion:no-preference){
  html.js-on:not(.no-gsap) .chap{ position:sticky; top:0; height:100svh; min-height:0; }
}

/* ================================================= 7b RENDER, THEN REALITY */
/* Ported from V2 (orovell.css .ba-*) and re-expressed in this sheet's tokens:
   --hair-d for the frame, --r for every corner, --o55 for the caption. The
   handle knob and the pair dots were circles in V2; rule 5 keeps circles for
   avatars and the cursor ring only, so both are squares at --r here. The active
   dot is a gold fill under obsidian (deviation 2), the same signal as the active
   deck card. No shadows, no backdrop blur, no hover zoom. */
.ba{ padding-block:var(--pad-xl); }
.ba-head{ padding-bottom:clamp(30px,4vw,56px); }
.ba-head .dl{ max-width:14ch; }
/* the slider block is 80vw and centred (client, 2026-09-16); the shell's
   own width below 1024. The frame is 16:9 at desktop: max-height transfers
   to a max-width through the aspect ratio, so a 3:2 frame would never reach
   80vw on a laptop (795px tall: 84svh x 1.5 = 1002px). At 16:9 the full
   80vw fits every common viewport; the sources are 3:2, so cover crops a
   band top and bottom. */
.ba-head,#ba-sliders{ width:100%; margin-inline:auto; }
/* the head takes the same measure, so the label and the headline sit on the
   frame's left edge (client, 2026-09-16) */
@media (min-width:1024px){ .ba-head,#ba-sliders{ width:min(80vw, 100%); } }
.ba-slider{
  position:relative; aspect-ratio:3/2; max-height:min(84svh, 860px);
  border-radius:var(--r); overflow:hidden;
  outline:1px solid var(--hair-d); outline-offset:-1px;
  background:rgba(75,25,40,.10);
  touch-action:pan-y; user-select:none; -webkit-user-select:none;
}
@media (min-width:1024px){ .ba-slider{ aspect-ratio:16/9; } }
.ba-slider > img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.ba-after{ position:absolute; inset:0; overflow:hidden; clip-path:inset(0 0 0 var(--ba-pos,50%)); }
.ba-after img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.ba-handle{
  position:absolute; top:0; bottom:0; left:var(--ba-pos,50%); width:1px;
  background:var(--gold); z-index:3; pointer-events:none;
}
.ba-handle::after{
  content:"\2194"; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:44px; height:44px; border-radius:var(--r);
  background:var(--gold); color:var(--obsidian);
  display:flex; align-items:center; justify-content:center;
  font-family:Figtree, system-ui, sans-serif; font-size:15px; line-height:1;
}
.ba-tag{
  position:absolute; bottom:14px; z-index:2;
  font-family:Figtree, system-ui, sans-serif;
  font-size:10px; letter-spacing:.12em; text-transform:uppercase; line-height:1;
  padding:7px 12px 6px; border-radius:var(--r);
  background:var(--o72); color:var(--l90);
}
.ba-tag-left{ left:14px; }
.ba-tag-right{ right:14px; }
/* the range input is the keyboard and screen reader path; the pointer path is
   the whole frame, handled in vc.js. It sits above everything, invisible. */
.ba-range{ position:absolute; inset:0; z-index:4; width:100%; height:100%; margin:0;
  opacity:0; cursor:ew-resize; }
.ba-slider:has(.ba-range:focus-visible){ outline:1px solid var(--gold); }
.ba-caption{ margin-top:16px; font-size:13px; color:var(--o55); }
.ba-dots{ display:flex; gap:8px; margin-top:18px; }
.ba-dot{
  width:34px; height:34px; border-radius:var(--r); border:1px solid var(--o20);
  font-family:Aboreto, Georgia, serif; font-size:11px; line-height:1; color:var(--o55);
  transition:background-color .2s ease, border-color .2s ease, color .2s ease;
}
.ba-dot:hover{ border-color:var(--o55); color:var(--obsidian); }
.ba-dot.on{ background:var(--gold); border-color:var(--gold); color:var(--obsidian); }

/* ================================================================ 8 STORIES */
.stories{ position:relative; }
.stories-bg{
  position:absolute; inset:0; z-index:0;
  /* under an .82 to .9 obsidian grade the 1000px frame is indistinguishable
     from the 1800px one; phones get the 520px one (bandwidth, 2026-09-16) */
  background-image:linear-gradient(rgba(5,5,5,.82), rgba(5,5,5,.9)), url("../assets/projects/w-by-dsr/00-m.webp");
  background-size:cover; background-position:center;
  /* scroll, never fixed, at every width: a fixed attachment repaints the
     whole plate under Lenis on every frame */
  background-attachment:scroll;
}
/* 300vh gave the crossing a motion-to-scroll ratio of about 0.23, which reads as
   two pinned cards. 220vh plus the matched px travel in vc.js lands near 0.65. */
.stories-outer{ position:relative; z-index:1; height:220vh; }
.stories-inner{
  position:sticky; top:clamp(96px,14svh,170px);
  height:calc(100svh - clamp(140px,20svh,240px));
  min-height:min(470px, calc(100svh - 104px));
  overflow:hidden;
}
.stories-h{
  position:absolute; inset:0; z-index:1;
  display:flex; align-items:center; justify-content:center; text-align:center;
  pointer-events:none;
}
/* exactly two pre-authored lines, "VOICES FROM OUR" / "TRANSFORMED SPACES.":
   no measure cap, each masked line held on one line, and a ramp whose floor
   keeps the longer line (19 Aboreto capitals, about 14em) inside a phone */
.stories-h h2{ max-width:none; opacity:.9; font-size:clamp(22px, 4.2vw, 60px); }
.stories-h .hl{ white-space:nowrap; }
.rev{
  position:absolute; z-index:2; width:min(400px,84vw);
  border-radius:var(--r); padding:clamp(22px,2.4vw,34px);
  /* no backdrop-filter. These two cards are transform-animated every frame over
     a fixed-attachment background, and a moving backdrop-filter forces a fresh
     backdrop snapshot plus a blur pass per frame. The card is already a wine
     gradient over 86% obsidian, so the blur was contributing almost nothing. */
  background:linear-gradient(rgba(75,25,40,.34), rgba(75,25,40,.16)), rgba(5,5,5,.86);
  border:1px solid var(--hair);
}
.rev-a{ left:clamp(12px,6vw,110px); top:-6%; }
.rev-b{ right:clamp(12px,6vw,110px); bottom:-6%; }
/* gold marks the rating GRAPHIC, not the number: rule 9 reserves gold numerals
   for the Standard band. 11px keeps the tracking inside the label band. */
.rev-stars{ display:flex; align-items:center; gap:9px; color:var(--gold); font-size:11px; letter-spacing:.14em; }
.rev-stars b{ font-weight:400; color:var(--linen); }
.rev blockquote{ margin-top:16px; color:var(--l90); font-size:16px; line-height:1.65; }
.rev-who{ margin-top:18px; display:flex; align-items:baseline; gap:9px; flex-wrap:wrap; }
.rev-who strong{ font-weight:400; color:var(--linen); font-size:14px; }
.rev-tags{ margin-top:18px; display:flex; gap:7px; flex-wrap:wrap; }

/* ---- the reviews wall under the stories stage. The SAME card as .rev: the
   wine gradient over obsidian at .86, the one hairline, the same padding, the
   same star and name treatment; static, three across, no shadow, no blur. */
.rev-wall{ position:relative; z-index:1; padding:0 var(--gut) clamp(64px,8vw,120px); }
.rev-grid{ list-style:none; margin:0; padding:0; display:grid; grid-template-columns:repeat(3,1fr); gap:clamp(14px,1.6vw,22px); }
.rev-card{
  border:1px solid var(--hair); border-radius:var(--r);
  background:linear-gradient(rgba(75,25,40,.34), rgba(75,25,40,.16)), rgba(5,5,5,.86);
  padding:clamp(22px,2.4vw,34px); display:flex; flex-direction:column;
}
.rev-card .rev-stars{ font-size:11px; letter-spacing:.14em; color:var(--gold); }
.rev-card blockquote{ margin:16px 0 0; color:var(--l90); font-size:16px; line-height:1.65; flex:1; }
.rev-card .rev-who{ margin-top:18px; display:flex; align-items:baseline; gap:9px; flex-wrap:wrap; }
.rev-card .rev-who strong{ font-weight:400; color:var(--linen); font-size:14px; }
.rev-more{ margin-top:clamp(26px,3vw,40px); display:flex; justify-content:center; }
@media (max-width:1023px){ .rev-grid{ grid-template-columns:repeat(2,1fr); } }
@media (max-width:640px){ .rev-grid{ grid-template-columns:1fr; } }


/* =============================================================== 9 THE WORK */
/* 140vh, not 200vh: the staircase only travels the height difference between the
   tallest and shortest card, and over 200vh that read as three pinned cards.
   top + min-height is viewport-aware so the stage cannot hang past the fold, and
   overflow:hidden is the guard of last resort against the section below. */
.work-outer{ height:140vh; position:relative; }
.work-inner{
  position:sticky; top:clamp(96px,14svh,170px);
  height:calc(100svh - clamp(140px,20svh,240px));
  min-height:min(520px, calc(100svh - 104px));
  overflow:hidden;
  display:flex; flex-direction:column;
}
/* the same action again under the cards, phones and tablets only (client
   2026-09-16): the head's button is above the fold, this one is where the
   thumb is after the third card */
.work-foot{ display:none; }
@media (max-width:1023px){ .work-foot{ display:block; margin-top:clamp(28px,5vw,40px); } }
.work-head{ display:flex; align-items:flex-end; justify-content:space-between; gap:20px; flex-wrap:wrap;
  padding-bottom:clamp(24px,3vw,44px); }
.work-grid{
  margin-top:clamp(24px,3vw,44px);
  display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:clamp(14px,1.8vw,26px);
  align-items:end;
}
/* rule 22: the catalogue numbering the craft deck and the three-step already
   carry, on the one list that is an actual catalogue */
.work-grid{ counter-reset:pc; }
.pc{ position:relative; counter-increment:pc; }
.pc::before{
  content:counter(pc, decimal-leading-zero);
  position:absolute; top:12px; right:12px; z-index:2;
  font-family:Figtree, system-ui, sans-serif;
  font-size:11px; letter-spacing:.12em; line-height:1;
  color:var(--l72);
}
.pc-fig{ position:relative; border-radius:var(--r); overflow:hidden; }
/* height:133% + top anchor: the .std-fig crop, see .run-media-still img */
.pc-fig img{ width:100%; height:133%; object-fit:cover; object-position:50% 0%; }
/* the deliberately unequal heights, 480 / 392 / 320, held as a ratio so the
   whole staircase still fits inside the sticky stage on a short laptop */
.pc1 .pc-fig{ height:clamp(196px,36svh,480px); }
.pc2 .pc-fig{ height:clamp(160px,29.4svh,392px); }
.pc3 .pc-fig{ height:clamp(131px,24svh,320px); }
.pc-scrim{
  position:absolute; inset:0;
  background:linear-gradient(rgba(5,5,5,.5), rgba(5,5,5,0) 34%, rgba(5,5,5,0) 58%, rgba(5,5,5,.66));
}
.pc-tag-b{ position:absolute; bottom:12px; left:12px; z-index:2; }
/* The container must be a block, or overflow:hidden is a no-op on it and the
   gold duplicate simply prints underneath the title. It also has to carry the
   font-size itself, so its 1.2em window matches the child's line box. */
.pc-name{
  display:block; margin-top:14px; overflow:hidden;
  font-family:Aboreto, Georgia, serif; font-size:clamp(17px,1.5vw,23px);
  letter-spacing:0; text-transform:uppercase; line-height:1.2; height:1.2em;
  color:var(--obsidian);
}
.pc-name i{ display:block; font-style:normal; transition:transform .5s var(--ease); }
/* the duplicate stays obsidian. Rule 20 asks for a roll-up, which is a MOTION
   signal; the gold on hover is carried by the tag fill two rules down, where
   gold is a surface rather than a 23px heading colour. */
.pc-name i::after{ content:attr(data-t); display:block; }
.pc:hover .pc-name i{ transform:translateY(-50%); }
.pc-meta{ display:block; margin-top:6px; }
.pc:hover .tag{ background:var(--gold); color:var(--obsidian); }
.pc .tag{ background:rgba(245,245,244,.82); color:var(--obsidian); }

/* ============================================================= 10 QUESTIONS */
/* on linen (band-linen, on-linen in the markup), blueprint section 9: after
   the Standard Test's oxblood wash and before the wine close */
.qa{ background:var(--linen); }
.qa-grid{ display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:clamp(14px,1.6vw,22px); align-content:start; }
.acc{
  border-radius:var(--r); border:1px solid var(--hair);
  background:rgba(245,245,244,.03);
  padding:0 clamp(16px,1.6vw,24px);
  transition:background-color .3s ease, border-color .3s ease;
}
.acc[open]{ background:rgba(245,245,244,.06); border-color:var(--l30); }
.acc summary{
  list-style:none; cursor:pointer; display:flex; align-items:flex-start; gap:16px;
  padding-block:clamp(16px,1.6vw,22px);
  font-size:15px; line-height:1.55; color:var(--l90);
}
.acc summary::-webkit-details-marker{ display:none; }
.acc summary::after{
  content:'+'; margin-left:auto; flex:none;
  font-family:Figtree, system-ui, sans-serif; font-size:17px; line-height:1; color:var(--gold);
  transition:transform .3s var(--ease);
}
.acc[open] summary::after{ content:'+'; transform:rotate(45deg); }
.acc-in{
  padding-bottom:clamp(16px,1.6vw,22px); padding-right:34px;
  border-top:1px solid var(--hair); padding-top:16px; margin-top:2px;
  color:var(--l72); font-size:14.5px; line-height:1.72;
}
.acc[open] .acc-in{ animation:accIn .45s ease both; }
@keyframes accIn{ from{ opacity:0; transform:translateY(-5px);} to{ opacity:1; transform:none;} }
/* the on-linen variant, never linen-on-linen: obsidian summaries, dark
   hairlines, --o72 answers. The gold plus stays: it is the signal. */
.on-linen .acc{ border-color:var(--hair-d); background:rgba(5,5,5,.03); }
.on-linen .acc[open]{ background:rgba(5,5,5,.05); border-color:var(--o20); }
.on-linen .acc summary{ color:var(--obsidian); }
.on-linen .acc-in{ border-top-color:var(--hair-d); color:var(--o72); }

/* ======================================================= 9b THE STANDARD TEST */
/* The quiz, a client pick (rule 14), re-expressed from the retired V2 sheet in
   this sheet's tokens: --r for every corner, --hair for every rule, squares
   where V2 had circles, no shadow, no blur. Gold is a signal here and never
   running text: the progress rule, the square before a correct verdict, the
   rule beside the Orovell standard, the score numeral (rule 9's numeral
   licence) and the finale's one gold button. The surface is the page's
   oxblood wash, the same as the chapters four sections up. */
.test{
  background:linear-gradient(rgba(5,5,5,.66), rgba(5,5,5,.74)) var(--wine);
  color:var(--l72);
}
.test-head{ text-align:center; max-width:min(920px,100%); margin-inline:auto; }
.test-head .dl{ max-width:18ch; margin:18px auto 0; }
.test-head .prose{ margin:20px auto 0; }
.quiz{ max-width:760px; margin:clamp(36px,4.6vw,64px) auto 0; }
.q-card{
  position:relative; border:1px solid var(--hair); border-radius:var(--r);
  background:rgba(245,245,244,.03); padding:clamp(24px,3.2vw,44px);
  text-align:left;
}
/* the card takes focus on every render so a keyboard user lands on the new
   question, not back at the top of the document; the ring is the page's */
.q-card:focus{ outline:none; }
.q-card:focus-visible{ outline:1px solid var(--gold); outline-offset:3px; }
.q-meta{ display:flex; justify-content:space-between; gap:16px; flex-wrap:wrap; }
.q-q{ text-align:left; margin-top:clamp(22px,2.6vw,32px); color:var(--linen); }
/* two answers, stacked, the page's own hairline buttons. Long labels wrap, so
   the roll-up's fixed-height window is released here and the duplicate glyph
   is not printed; the wipe still fires. */
.q-actions{ margin-top:clamp(24px,3vw,36px); display:flex; flex-direction:column; gap:10px; align-items:stretch; }
.q-actions .btn{ width:100%; justify-content:flex-start; text-align:left; padding:16px 22px; white-space:normal; }
.q-actions .roll{ height:auto; line-height:1.5; }
.q-actions .roll > i{ line-height:1.5; }
.q-actions .roll > i::after{ content:none; }
.q-actions .btn:hover .roll > i,.q-actions .btn:focus-visible .roll > i{ transform:none; }
.q-skip{ margin-top:18px; }
.q-skip a{ color:var(--l45); border-bottom:1px solid var(--l20); padding-bottom:2px;
  transition:color .2s ease, border-color .2s ease; }
.q-skip a:hover,.q-skip a:focus-visible{ color:var(--linen); border-color:var(--gold); }
.q-bar{ height:1px; background:var(--l12); margin-top:clamp(24px,3vw,36px); overflow:hidden; }
.q-bar i{ display:block; height:100%; background:var(--gold); transform-origin:left center;
  transform:scaleX(0); transition:transform .6s var(--ease); }
/* the verdict: a filled gold square for correct, a hollow one for not quite */
.q-verdict{ margin-top:clamp(22px,2.6vw,32px); display:inline-flex; align-items:center; gap:12px; color:var(--linen); }
.q-verdict::before{ content:''; flex:none; width:7px; height:7px; background:var(--gold); }
.q-verdict.q-bad::before{ background:transparent; border:1px solid var(--l45); }
.q-lesson{ margin:16px auto 0; max-width:46ch; color:var(--l72); font-size:15px; line-height:1.75; }
.q-std{
  margin:22px auto 0; max-width:46ch; text-align:left;
  padding-left:16px; border-left:1px solid var(--gold);
  color:var(--l90); font-size:14.5px; line-height:1.7;
}
.q-std .lbl{ display:block; margin-bottom:4px; }
.q-next{ margin-top:clamp(24px,3vw,36px); }
.q-score{
  margin-top:clamp(22px,2.6vw,32px);
  display:flex; justify-content:center; align-items:baseline; gap:12px;
  font-family:Aboreto, Georgia, serif; font-weight:400; color:var(--gold);
  font-size:clamp(44px,5.5vw,80px); line-height:1; letter-spacing:-.03em;
}
.q-score .lbl{ color:var(--l45); }

/* The statement band's rules are gone with the section. The V3 spine offers
   "Anatomy OR statement band" and the port shipped both; the promise it carried
   is the close's eyebrow. The handover strip and the CTA band are gone as two
   sections too: both live inside the close below. */

/* ====================================================== 11 STEP INTO OROVELL
   Daylight's close, blueprint section 10: a diptych. Static first: the media
   panel (the Lodha still with the handover film in a static window and one
   static hairline box) above the copy panel, which is what phones, reduced
   motion, a blocked CDN and JavaScript-off all get. The two-column diptych
   and the scrubbed window are armed further down under the hero's three
   locks. Wine: the page's large quiet surface (deviation 2), between the
   linen questions and the obsidian footer. No sticky here, no runway: the
   scrub is keyed to the section's own passage through the viewport. */
.close{ position:relative; isolation:isolate; background:var(--wine); color:var(--l90); }
.close-grid{ display:grid; grid-template-columns:minmax(0,1fr); overflow:clip; }
/* the copy panel. Clears the fixed nav at the top when the panel holds the
   fold; the stack is centred inside whatever height the grid takes. */
.close-copy{
  order:2;
  display:flex; flex-direction:column; justify-content:center;
  gap:clamp(36px,5svh,56px);
  padding:clamp(48px,7svh,80px) var(--gut) clamp(48px,7svh,72px);
}
/* its own ramp inside .dh: "STEP INTO" has to sit on one line in half the
   shell at 1024, and both lines with the sub block inside a 700px fold. Type
   ruling 2026-09-16: 64px at the top of the ramp, not 80. */
.close-h2{ font-size:clamp(30px, min(4.3vw, 7svh), 64px); }
.close-sub{ display:grid; grid-template-columns:minmax(0,1fr); gap:clamp(24px,3svh,36px); align-items:start; }
.close-say{ display:flex; flex-direction:column; align-items:flex-start; }
/* the plate caps sit on wine here, not obsidian: --l45 lands under AA against
   the wine, so every label in the close takes --l72 */
.close .lbl{ color:var(--l72); }
.cta-eyebrow{ display:block; }
/* the CTA's old h2 as a prose-rank display line (blueprint: "becomes a prose
   slot"); .ds's own ramp, letter-spacing 0 */
/* one line at desktop (client ruling 2026-09-16, the sub-heading rule): the
   nowrap is set with the other sub lines above; 22.6 ems of Aboreto at the
   .ds ramp is 384px at 1024 against a 426px column, 488px at 1440 against 608 */
.close-q{ margin-top:14px; }
.close-say .prose{ margin-top:18px; max-width:46ch; }
/* the booking pair: rectangles at 5px, gold fill for the primary path, a 1px
   linen-30 hairline button for the secondary. Contract rule 11. */
.cta-actions{ margin-top:clamp(24px,3vw,32px); display:flex; gap:12px; flex-wrap:wrap; }
/* the caps line under the buttons: the hero's trust-line grammar, plate caps
   on 3px gold squares (no circles on this site) */
.close-caps{
  display:flex; flex-wrap:wrap; align-items:center; gap:10px 14px;
  margin-top:clamp(18px,2.2vw,24px);
}
.close-caps i{ display:block; width:3px; height:3px; background:var(--gold); flex:none; }
.close-caps a{ position:relative; }
.close-caps a::after{ content:''; position:absolute; left:0; right:0; bottom:-3px; height:1px;
  background:var(--gold); transform:scaleX(0); transform-origin:left; transition:transform .25s var(--ease); }
.close-caps a:hover::after,.close-caps a:focus-visible::after{ transform:scaleX(1); }

/* the dossier form, #capForm, unchanged in behaviour (vc.js module 7) */
/* 44px, not 42: this is the primary control of the form and the only way to
   submit it on touch, and 42 is under the WCAG 2.2 SC 2.5.8 target minimum.
   A hairline square at rest rather than a second solid gold plate: "Let's
   find out" is the single gold surface in the panel and gold arrives here
   only on hover and focus, as a signal. */
/* Deviation clause 2: gold is never a body or label text colour. The signal is
   a SURFACE, a gold rule under the confirmation and a small gold dot before the
   error, which also makes success and failure distinguishable at a glance. */
/* the label pointed at an input that is now display:none, so it was left
   rendered above the confirmation as a dead click target */

/* the media panel: a 475px band in the static form, a full-height panel on
   the desktop path. Every layer inside is a grid cell of one grid, so nothing
   ever re-heights. */
.close-media{ order:1; position:relative; isolation:isolate; overflow:clip; height:475px; background:rgba(5,5,5,.3); }
.close-still{ position:absolute; inset:0; z-index:0; overflow:clip; }
/* the parallax handle: 200px taller than the panel, anchored 100px above it,
   so the +-100px of travel the scrub gives it never uncovers an edge. Biased
   above centre: these masters are 3:2 into a portrait panel, and the quarter
   this drops is the bottom one, where the wordmark is burned in. The frame
   is the W by DSR foyer (the mirrors and the console sit right of centre,
   so the crop leans that way): the home you step into. */
.close-still img{
  position:absolute; left:0; top:-100px; width:100%; height:calc(100% + 200px);
  object-fit:cover; object-position:56% 30%;
}
/* a bottom wash under the caption, obsidian alpha only, and a light lift at
   the top so the window's hairline reads against a bright frame */
.close-still::after{
  content:''; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(rgba(5,5,5,.16), rgba(5,5,5,0) 28%, rgba(5,5,5,0) 62%, rgba(5,5,5,.58));
}
.close-stack{ position:absolute; inset:0; z-index:1; display:grid; grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(0,1fr); }
/* the window. clip-path only: the box never changes size, the visible region
   does. Static inset first (the mobile, reduced and no-GSAP window); on the
   desktop scrub vc.js writes the inset in px from the panel's own measurements
   so the hairline, computed from the same numbers, lands exactly on the edge. */
.close-win{ grid-area:1 / 1; position:relative; z-index:2; clip-path:inset(8.42% 10.6% 72.2% 41.3%); }
.close-win > video,.close-win > img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; object-position:50% 40%; }
/* the gold hairline. Static form first: one box on the static window's own
   edges at gold .28, which is what phones, reduced motion and the no-GSAP path
   all get. The four spans inside are only rendered on the desktop scrub, where
   vc.js translates them onto the window's edges. Gold as a signal, rule 9. */
.close-hair{ grid-area:1 / 1; position:relative; z-index:3; pointer-events:none; }
.close-hair::after{
  content:''; position:absolute; inset:8.42% 10.6% 72.2% 41.3%;
  border:1px solid rgba(224,184,126,.28);
}
.close-hair span{ display:none; }
/* the strip's caption slot, now the window's caption */
.close .strip-cap{ position:absolute; left:var(--gut); right:var(--gut); bottom:18px; z-index:4; color:var(--l72); }

/* ---- the diptych and the scrubbed window, desktop only, under the hero's
   three locks (1024px up, motion allowed, GSAP proven). Two equal columns at
   least a viewport tall; the media panel fills its column; the window opens
   at the top right of the panel and the four spans are rendered for vc.js to
   move. Only transform and clip-path move here; will-change is applied by
   vc.js inside the matchMedia enter and removed in its cleanup. */
@media (min-width:1024px) and (prefers-reduced-motion:no-preference){
  html.js-on:not(.no-gsap) .close-grid{ grid-template-columns:repeat(2, minmax(0,1fr)); min-height:100svh; }
  html.js-on:not(.no-gsap) .close-copy{
    order:1; padding-top:clamp(80px,11svh,110px);
    /* the diptych's tension: the h2 sits at the top under the nav clearance
       and the sub-grid on the panel's bottom padding; the stacked form below
       1024 keeps its centred stack */
    justify-content:space-between; gap:0;
  }
  html.js-on:not(.no-gsap) .close-media{ order:2; height:auto; min-height:100svh; }
  html.js-on:not(.no-gsap) .close-win{ clip-path:inset(7.5% 9.57% 65.8% 42.67%); }
  html.js-on:not(.no-gsap) .close-hair::after{ display:none; }
  html.js-on:not(.no-gsap) .close-hair span{
    display:block; position:absolute; background:var(--gold); opacity:.28;
  }
  /* each span starts 1px OUTSIDE the panel on its own side, full length, and
     is translated in by the inset plus that 1px, then scaled along its length
     to the window's edge: the hero's own hairline, a second instance */
  html.js-on:not(.no-gsap) .close-hair span:nth-child(1){ top:-1px; left:0; width:100%; height:1px; transform-origin:left center; }
  html.js-on:not(.no-gsap) .close-hair span:nth-child(2){ bottom:-1px; left:0; width:100%; height:1px; transform-origin:left center; }
  html.js-on:not(.no-gsap) .close-hair span:nth-child(3){ left:-1px; top:0; width:1px; height:100%; transform-origin:center top; }
  html.js-on:not(.no-gsap) .close-hair span:nth-child(4){ right:-1px; top:0; width:1px; height:100%; transform-origin:center top; }
}
/* the sub block used to go two-up (copy, then the form beside it) from
   1360px. It stays one column at every width now: the question is one line
   at desktop (client ruling 2026-09-16) and needs the whole column, and the
   two-up's 311px copy cell could not carry it. The form sits under the caps
   line everywhere. */

/* ---- the footer (.foot and every .foot-*) lives in css/chrome.css, loaded
   after this sheet: the home renders the same footer as every inner page, from
   the same rules. Nothing here overrides it. */


/* ============================================================== RESPONSIVE */
@media (max-width:1023px){
  /* motion B: every sticky/scrub stage is replaced by a plain stacked layout.
     The craft run and the three-step are gated on min-width:1024px and the
     static form is their default; these are the mirror, so the phone stays
     static even if that gate is ever loosened. */
  .run-cell{ clip-path:none; }
  .run-copy{ position:static; transform:none !important; opacity:1 !important; visibility:visible !important; }
  .run::before,.run::after{ display:none; }
  /* client 2026-09-16: the craft frames fill the column on phones and tablets
     (4:5, like the step tiles), not a 60vw sliver */
  .run-media{ width:100%; max-width:440px; aspect-ratio:4 / 5; }
  .hiw{ clip-path:none; }
  .hiw-intro{ position:static; height:auto; min-height:70svh; }
  .hiw-curtain{ display:none; }
  .hiw-item{ position:static; height:auto; transform:none !important; opacity:1 !important; visibility:visible !important; }
  .hiw-card{ height:auto; }
  /* client 2026-09-16: the step still fills the card's width on phones and
     tablets (a 4:5 crop of the portrait still, not a 60vw sliver) */
  .hiw-tile{ clip-path:none !important; width:100%; max-width:440px; aspect-ratio:4 / 5; max-height:none; }
  /* the step's line wraps on narrow cards (the desktop one-line rule clipped it) */
  .hiw-copy .prose{ white-space:normal; }

  /* the anatomy chapters: the sticky stacking is gated on min-width:1024px
     and the static bands are the default; the mirror, plus the clears for
     whatever inline state the parallax and the line rise had written before
     the viewport narrowed. */
  .chap{ position:static; height:auto; min-height:70svh; }
  .chap-par{ transform:none !important; }
  .chap-title .hl > span{ transform:none !important; opacity:1 !important; visibility:visible !important; }

  .stories-outer{ height:auto; }
  .stories-inner{ position:static; height:auto; min-height:0; overflow:visible;
    padding:var(--pad) var(--gut); }
  .stories-h{ position:static; display:block; }
  .stories-h h2{ max-width:none; }
  .rev{ position:static; width:100%; margin-top:clamp(18px,3vw,26px); transform:none !important; }

  .work-outer{ height:auto; }
  .work-inner{ position:static; height:auto; min-height:0; overflow:visible; padding-block:var(--pad); }
  .work-grid{ grid-template-columns:minmax(0,1fr); align-items:stretch; }
  .pc{ transform:none !important; }
  .pc1 .pc-fig,.pc2 .pc-fig,.pc3 .pc-fig{ height:clamp(220px,58vw,380px); }

  /* the close needs nothing for its layout: the diptych and the scrubbed
     window are gated on min-width:1024px and the stacked form is the default.
     These clear whatever inline state the scrub had written before the
     viewport narrowed. */
  .close-grid{ grid-template-columns:minmax(0,1fr); min-height:0; }
  .close-copy{ order:2; }
  .close-media{ order:1; height:475px; min-height:0; }
  .close-win{ clip-path:inset(8.42% 10.6% 72.2% 41.3%) !important; }
  .close-hair::after{ display:block; }
  .close-hair span{ display:none; }
  .close-still img{ transform:none !important; }
  .close-h2 .hl > span{ transform:none !important; opacity:1 !important; visibility:visible !important; }
}

@media (max-width:900px){
  .qa-grid{ grid-template-columns:minmax(0,1fr); }
  .hero-rails{ display:none; }
  /* the copy column is full width here, so a left-to-right scrim would leave
     the end of every line on a bright wall. The grade turns bottom-up. */
  .hero-scrim{
    /* client 2026-09-16: the copy sits mid-frame on a bright foyer wall, so
       the middle band is graded to .7 and the whole frame carries a base
       wash; the room still reads at the top */
    background:
      linear-gradient(rgba(5,5,5,.46) 0%, rgba(5,5,5,.58) 22%,
        rgba(5,5,5,.72) 48%, rgba(5,5,5,.82) 70%, rgba(5,5,5,.92) 100%),
      radial-gradient(128% 96% at 50% 46%, rgba(5,5,5,.08) 42%, rgba(5,5,5,.42) 100%);
  }
  .hero-col{ max-width:none; }
  /* client 2026-09-16: no gold frame around the phone hero */
  .hero-hair{ display:none; }
}

@media (max-width:620px){
  .stories-bg{ background-image:linear-gradient(rgba(5,5,5,.82), rgba(5,5,5,.9)), url("../assets/projects/w-by-dsr/00-s.webp"); }
  .lbl{ font-size:10px; }
  .ba-slider{ aspect-ratio:4/3; }
  .close-media{ height:clamp(300px,90vw,475px); }
  .std-nums{ gap:28px; }
  .stat-fig{ width:100%; }
  .rev{ width:100%; }
}

/* -------------------------------------------------- compositor promotion */
/* will-change belongs where the tweens are, and every SCRUBBED tween on this
   page lives inside mm.add('(min-width:1024px)'). Declared unconditionally,
   these promoted a permanent layer per card on phones and under html.no-gsap,
   where nothing ever moves them. .grain keeps its own inline, because it animates
   at every width; the hero's four subjects deliberately do NOT get one, since a
   permanent full-bleed layer per subject is four screens of compositor memory to
   buy an opacity fade the compositor already handles. */
/* the stories cards and the work cards (transform), the craft run's copies
   (transform, opacity), the three-step's curtain (opacity), cards (transform,
   opacity) and tiles (clip-path), the anatomy's parallax handles (transform), the close's window
   (clip-path), hairline spans and still (transform) are all promoted by vc.js
   inside the matchMedia enter and cleared in its cleanup, none declared here */

/* both scroll paths land clear of the fixed nav */
#top,#standard,#work,#craft,#build,#handover,#anatomy,#reality,#test,#close{ scroll-margin-top:92px; }

/* -------------------------------------------------------- reduced motion */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important; animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
  }
  .rv{ opacity:1 !important; transform:none !important; }
  .grain{ display:none; }
  .proof-track{ animation:none; }
  /* the hero holds its opening subject and nothing drifts. vc.js does not start
     the cycle under reduce and it parks the film on its poster with the control
     showing "play", so the motion is opt-in rather than removed. */
  .hero-h1 .hl > span{ transform:none !important; }
  .hm{ transform:none !important; }
  .nav{ transform:none !important; }
  .stories-bg{ background-attachment:scroll; }
  /* the craft run and the three-step stay in their static form: the armed
     rules above are already gated on no-preference; these are the mirror */
  .run-cell{ clip-path:none; }
  .run-copy{ position:static; transform:none !important; opacity:1 !important; visibility:visible !important; }
  .run::before,.run::after{ display:none; }
  .hiw{ clip-path:none; }
  .hiw-intro{ position:static; height:auto; min-height:70svh; }
  .hiw-curtain{ display:none; }
  .hiw-item{ position:static; height:auto; transform:none !important; opacity:1 !important; visibility:visible !important; }
  .hiw-card{ height:auto; }
  .hiw-tile{ clip-path:none !important; }
  .hiw-h2 .hl > span{ transform:none !important; opacity:1 !important; visibility:visible !important; }
  .run-title .hl > span,
  .chap-title .hl > span,.stat-h2 .hl > span,.stories-h .hl > span,
  .close-h2 .hl > span{ transform:none !important; opacity:1 !important; visibility:visible !important; }
  /* the anatomy chapters stay three static bands: the sticky stacking is
     gated on no-preference; these are the mirror */
  .chap{ position:static; height:auto; min-height:70svh; }
  .chap-par{ transform:none !important; }
  /* the close stays stacked with a static window: the diptych and the scrub
     are gated on no-preference; these are the mirror */
  .close-grid{ grid-template-columns:minmax(0,1fr); min-height:0; }
  .close-copy{ order:2; }
  .close-media{ order:1; height:clamp(300px,90vw,475px); min-height:0; }
  .close-win{ clip-path:inset(8.42% 10.6% 72.2% 41.3%) !important; }
  .close-hair::after{ display:block; }
  .close-hair span{ display:none; }
  .close-still img{ transform:none !important; }
  .q-bar i{ transition:none; }
}

/* no GSAP (blocked CDN, offline, old browser): every stage falls back to a
   readable stacked layout, exactly like the sub-1024 path */
/* the hero keeps its frame and its dissolve, both of which are CSS. Only the
   masked line rise needs GSAP, and this is the second lock on it: the arming
   rule already excludes .no-gsap, and this catches the case where GSAP armed
   the lines with an inline transform before something later threw. vc.js also
   clears those inline styles by hand. */
html.no-gsap .hero-h1 .hl > span{ transform:none; }
html.no-gsap .nav{ transform:none !important; }
/* the hero's runway rules are gated on :not(.no-gsap) so they simply stop
   matching; these clear whatever inline state the scrub had written before a
   later throw re-armed the class. */
html.no-gsap .hero-clip{ clip-path:none !important; }
html.no-gsap .hero-zoom,html.no-gsap .hero-col,html.no-gsap .hero-sec,
html.no-gsap .hero-credit-w,html.no-gsap .hero-rails,html.no-gsap .hero-scrim{
  transform:none !important; opacity:1 !important; visibility:visible !important; }
/* the craft run. Its framed form is gated on :not(.no-gsap) so it simply
   stops matching; these clear whatever inline state the scrub had written
   before a later throw re-armed the class (autoAlpha writes visibility). */
html.no-gsap .run-cell{ clip-path:none; }
html.no-gsap .run-copy{ position:static; transform:none !important; opacity:1 !important;
  visibility:visible !important; }
html.no-gsap .run::before,html.no-gsap .run::after{ display:none; }
html.no-gsap .run-title .hl > span,
html.no-gsap .chap-title .hl > span,
html.no-gsap .stat-h2 .hl > span,
html.no-gsap .stories-h .hl > span,
html.no-gsap .close-h2 .hl > span{ transform:none !important; opacity:1 !important; visibility:visible !important; }
/* the three-step: its sticky intro, curtain and items are gated on
   :not(.no-gsap) so they simply stop matching; these clear whatever inline
   state the scrub had written before a later throw re-armed the class */
html.no-gsap .hiw-h2 .hl > span,
html.no-gsap .hiw-intro-copy{ transform:none !important; opacity:1 !important; visibility:visible !important; }
html.no-gsap .hiw{ clip-path:none; }
html.no-gsap .hiw-intro{ position:static; height:auto; min-height:70svh; }
html.no-gsap .hiw-curtain{ display:none; }
html.no-gsap .hiw-item{ position:static; height:auto; transform:none !important; opacity:1 !important;
  visibility:visible !important; }
html.no-gsap .hiw-card{ height:auto; }
html.no-gsap .hiw-tile{ clip-path:none !important; }
html.no-gsap .stories-outer,
html.no-gsap .work-outer{ height:auto; }
html.no-gsap .stories-inner,
html.no-gsap .work-inner{ position:static; height:auto; min-height:0; overflow:visible; }
/* the anatomy chapters: their sticky form is gated on :not(.no-gsap) so it
   simply stops matching; these clear whatever inline state the parallax had
   written before a later throw re-armed the class */
html.no-gsap .chap{ position:static; height:auto; min-height:70svh; }
html.no-gsap .chap-par{ transform:none !important; }
/* the close: its diptych and scrubbed window are gated on :not(.no-gsap) so
   they simply stop matching; these clear whatever inline state the scrub had
   written before a later throw re-armed the class (the clip in px, the
   hairline transforms, the still's parallax) */
html.no-gsap .close-grid{ grid-template-columns:minmax(0,1fr); min-height:0; }
html.no-gsap .close-copy{ order:2; }
html.no-gsap .close-media{ order:1; height:clamp(300px,90vw,475px); min-height:0; }
html.no-gsap .close-win{ clip-path:inset(8.42% 10.6% 72.2% 41.3%) !important; }
html.no-gsap .close-hair::after{ display:block; }
html.no-gsap .close-hair span{ display:none; }
html.no-gsap .close-still img{ transform:none !important; }
html.no-gsap .work-inner{ padding-block:var(--pad); }
html.no-gsap .stories-inner{ padding:var(--pad) var(--gut); }
html.no-gsap .stories-h{ position:static; display:block; margin-bottom:26px; }
html.no-gsap .rev{ position:static; width:100%; margin-top:22px; transform:none !important; }
html.no-gsap .pc{ transform:none !important; }
