/* ═══════════════════════════════════════════════════════════════════════════
   CADENCE 3 — COMPONENTS
   Depends on theme.css. Contains ZERO raw hex values.
   In the Next.js port this becomes @layer components in globals.css.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══ RESET ═══ */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
/* html carries the background too, not just body. The root element is what the
   browser propagates to the canvas — the area painted during navigation, over-
   scroll bounce, and between view-transition snapshots. */
/* Literal fallbacks are deliberate. If theme.css is ever slow, 404s, or is
   edited badly, `background:var(--x-void)` with --x-void undefined is invalid
   at computed-value time — the property resets to its INITIAL value, which is
   transparent, i.e. white. And because app.css loads after the inline <style>
   in the head, that failure would override the inline fallback too. A literal
   second argument makes the white flash impossible rather than unlikely. */
html{-webkit-text-size-adjust:100%;background:var(--x-void,#0A0E12);color-scheme:dark}
body{
  background:var(--x-void,#0A0E12);color:var(--t-mid);
  font-family:var(--f-body);font-weight:300;font-size:var(--fs-body);
  line-height:1.65;-webkit-font-smoothing:antialiased;overflow-x:hidden;
}
img,svg,video{display:block;max-width:100%}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
ul{list-style:none}
::selection{background:var(--accent);color:var(--x-void)}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

/* ═══ PAGE TRANSITIONS ═══════════════════════════════════════════════════════
   Two mechanisms, same visual result:
   1. Native View Transitions (Chrome/Edge) — cross-document, zero JS.
   2. JS fallback in app.js for Firefox/Safari — .is-leaving on <html>.
   The nav and footer are given view-transition-name so they persist across
   navigations instead of fading with the page.
   ═════════════════════════════════════════════════════════════════════════ */
@view-transition{navigation:auto}

/* THE BACKDROP.
   During a transition the real page is not painted — the root is captured into
   this pseudo tree and the ::view-transition overlay covers the viewport. Its
   default background is transparent, so whatever the browser paints beneath it
   shows through, and mid-crossfade (both snapshots ~50% opaque) that backdrop
   is visible. Painting it with the page colour is what removes the flash;
   html/body backgrounds do NOT cover this, because neither is being painted. */
::view-transition{background-color:var(--x-void,#0A0E12)}

/* Snapshots themselves get the page colour too, so a partially-transparent
   frame never reveals anything lighter behind it. */
::view-transition-old(root),
::view-transition-new(root){background-color:var(--x-void,#0A0E12)}

::view-transition-old(root){
  animation:vt-out var(--dur-page) var(--ease) both;
}
::view-transition-new(root){
  animation:vt-in calc(var(--dur-page) * 1.35) var(--ease) both;
}
@keyframes vt-out{
  from{opacity:1;transform:translateY(0) scale(1)}
  to  {opacity:0;transform:translateY(-14px) scale(.994)}
}
@keyframes vt-in{
  from{opacity:0;transform:translateY(20px) scale(1.006)}
  to  {opacity:1;transform:none}
}
/* persistent chrome — these don't animate with the page body */
.nav{view-transition-name:site-nav}
.foot{view-transition-name:site-foot}
::view-transition-group(site-nav),
::view-transition-group(site-foot){animation-duration:var(--dur-page)}

/* THE WHITE FLASH, second time around.
   View-transition snapshots default to mix-blend-mode:plus-lighter — the old
   and new frames are ADDED, not layered. When both frames are identical that
   sums back to the original exactly, which is the whole point and why this was
   invisible in phase 2: the nav was the same on every page. Phase 3 added Home
   and a current-page state, so the two nav snapshots now DIFFER, and adding two
   differing bright layers blows out to white.
   The root has opaque backgrounds, so plus-lighter lifts it too — dark + dark
   is lighter than dark. Normal blending on all four. */
::view-transition-old(root),      ::view-transition-new(root),
::view-transition-old(site-nav),  ::view-transition-new(site-nav),
::view-transition-old(site-foot), ::view-transition-new(site-foot){
  mix-blend-mode:normal;
}

/* JS fallback for browsers without the API */
html.no-vt main{transition:opacity var(--dur-page) var(--ease),transform var(--dur-page) var(--ease)}
html.no-vt.is-leaving main{opacity:0;transform:translateY(-14px)}
html.no-vt.is-entering main{animation:vt-in calc(var(--dur-page)*1.35) var(--ease) both}

/* wipe curtain — a thin accent sweep that reads as "loading" without a spinner */
.curtain{
  position:fixed;inset:0 0 auto;height:2px;z-index:200;
  background:var(--accent);transform:scaleX(0);transform-origin:left;
  transition:transform 420ms var(--ease),opacity 260ms var(--ease);pointer-events:none;
}
.curtain.go{transform:scaleX(1)}
.curtain.done{opacity:0;transform-origin:right;transform:scaleX(0)}

/* ═══ PRIMITIVES ═══ */
.wrap{max-width:var(--maxw);margin-inline:auto;padding-inline:var(--gutter)}
.section{padding-block:var(--section-y);position:relative}
.section--tight{padding-block:calc(var(--section-y) * .72)}
/* Was an inline style="background:var(--x-base)". Inline styles beat class
   rules, so .light could never repaint those sections — they stayed dark no
   matter what the theme said. As a class it is overridable; .light is declared
   later in this file and therefore wins. */
.section--alt{
  background:linear-gradient(180deg,
    var(--x-base) 0%,
    color-mix(in srgb, var(--x-void) 50%, var(--x-base)) 100%);
}

.eyebrow{
  font-family:var(--f-body);font-weight:500;font-size:var(--fs-eyebrow);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;
  color:var(--t-low);display:flex;align-items:center;gap:.85rem;
}
.eyebrow::before{content:attr(data-num);color:var(--accent);font-variant-numeric:tabular-nums}
.eyebrow::after{content:"";flex:1;height:1px;background:var(--x-line);max-width:180px}
.eyebrow--c{justify-content:center}
.eyebrow--c::after{display:none}

h1,h2,h3,h4{
  font-family:var(--f-display);font-weight:500;color:var(--t-hi);
  letter-spacing:var(--tracking-tight);line-height:1.03;text-wrap:balance;
}
h1{font-size:var(--fs-h1)}
h2{font-size:var(--fs-h2)}
h3{font-size:var(--fs-h3);line-height:1.15;letter-spacing:-0.02em}
.lead{font-size:var(--fs-lead);line-height:1.55;color:var(--t-mid);max-width:52ch;font-weight:300}
.prose p{max-width:64ch;margin-bottom:1.4rem}
.prose p:last-child{margin-bottom:0}

.btn{
  display:inline-flex;align-items:center;gap:.7rem;
  padding:1rem 1.9rem;border:1px solid var(--x-line);border-radius:var(--radius);
  font-family:var(--f-body);font-weight:500;font-size:.875rem;letter-spacing:.01em;
  color:var(--t-hi);position:relative;overflow:hidden;isolation:isolate;
  transition:color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease);
}
.btn::before{
  content:"";position:absolute;inset:0;z-index:-1;background:var(--accent);
  transform:translateY(101%);transition:transform var(--dur-fast) var(--ease);
}
/* P02 · One rule, both button types: blue background takes white text,
   white background takes black text.
   Secondary hovers to a BLUE plate (.btn::before is --accent), so its label
   stays white. Primary hovers to a WHITE plate (.btn--solid::before is --t-hi),
   so its label goes black — see .btn--solid:hover below. */
.btn:hover{color:var(--t-on-accent);border-color:var(--accent)}
.btn:hover::before{transform:translateY(0)}
/* P02 · Primary button: blue fill, white label and icon at rest.
   Hover is unchanged from before — the white plate slides up and the label goes
   black, via the existing .btn:hover rule. Only the resting colour moved. */
.btn--solid{background:var(--accent);border-color:var(--accent);color:var(--t-on-accent)}
.btn--solid::before{background:var(--t-hi)}
.btn--solid:hover{color:var(--x-void)}
.btn .arw{transition:transform var(--dur-fast) var(--ease)}
.btn:hover .arw{transform:translateX(4px)}
.btn--light{border-color:var(--x-light-line);color:var(--t-on-light-hi)}
.btn--light:hover{color:var(--x-light)}

/* ═══ REVEALS ═══════════════════════════════════════════════════════════════
   Gated on html.js, which an inline script in <head> sets before first paint.
   If JS is disabled, blocked, or throws, .rv never hides and the page reads as
   a plain static document instead of a blank one. Never hide content behind an
   animation that has no failure path.
   ═════════════════════════════════════════════════════════════════════════ */
.js .rv{opacity:0;transform:translateY(28px);
    transition:opacity var(--dur) var(--ease),transform var(--dur) var(--ease)}
.js .rv.in{opacity:1;transform:none}
.rv[data-d="1"]{transition-delay:.09s}
.rv[data-d="2"]{transition-delay:.18s}
.rv[data-d="3"]{transition-delay:.27s}
.rv[data-d="4"]{transition-delay:.36s}

.mask{display:block;overflow:hidden}
.js .mask>span{display:block;transform:translateY(110%);transition:transform 1250ms var(--ease)}
.js .mask.in>span{transform:none}
.mask:nth-of-type(2)>span{transition-delay:.1s}
.mask:nth-of-type(3)>span{transition-delay:.2s}

@media (prefers-reduced-motion:reduce){
  *{animation-duration:.01ms!important;transition-duration:.01ms!important}
  .rv{opacity:1;transform:none}.mask>span{transform:none}
  @view-transition{navigation:none}
}

/* ═══ NAV ═══ */
/* Transparent while it sits over the hero/page banner so the image reads as
   full-bleed; picks up the page colour the moment the banner scrolls past.
   `.over-banner` is set by app.js from the banner's measured height, not from a
   fixed scroll offset — banners differ in height between home and inner pages. */
.nav{
  position:fixed;inset:0 0 auto;z-index:100;padding-block:.7rem;
  background:color-mix(in srgb,var(--x-void,#0A0E12) 94%,transparent);
  backdrop-filter:blur(18px) saturate(140%);
  border-bottom:1px solid var(--x-line-soft);
  transition:transform 480ms var(--ease),background var(--dur-fast) var(--ease),
             border-color var(--dur-fast) var(--ease),backdrop-filter var(--dur-fast) var(--ease);
  will-change:transform;
}
/* over the banner: no plate at all. The banner's own top scrim keeps the logo
   and links legible — see .hero__grade / .phead::before. */
/* Height is FIXED in every state. Only background, border and blur change, so
   the bar never resizes under the pointer and the scroll threshold below stays
   a stable multiple of a constant height. */
.nav.over-banner{
  background:transparent;
  border-bottom-color:transparent;
  backdrop-filter:none;
}
/* off-canvas upward; translate only, so it never triggers layout */
.nav.hide{transform:translateY(-101%)}
.nav:focus-within{transform:none}
.nav__in{display:flex;align-items:center;justify-content:space-between;gap:2rem}
.logo{width:clamp(102px,8.4vw,128px);flex:none}
.logo svg{width:100%;height:auto;fill:var(--t-hi)}
.nav__links{display:flex;gap:clamp(1.2rem,2.4vw,2.4rem);align-items:center}
.nav__links a{
  font-size:.8125rem;font-weight:500;color:var(--t-mid);position:relative;
  padding-block:.3rem;transition:color var(--dur-fast) var(--ease);
}
.nav__links a::after{
  content:"";position:absolute;left:0;bottom:0;height:1px;width:100%;background:var(--accent);
  transform:scaleX(0);transform-origin:right;transition:transform var(--dur-fast) var(--ease);
}
.nav__links a:hover{color:var(--t-hi)}
.nav__links a:hover::after{transform:scaleX(1);transform-origin:left}
.nav__links a[aria-current="page"]{color:var(--t-hi)}
.nav__links a[aria-current="page"]::after{transform:scaleX(1);transform-origin:left}
.nav__links a.btn::after{display:none}
.nav__cta{padding:.65rem 1.25rem!important;margin-left:.4rem}

.burger{display:none;width:26px;height:14px;position:relative;z-index:110}
.burger i{position:absolute;left:0;right:0;height:1.5px;background:var(--t-hi);
          transition:transform .4s var(--ease),opacity .3s var(--ease)}
.burger i:first-child{top:0}.burger i:last-child{bottom:0}
.burger[aria-expanded="true"] i:first-child{transform:translateY(6px) rotate(45deg)}
.burger[aria-expanded="true"] i:last-child{transform:translateY(-6px) rotate(-45deg)}

.drawer{
  position:fixed;inset:0;z-index:99;background:var(--x-void);
  display:flex;flex-direction:column;justify-content:center;gap:.4rem;
  padding:var(--gutter);opacity:0;visibility:hidden;
  transition:opacity var(--dur-fast) var(--ease),visibility var(--dur-fast);
}
.drawer.open{opacity:1;visibility:visible}
.drawer a{
  font-family:var(--f-display);font-weight:500;color:var(--t-hi);
  font-size:clamp(1.8rem,7vw,3rem);letter-spacing:-0.03em;
  padding-block:.4rem;border-bottom:1px solid var(--x-line-soft);
}
@media(max-width:960px){
  .nav__links{display:none}
  .burger{display:block}
  /* Taller bar on mobile. Desktop height is set by the "Let's talk" button
     (~34px); with the links hidden, only the ~20px logo is left, so the bar
     would otherwise be SHORTER on mobile than on desktop. min-height on the
     inner row restores presence, and align-items:center keeps the logo and
     burger optically centred within it. */
  .nav{padding-block:1.1rem}
  .nav__in{min-height:2.5rem;align-items:center}
}
@media(min-width:961px){.drawer{display:none}}

/* ═══ HERO — home ═══ */
/* Bottom-aligned via margin-top:auto rather than align-items:flex-end, so that
   when the content is taller than the viewport the hero GROWS instead of
   overflowing upward under the fixed nav. padding-top reserves the nav band. */
.hero{
  min-height:100svh;position:relative;display:flex;flex-direction:column;
  padding-top:clamp(7rem,14vh,10rem);   /* clears the transparent nav */
  padding-bottom:clamp(2.5rem,6vh,5.5rem);overflow:hidden;isolation:isolate;
}
.hero__in{margin-top:auto}
/* Positive layer stack. Negative z-index inside an isolated stacking context
   is unreliable once the element also has overflow/filters in play — layer
   upward from 0 instead, and give the content the top index explicitly. */
.hero__media{position:absolute;inset:-8% 0 0;z-index:0;will-change:transform}
/* Cinematic grade applied in CSS, not baked into the file, so it retunes with
   the tokens and the same source can be reused ungraded elsewhere.
   brightness pulls the photo under the type; saturate keeps it from competing
   with Sky Blue; the slight contrast bump stops it going flat and muddy. */
.hero__media img{
  width:100%;height:112%;object-fit:cover;object-position:center 45%;
  /* This frame is already dark on the left (mean luminance ~26%), so it needs a
     far lighter hand than the old library shot did. Crushing it further just
     turns the photograph into a black rectangle. */
  filter:brightness(.94) contrast(1.04) saturate(.92);
}
.hero__grade{
  position:absolute;inset:0;z-index:1;
  /* Four layers, in paint order:
     1. vertical scrim  — seats the hero into the page top and bottom
     2. horizontal scrim — the headline is left-aligned, so the left third
        needs to be reliably dark regardless of what the photo is doing there
     3. sapphire glow bottom-left, 4. golden-hour warmth top-right (brand p.18) */
  /* Paint order, top layer first:
     1. vignette   — pulls all four corners down, seats the frame
     2. text panel — a soft horizontal wedge under the left column only
     3. vertical   — blends into the nav above and the next section below
     4/5. sapphire glow bottom-left, golden-hour warmth top-right (brand p.18) */
  background:
    radial-gradient(122% 108% at 50% 44%,
      transparent 36%,
      color-mix(in srgb,var(--x-void) 22%,transparent) 72%,
      color-mix(in srgb,var(--x-void) 58%,transparent) 100%),
    linear-gradient(90deg,
      color-mix(in srgb,var(--x-void) 74%,transparent) 0%,
      color-mix(in srgb,var(--x-void) 56%,transparent) 28%,
      color-mix(in srgb,var(--x-void) 26%,transparent) 52%,
      color-mix(in srgb,var(--x-void) 6%,transparent) 74%,
      transparent 88%),
    linear-gradient(180deg,
      color-mix(in srgb,var(--x-void) 82%,transparent) 0%,
      color-mix(in srgb,var(--x-void) 30%,transparent) 26%,
      color-mix(in srgb,var(--x-void) 46%,transparent) 60%,
      var(--x-void) 99%),
    radial-gradient(90% 68% at 12% 88%, color-mix(in srgb,var(--accent-deep) 30%,transparent), transparent 68%),
    radial-gradient(64% 50% at 88% 14%, color-mix(in srgb,var(--accent-warm) 12%,transparent), transparent 70%);
}
.grain{
  position:absolute;inset:0;z-index:2;opacity:.22;pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}
.hero__in{width:100%;position:relative;z-index:3}
/* max-width must clear the longest line ("Few create momentum.") or it wraps to
   a 4th line and the whole stack stops fitting in 100svh. Each .mask is its own
   block, so this only guards against accidental wrapping. */
.hero h1{font-size:var(--fs-hero);letter-spacing:-0.04em;line-height:.96;max-width:22ch;margin-top:1.4rem}
/* Light Blue, not Sky Blue. Over a photograph #4E9AD4 sits at ~6.5:1 against
   the canvas while #89C3FF reaches ~10:1 — and Light Blue is an official brand
   extension colour, so this is still on-system. Sky Blue stays the accent
   everywhere it appears on a flat background. */
.hero h1 em{font-style:normal;color:var(--color-lightblue)}

/* Hero copy carries more weight than body copy elsewhere — it has to survive
   sitting on an image rather than a flat panel. */
/* 48ch keeps this to four lines at desktop. Narrower and it wraps to five,
   which pushes the whole bottom-aligned stack past 100svh. */
.hero__sub{margin-top:1.6rem;max-width:48ch;color:#C6D1DC;font-weight:400;
  text-shadow:0 1px 24px color-mix(in srgb,var(--x-void) 85%,transparent)}
.hero .eyebrow{color:#93A2B0}
.hero h1{text-shadow:0 2px 40px color-mix(in srgb,var(--x-void) 60%,transparent)}
/* the ghost button needs a real edge against photography */
.hero__actions .btn:not(.btn--solid){
  border-color:color-mix(in srgb,var(--t-hi) 42%,transparent);
  background:color-mix(in srgb,var(--x-void) 30%,transparent);
  backdrop-filter:blur(6px);
}
.hero__actions{display:flex;flex-wrap:wrap;gap:.85rem;margin-top:2.2rem}
.hero__foot{
  /* Only the scroll cue lives here now. space-between would push a lone child
     to the LEFT; flex-end keeps it on the right where it was. */
  display:flex;justify-content:flex-end;align-items:flex-end;gap:2rem;
  margin-top:clamp(2rem,5vh,3.5rem);padding-top:1.4rem;border-top:1px solid var(--x-line);
  font-size:.75rem;letter-spacing:.09em;text-transform:uppercase;color:var(--t-low);
}
/* P01 · The rule under the hero buttons doubles as a countdown for the photo
   rotation. This paints an accent line directly over the existing 1px
   border-top and fills it left-to-right across one interval.

   --hero-dur is published by app.js from the SAME value that drives the
   rotation, and the animation is restarted by the rotation callback itself —
   so there is no second clock that can drift out of step with the photos.
   scaleX on a 1px line is compositor-only: no layout, no paint. */
.hero__foot{position:relative}
.hero__foot::before{
  content:"";position:absolute;top:-1px;left:0;width:100%;height:1px;
  background:var(--accent);transform:scaleX(0);transform-origin:left center;
  pointer-events:none;
}
.hero__foot.is-timing::before{animation:hero-fill var(--hero-dur,4500ms) linear both}
.hero__foot.is-held::before{animation-play-state:paused}
@keyframes hero-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}
/* Reduced motion: the photos do not rotate, so there is nothing to count down
   to. Leave the plain rule exactly as it was. */
@media (prefers-reduced-motion:reduce){ .hero__foot::before{display:none} }

.scrollcue{display:flex;align-items:center;gap:.7rem}
.scrollcue .rail{display:block;width:1px;height:38px;background:var(--x-line);position:relative;overflow:hidden}
.scrollcue .rail::after{content:"";position:absolute;inset:0;background:var(--accent);
  animation:cue 2.4s var(--ease-io2) infinite}
@keyframes cue{0%{transform:translateY(-100%)}55%,100%{transform:translateY(100%)}}

/* ═══ PAGE HEAD — inner pages ═══ */
.phead{
  position:relative;padding-top:clamp(9rem,20vh,15rem);padding-bottom:clamp(4rem,8.5vh,7rem);
  overflow:hidden;isolation:isolate;
}
.phead::before{
  content:"";position:absolute;inset:0;z-index:0;
  background:radial-gradient(70% 90% at 8% 0%, color-mix(in srgb,var(--accent-deep) 26%,transparent), transparent 62%);
}
.phead>.wrap{position:relative;z-index:3}
.phead h1{margin-top:var(--stack)}   /* measure: see the P07 block */
.phead .lead{margin-top:var(--stack)}
.phead__media{position:relative;margin-top:var(--block);overflow:hidden;
  border-radius:var(--radius);aspect-ratio:21/9}
.phead__media img{width:100%;height:100%;object-fit:cover}

/* section intro: headline left, supporting paragraph right */
/* Section head. STACKED by default — heading, then the paragraph beneath it.
   The two-column split (heading left, lead right) is now opt-in via
   .intro--split and is used only on the video grids, where the paragraph is
   short and the grid below is wide enough to carry the asymmetry.
   Changing the default rather than editing twelve call sites means a new
   section inherits the right behaviour without anyone remembering to. */
.intro{display:grid;gap:1rem;margin-bottom:var(--block)}
.intro .lead{max-width:62ch}
@media(min-width:1000px){
  .intro--split{grid-template-columns:1.15fr .85fr;align-items:end;gap:4rem}
  .intro--split .lead{max-width:none}
}

/* ═══ HEADING & SUBHEAD MEASURE — P07 ════════════════════════════════════════
   Headings 50% of the content width on desktop, up to 100% on mobile.
   Subheads 75% / 90%. Every heading at most two lines.

   Why a percentage holds the two-line rule, when it looks like it should not:
   the type scale and the container BOTH scale with vw, so they cancel. 50% of
   .wrap is ~26.8 characters per line at 1024, 1280, 1440 and 1560 alike — a
   percentage behaves here exactly like a fixed ~26ch. The longest heading on
   the site is 51 characters, which lands at 1.9 lines. Nothing reaches three.

   This replaces 21 per-heading `max-width:Nch` values that had been tuned by
   hand. They are redundant rather than lost: 50% is the wider constraint in
   every one of the 21 cases, so headings that used to break early now fill the
   measure and short ones sit on a single line — confirmed as fine.

   Deliberately NOT covered: the homepage hero (.hero h1 keeps its 22ch), the
   CTA above the footer, the nav and the footer. .cta .lead and .hero are not
   matched by any selector here — check before widening them.

   Below 860px everything goes full measure; line counts vary with the viewport
   there, which was accepted when this was specified.

   Measured at 360/390/768/1024/1280/1440 in Chrome, real webfonts, each width
   counting its own lines: every section heading on every page that SHIPS fits
   two lines at desktop widths. The longest is 51 characters ("Want to make a
   video but don't know where to start?") and it lands on two.

   One heading does need three — "The follow-up meeting stopped being the hard
   part." — but it lives only on case-briefing.html, one of the two orphan
   pages build.py does not generate and package.py excludes. Do not add an
   override for it; it never reaches the deployment.
   ═════════════════════════════════════════════════════════════════════════ */
.section h2,
.phead h1{max-width:50%}
/* The 50% measure is for a heading that has the horizontal band to ITSELF —
   heading over subtitle, nothing beside them. A .split or .intro--split puts
   the heading in a column next to a photograph or a lead, where the column is
   already the constraint and halving it again just makes a ragged stack.
   Rigon, on "Built by People Who Like the Hard Part.": "this header should not
   have that limitation as its a different section." */
.split h2,
.intro--split h2{max-width:none}
.section > .wrap > .lead,
.intro:not(.intro--split) .lead,
.phead .lead{max-width:75%}

@media(max-width:859px){
  .section h2,
  .phead h1{max-width:100%}
  .section > .wrap > .lead,
  .intro:not(.intro--split) .lead,
  .phead .lead{max-width:90%}
}

/* ═══ STATS ═══ */
/* Full border, not border-block. The 1px gaps are drawn by the grid's own
   background showing through; without left/right edges the rounded corners
   have nothing to close against and the outline reads as broken. */
.stats{display:grid;gap:1px;background:var(--x-line);border:1px solid var(--x-line);
  border-radius:var(--radius);overflow:hidden;margin-top:var(--block)}
@media(min-width:700px){.stats{grid-template-columns:repeat(2,1fr)}}
@media(min-width:1100px){.stats{grid-template-columns:repeat(4,1fr)}}
.stat{background:var(--x-base);padding:var(--card);position:relative;
      transition:background var(--dur-fast) var(--ease)}
/* P03.1 · Hover shade, on a pseudo-element whose OPACITY animates.
   box-shadow was transitioned directly first and appeared instantly — opacity
   is the one property that always interpolates, so the shade lives on ::after
   and simply fades in. inset, not a border: .stats draws its dividers as 1px
   grid gaps and the cards carry no border of their own, so a real border would
   resize every card and shift the grid. */
.stat::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  /* P03.1 · Inset glow: primary at the card edge, fading inward ~16px. No ring
     and no radius — the cards are square and only the .stats container is
     rounded, so anything with a hard edge either looked like a border or had to
     chase a corner curve. A soft inset fades out before it reaches the corner,
     so the container's clip is invisible.
     The shadow itself never animates — it is static and only ::after's opacity
     transitions, which is the one property that always interpolates.
     16px is the reach; raise it to spread further inward. */
  box-shadow:inset 0 0 16px 0 var(--accent);
  opacity:0;transition:opacity var(--dur) var(--ease);
}
.stat:hover{background:var(--x-raised)}
.stat:hover::after{opacity:1}
.stat__n{font-family:var(--f-display);font-weight:500;color:var(--t-hi);
  font-size:clamp(2.6rem,4.6vw,4.6rem);line-height:1;letter-spacing:-0.045em;
  font-variant-numeric:tabular-nums;display:flex;align-items:baseline}
.stat__n small{font-size:.42em;color:var(--accent);margin-left:.12em;font-weight:500}
.stat__l{margin-top:1rem;font-size:.875rem;color:var(--t-mid);max-width:24ch}
.stat__k{margin-top:1.4rem;font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--t-low);font-weight:500}

/* ═══ FULL-BLEED BAND ═══ */
.band{position:relative;height:clamp(320px,58vh,640px);overflow:hidden}
/* Bands were sitting at native exposure under a heavy veil, which read as
   near-black. Lift the image and thin the veil so the photograph survives. */
.band img{width:100%;height:118%;object-fit:cover;will-change:transform;
  filter:brightness(1.06) saturate(1.04) contrast(.98)}
.band__veil{position:absolute;inset:0;
  background:linear-gradient(180deg,
    var(--x-base) 0%, transparent 16%, transparent 82%, var(--x-base) 100%)}
.band--void .band__veil{background:linear-gradient(180deg,
    var(--x-void) 0%, transparent 16%, transparent 82%, var(--x-void) 100%)}

/* ═══ STEPS ═══ */
/* ═══ APPROACH STEPS — P05 ════════════════════════════════════════════════════
   Figma node 96-245. A row of white cards on the dark field, overlapping
   slightly, each with a black title, a blue description and a blue number in
   the bottom-right corner. Hovering one scales it up and lifts it in front.

   The hover is a single transform. The design's active card is its resting
   card times 1.0705 on every axis — width, height, radius, shadow blur and
   shadow offset — with both centres on the same point, which is the signature
   of a scale() rather than a set of hand-tuned values. So it is written as
   one, and radius and shadow scale for free.

   The factor is the design's own 1.0705. It was briefly raised to 1.2 because
   the lift looked like it was doing nothing — but the cause was not the size,
   it was that the scale never applied at all (see the .rv note below). With
   that fixed, 7% reads correctly and the design's figure stands.

   Because it is a transform, the card cannot push its neighbours no matter how
   large the factor gets — "larger in the aspect that it does not break the
   page layout". The row also cannot overflow the viewport: the card is about
   22.75vw wide and grows 2.275vw per side, against a gutter of 4.5vw.

   DO NOT put `.rv` back on `.step`. The reveal animates `transform` as well
   (`translateY(28px)` in, then `.js .rv.in{transform:none}`), and its selector
   is the more specific: (0,3,0) against `.step:hover`'s (0,2,0). With `.rv` on
   the card the reveal pinned `transform:none` and this scale silently never
   applied — no error, no warning, just a hover that did nothing. `.rv` lives
   on `.steps__grid` so the card owns `transform` by itself.

   What did NOT survive the redesign: the accent progress rail above the row
   and the dim-the-others-to-55% hover. Both were the old visual system for
   the same job and would fight scale + lift.

   Proportions are the design's, sizes are NOT. The Figma is a 1440 recreation
   whose content column is 770px, so its cards are 200px and its titles 30px
   — 15% of card width. The real .wrap is up to 1560px; four cards filling it
   would be ~375px each and, at the design's ratio, carry a 57px title, larger
   than an h2. So the card row is capped and the title stays on the site's own
   --fs-h3 step. Matching the recreation's pixels would have broken the page's
   type scale.
   ═════════════════════════════════════════════════════════════════════════ */
.steps{margin-top:var(--block);position:relative}
.steps__grid{display:flex;align-items:stretch}
.step{
  flex:1 1 0;min-width:0;position:relative;
  display:flex;flex-direction:column;justify-content:center;
  text-align:left;
  background:var(--step-card);border-radius:var(--radius);
  /* Design ratio 199.83 : 286.95. Held as an aspect-ratio so the row keeps its
     shape at every width rather than collapsing to the tallest card's text. */
  aspect-ratio:199.83 / 286.95;
  /* The card is the type's container. The design sizes its title at 15.1% of
     card width and its description at 6.2% — that ratio is what makes every
     title wrap to two lines and the block read the same in each card. Held in
     cqw so it survives any row width instead of being pinned to one viewport.
     rem values sit first as the fallback where container queries are missing. */
  container-type:inline-size;
  padding:clamp(1.1rem,1.7vw,1.75rem);
  /* Heavier at the foot than the head so the centred block sits optically
     centred ABOVE the number rather than colliding with it. */
  padding-bottom:clamp(2.8rem,3.6vw,3.6rem);
  box-shadow:0 4px 34px rgba(0,0,0,.25);
  /* The design's own timing: 300ms, ease-OUT. --ease is the brand's
     cubic-bezier(.16,1,.3,1), which travels most of the distance early and
     settles — the card arrives rather than slides. --ease-io2 was wrong here:
     an ease-in-out starts slow, which on a hover reads as lag. */
  transition:transform var(--dur-hover) var(--ease);
}
/* The overlap. 5.7px on a 199.83px card in the design — 2.85% of card width,
   which across four cards is ~0.7% of the row. A negative margin resolves
   against the ROW width, not the card, hence the smaller number. */
.step + .step{margin-left:-.7%}
/* EARLIER cards sit above later ones: 01 on top, then 02, 03, 04. That is the
   design's own stacking — in the Figma each card's shadow falls on its LEFT
   edge, which only happens if the card to its left is in front. It also reads
   left-to-right, the order the steps are meant to be read in.

   Hover lifts the active card to 5, clear of all four. Nothing restores it on
   leave because nothing changed: :hover simply stops matching and the card
   falls back to its nth-child value. */
.step:nth-child(1){z-index:4}
.step:nth-child(2){z-index:3}
.step:nth-child(3){z-index:2}
.step:nth-child(4){z-index:1}
@media(min-width:860px){.step{border-bottom:0;padding-right:clamp(1.4rem,2.4vw,3rem)}}
.step__n{position:absolute;right:clamp(1.1rem,1.7vw,1.75rem);
  bottom:clamp(1rem,1.5vw,1.5rem);
  font-family:var(--f-display);font-weight:500;font-size:.8125rem;color:var(--step-blue);
  letter-spacing:.06em;font-variant-numeric:tabular-nums}
/* Title and description carry the design's ink and its tight metrics
   (line-height 1.086, letter-spacing -3%) but the site's own size step. */
.step h3{margin:0 0 .7rem;color:var(--step-ink);
  font-size:var(--fs-h3);line-height:1.086;letter-spacing:-.03em}
.step p{margin:0;font-size:.9375rem;line-height:1.31;letter-spacing:-.03em;
  color:var(--step-blue);font-weight:300}
@supports (font-size:1cqw){
  /* Clamped at the top so a wide card on a phone does not hand back a 45px
     title; the design's ratio holds everywhere below that. */
  .step h3{font-size:clamp(1.4rem,15.1cqw,2.35rem)}
  .step p{font-size:clamp(.85rem,6.2cqw,1rem)}
}

/* ═══ CAPABILITY ROWS ═══════════════════════════════════════════════════════
   Desktop: number | title | description | arrow — four columns, description
   sits to the RIGHT of the title and is always legible. On hover the peek
   image fades in directly OVER the description column, so nothing reflows.
   Mobile: collapses to number | (title + description) | arrow.
   ═════════════════════════════════════════════════════════════════════════ */
/* Mobile: 3 columns, description wraps under the title. */
.cap{
  display:grid;grid-template-columns:auto 1fr auto;align-items:center;
  gap:.4rem clamp(1.2rem,3.2vw,3rem);padding-block:clamp(2rem,4.2vh,3.2rem);
  border-top:1px solid var(--x-line);position:relative;
}
.cap__n {grid-column:1;grid-row:1;align-self:center}
.cap__t {grid-column:2;grid-row:1}
.cap__d {grid-column:2;grid-row:2}
.cap__go{grid-column:3;grid-row:1/span 2;align-self:center}

/* Desktop: 4 columns — description moves to its own column on the right. */
@media(min-width:1024px){
  .cap{grid-template-columns:auto minmax(0,1.02fr) minmax(0,1fr) auto}
  .cap__d {grid-column:3;grid-row:1;margin-top:0;
           padding-left:clamp(1rem,2vw,2rem);border-left:1px solid var(--x-line-soft)}
  .cap__go{grid-column:4;grid-row:1}
}
.cap:last-child{border-bottom:1px solid var(--x-line)}
.cap__n{font-size:.75rem;color:var(--t-low);font-variant-numeric:tabular-nums;
  letter-spacing:.08em;transition:color var(--dur-fast) var(--ease)}
.cap__t{display:block;font-family:var(--f-display);font-weight:500;color:var(--t-hi);
  font-size:clamp(1.7rem,4.1vw,3.5rem);letter-spacing:-0.035em;line-height:1;
  transition:transform var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
/* Description is ALWAYS rendered and always occupies its space. Hover only
   changes opacity and shifts it by a few px — the row never resizes, so
   neighbouring rows never jump under the pointer. */
.cap__d{display:block;font-size:.9375rem;color:var(--t-low);max-width:44ch;
  margin-top:.9rem;opacity:.62;
  transition:opacity var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease),
             transform var(--dur-fast) var(--ease)}
.cap__go{width:38px;height:38px;border:1px solid var(--x-line);border-radius:50%;
  display:grid;place-items:center;color:var(--t-low);flex:none;transition:.4s var(--ease)}
.cap:hover .cap__t,.cap:focus-visible .cap__t{transform:translateX(clamp(.4rem,1.2vw,1.4rem));color:var(--accent)}
.cap:hover .cap__n{color:var(--accent)}
.cap:hover .cap__d,.cap:focus-visible .cap__d{
  opacity:1;color:var(--t-mid);transform:translateX(clamp(.4rem,1.2vw,1.4rem))}
/* accent tick that grows from the left edge — costs no layout */
.cap::before{content:"";position:absolute;left:0;top:-1px;height:1px;width:100%;
  background:var(--accent);transform:scaleX(0);transform-origin:left;
  transition:transform var(--dur-fast) var(--ease)}
.cap:hover::before,.cap:focus-visible::before{transform:scaleX(1)}
.cap:hover .cap__go{background:var(--accent);border-color:var(--accent);color:var(--x-void);transform:rotate(-45deg)}
/* ── P13: the photograph, with the description captioned on it ──────────────
   The panel is the original one — a 4/3 photograph laid over the description
   column, deliberately taller than the row so it breaks the hairlines above
   and below. That overflow is what makes it read as a photograph on the list
   rather than a thumbnail in a cell. Absolute, so it costs no layout however
   far it spills.

   What P13 changes is that the description now rides ON the photograph,
   captioned along the bottom over a black gradient, instead of being covered
   by it. The first attempt shrank the picture to the description's own box so
   the text would align structurally — which aligned them perfectly and threw
   the photograph away. The picture is the point; the caption goes on it.
   ═════════════════════════════════════════════════════════════════════════ */
.cap__peek{position:absolute;top:50%;opacity:0;pointer-events:none;
  transform:translateY(-50%) scale(.96);
  transition:opacity var(--dur-fast) var(--ease),transform var(--dur-fast) var(--ease);
  overflow:hidden;border-radius:var(--radius);z-index:4;display:none}
.cap__peek img{width:100%;height:100%;object-fit:cover;filter:saturate(.9) contrast(1.04)}
/* Bottom-up black, so the caption reads on any photograph. Stops well short
   of the top — the picture has to stay a picture. */
.cap__peek::after{content:"";position:absolute;inset:0;z-index:1;
  background:linear-gradient(to top,
    rgba(0,0,0,.9) 0%, rgba(0,0,0,.72) 22%, rgba(0,0,0,.3) 52%, transparent 78%)}
.cap__cap{position:absolute;left:0;right:0;bottom:0;z-index:2;
  padding:clamp(.9rem,1.5vw,1.15rem) clamp(1rem,1.6vw,1.25rem);
  font-size:.875rem;line-height:1.45;color:var(--color-white);
  text-wrap:pretty}

@media(hover:hover) and (min-width:1024px){
  .cap__peek{display:block;right:4.4rem;width:min(34%,400px);aspect-ratio:4/3;height:auto}
  .cap:hover .cap__peek,.cap:focus-visible .cap__peek{opacity:1;transform:translateY(-50%) scale(1)}
  /* Lift the hovered row above its neighbours so the spill is never clipped. */
  .cap:hover{z-index:5}
  /* The column copy recedes as the panel comes forward — the caption on the
     photograph is carrying the description now. */
  .cap:hover .cap__d{opacity:.18}
}

/* ═══ WORK CARDS ═══ */
.wgrid{display:grid;gap:clamp(1.4rem,2.8vw,2.6rem);margin-top:var(--block)}
@media(min-width:900px){.wgrid{grid-template-columns:repeat(12,1fr)}}
.wcard{position:relative;overflow:hidden;border-radius:var(--radius);
  background:var(--x-raised);display:block;isolation:isolate}
.wcard--lg{grid-column:span 8}
.wcard--sm{grid-column:span 4}
.wcard--half{grid-column:span 6}
.wcard--full{grid-column:span 12}
.wcard__img{display:block;position:relative;overflow:hidden;aspect-ratio:16/10}
.wcard--sm .wcard__img{aspect-ratio:4/5}
.wcard--full .wcard__img{aspect-ratio:21/9}

/* ── Mobile masonry — P08 ────────────────────────────────────────────────────
   Below the 12-column breakpoint the grid used to collapse to one column: six
   identical full-width cards. Rigon asked for a masonry here too — "one full
   card, and two card under it, repeating again, as there are 6 items" — so the
   row rhythm is 1 / 2 / 1 / 2 on two columns.

   Positional, not by size class. The desktop lg/sm/half spans are tuned for a
   12-column field and mean nothing at two columns, so which cards go
   full-width is decided by their place in the run: the 1st and 4th of every
   six. That keeps the pattern correct for the 4-card grids on home and
   approach too (full / pair / full).

   `:last-child:nth-child(2)` catches the 2-card "next project" grid, where the
   2nd card would otherwise sit alone in a half slot with a hole beside it.

   Aspect ratios are re-set for the same reason: at two columns a 16/10 card is
   a letterbox slot barely taller than its own caption. Paired cards go
   portrait so the two of them read as one band, full-width rows stay
   landscape. The desktop --sm/--full ratios are left untouched above.
   ═════════════════════════════════════════════════════════════════════════ */
/* Written as the exact complement of the desktop query above, NOT as
   `max-width:899px`. That pair leaves a hole: a viewport of 899.5px — which is
   what browser zoom produces — satisfies neither, so .wgrid gets no
   grid-template-columns at all and the spans build implicit columns instead.
   Measured at 899px: six rows of one card, widths 818/416/617/617/416/818.
   `not all and (min-width:900px)` cannot leave a gap. */
@media not all and (min-width:900px){
  .wgrid{grid-template-columns:repeat(2,1fr)}
  /* Reset the 12-column spans FIRST. Without this the cards keep --lg's
     `span 8` and --half's `span 6`, and a span wider than the explicit track
     list makes the grid invent implicit columns: at 360px it built EIGHT
     columns, two of 77px and six of 0px, and the cards came out 22px and 67px
     wide. The two-column template alone is not enough. */
  .wgrid > .wcard{grid-column:span 1}
  .wgrid > .wcard .wcard__img{aspect-ratio:4/5}
  .wgrid > :nth-child(6n+1),
  .wgrid > :nth-child(6n+4),
  .wgrid > :last-child:nth-child(2){grid-column:span 2}
  .wgrid > :nth-child(6n+1) .wcard__img,
  .wgrid > :nth-child(6n+4) .wcard__img,
  .wgrid > :last-child:nth-child(2) .wcard__img{aspect-ratio:16/10}
}
.wcard__img img{width:100%;height:100%;object-fit:cover;filter:saturate(1) contrast(1.01);
  transition:transform 1400ms var(--ease),filter 900ms var(--ease)}
.wcard:hover .wcard__img img{transform:scale(1.055);filter:saturate(1.05)}
.wcard__veil{position:absolute;inset:0;
  background:linear-gradient(180deg,transparent 34%,color-mix(in srgb,var(--x-void) 88%,transparent))}
.wcard__body{position:absolute;inset:auto 0 0;padding:clamp(1.6rem,2.8vw,2.6rem)}
/* Uppercase micro-labels sit at the BOTTOM of a card, consistently across the
   site — .stat__k, .vfig__tags, .quote figcaption and this one. The tag used to
   lead the card, which meant work cards read tag-first while every other card
   read title-first. */
.wcard__tag{display:block;font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--color-lightblue);font-weight:500;margin-top:.85rem}
.wcard__t{display:block;font-family:var(--f-display);font-weight:500;color:var(--t-hi);
  font-size:clamp(1.1rem,1.85vw,1.75rem);letter-spacing:-0.024em;line-height:1.15}
.wcard__m{margin-top:.9rem;display:flex;flex-wrap:wrap;gap:.5rem 1.4rem;font-size:.8125rem;
  color:var(--t-mid);opacity:0;transform:translateY(8px);
  transition:opacity var(--dur-fast) var(--ease),transform var(--dur-fast) var(--ease)}
.wcard:hover .wcard__m,.wcard:focus-visible .wcard__m{opacity:1;transform:none}
.wcard__m b{color:var(--accent);font-weight:500}

/* ═══ CLIENT MARQUEE ═════════════════════════════════════════════════════════
   The track holds the logo list TWICE and translates by exactly -50%, so the
   loop is seamless with no JS. Duplicate is aria-hidden so screen readers and
   the accessibility tree see each client once.
   ═════════════════════════════════════════════════════════════════════════ */
.marq{position:relative;overflow:hidden;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
          mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent)}
.marq+.marq{margin-top:clamp(1.2rem,2.4vw,2.2rem)}
/* Breathing room between the section heading and the first logo row. The
   marquee has no top padding of its own and its mask fades the edges, so the
   logos read as sitting higher than they measure — this needs more space than
   a normal --block, not less. */
.marq-block{margin-top:clamp(2.2rem,3.6vw,3.4rem)}
/* P04: the heading and paragraph are gone, so the band is labelled by a single
   centred eyebrow. The old top margin was sized to clear an h2 + lead; against
   one small line it reads as a hole, hence the tighter value. */
.marq-sec .marq__eyebrow{margin:0;justify-content:center}
/* .eyebrow::before prints attr(data-num). With no number there is still an
   empty box, and the flex `gap` after it pushes the label off true centre. */
.marq-sec .marq__eyebrow::before{display:none}
.marq-sec .marq-block{margin-top:clamp(1.3rem,2.2vw,2rem)}
.marq-block .marq:last-child{margin-bottom:clamp(.6rem,1.4vw,1.2rem)}
.marq__track{display:flex;width:max-content;align-items:center;
  gap:var(--marq-gap);animation:marq var(--marq-dur,58s) linear infinite}
.marq__set{display:flex;align-items:center;gap:var(--marq-gap);flex:none}
.marq{--marq-gap:clamp(2.5rem,5vw,5rem)}
.marq--rev .marq__track{animation-direction:reverse;--marq-dur:72s}
/* --marq-shift is set by app.js to the measured width of ONE set plus one gap,
   so the loop restarts on an exact item boundary — a -50% guess drifts once
   flex `gap` is involved and produces a visible stutter each cycle. */
@keyframes marq{
  from{transform:translate3d(0,0,0)}
  to  {transform:translate3d(calc(-1 * var(--marq-shift,50%)),0,0)}
}
/* Fallback only. app.js adds .marq--ease when it can tween playbackRate, which
   eases the row to a standstill instead of cutting it dead on one frame. This
   hard pause is what browsers without getAnimations() still get. */
.marq:not(.marq--ease):hover .marq__track{animation-play-state:paused}
.marq__track:not([data-ready]){animation:none}
/* An <img> for the logo, plus an accent-filled <i> masked by the same PNG for
   the hover wash. Two layers on purpose: if the mask ever fails to load, the
   hover tint is lost but the logo is still there. */
.marq__logo{position:relative;flex:none;display:block;line-height:0}
.marq__logo img{
  height:clamp(30px,3.1vw,46px);width:auto;display:block;
  opacity:.55;
  transition:opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.marq__logo i{
  position:absolute;inset:0;pointer-events:none;opacity:0;
  background-color:var(--accent);
  -webkit-mask-repeat:no-repeat;   mask-repeat:no-repeat;
  -webkit-mask-position:center;    mask-position:center;
  -webkit-mask-size:contain;       mask-size:contain;
  transition:opacity var(--dur-fast) var(--ease);
}
/* The img stays fully opaque on hover. It used to fade to 0 so only the
   accent overlay showed — but a mask-image that fails to load hides its
   element completely in Chrome, so a broken overlay left NOTHING behind
   and the logo went transparent on hover. Keeping the img means the
   worst case is "no tint", never "no logo". */
.marq__logo:hover img{opacity:1}
.marq__logo:hover i{opacity:1}
.light .marq__logo i{background-color:var(--lt-accent)}
@media (prefers-reduced-motion:reduce){
  .marq__track{animation:none;flex-wrap:wrap;width:auto;justify-content:center}
  .marq{mask-image:none;-webkit-mask-image:none}
}

/* ═══ PRICING / PACKAGES ═════════════════════════════════════════════════════ */
.plans{display:grid;gap:1px;background:var(--x-line);border:1px solid var(--x-line);
  margin-top:var(--block)}
@media(min-width:900px){.plans{grid-template-columns:repeat(3,1fr)}}
.plan{background:var(--x-base);padding:var(--card);
  display:flex;flex-direction:column;gap:1rem;position:relative;
  transition:background var(--dur-fast) var(--ease)}
.plan:hover{background:var(--x-raised)}
.plan--feat{background:var(--x-raised)}
.plan--feat::before{content:"";position:absolute;inset:0 0 auto;height:2px;background:var(--accent)}
.plan__tier{font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--t-low);font-weight:500;display:flex;
  justify-content:space-between;align-items:center;gap:1rem}
.plan__tier b{color:var(--accent);font-weight:500}
.plan__name{font-family:var(--f-display);font-weight:500;color:var(--t-hi);
  font-size:clamp(1.5rem,2.4vw,2rem);letter-spacing:-0.03em;line-height:1}
.plan__price{font-family:var(--f-display);font-weight:500;color:var(--t-hi);
  font-size:clamp(1.6rem,2.6vw,2.2rem);letter-spacing:-0.035em;line-height:1;
  font-variant-numeric:tabular-nums}
.plan__price small{display:block;font-family:var(--f-body);font-size:.75rem;
  font-weight:500;letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;
  color:var(--t-low);margin-bottom:.5rem}
.plan ul{display:grid;gap:.6rem;margin-top:.4rem;padding-top:1.2rem;
  border-top:1px solid var(--x-line)}
.plan li{font-size:.875rem;color:var(--t-mid);display:flex;gap:.7rem;align-items:flex-start}
.plan li::before{content:"";width:5px;height:5px;margin-top:.55rem;flex:none;
  background:var(--accent);border-radius:50%}
.plan .btn{margin-top:auto;justify-content:center}
.rates{display:flex;flex-wrap:wrap;gap:1px;background:var(--x-line);
  border:1px solid var(--x-line);margin-top:var(--block)}
.rate{background:var(--x-base);padding:1.7rem 2rem;flex:1 1 240px}
.rate dt{font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--t-low);font-weight:500}
.rate dd{margin-top:.5rem;font-family:var(--f-display);font-weight:500;
  color:var(--t-hi);font-size:1.5rem;letter-spacing:-0.03em}
.rate dd span{color:var(--accent);font-size:.75rem;letter-spacing:.06em;
  text-transform:uppercase;margin-left:.5rem;font-family:var(--f-body)}

/* ═══ LIGHT SECTION ═══ */
/* ═══ LIGHT SURFACES ═════════════════════════════════════════════════════════
   .light declares six variables; every rule below reads those, never a palette
   colour. A variant like .light--blue only has to redefine the six. This is the
   tone-token idea applied narrowly — light sections only, dark untouched.
   ═════════════════════════════════════════════════════════════════════════ */
.light{
  --lt-1:      var(--x-light);           /* section surface */
  --lt-2:      var(--color-light-2);     /* cells, hovers */
  --lt-line:   var(--x-light-line);
  --lt-rule:   var(--x-light-rule);
  --lt-ink-hi: var(--t-on-light-hi);
  --lt-ink-mid:var(--t-on-light-mid);
  --lt-accent: var(--accent-deep);
  /* Form fields need their own pair. A field painted --lt-1 sits at exactly
     the section colour, which leaves the 1px border doing 100% of the work of
     saying "this is an input" — and --lt-line against --lt-1 measures 1.22:1,
     which is not enough to see. White field + a stronger edge instead. */
  --lt-field:      #FFFFFF;
  --lt-field-line: #BCC5D0;
  /* Gradient, not a flat slab, and not an inset shadow — inset shadows on
     full-width sections read as dated panel chrome. This gives depth at the
     seam while staying flat enough to feel like paper. */
  background:linear-gradient(180deg,
    var(--lt-1) 0%,
    color-mix(in srgb, var(--lt-2) 55%, var(--lt-1)) 100%);
  color:var(--lt-ink-mid);
  border-block:1px solid var(--lt-line);
}

/* Baby blue — pale Sky Blue as a surface. Warmer than ivory and unmistakably
   brand-owned. Ink shifts to a deep navy rather than near-black so the whole
   section stays in the blue family. */
.light--blue{
  --lt-1:      var(--color-white);
  --lt-2:      var(--color-white-2);
  --lt-line:   var(--color-baby-line);
  --lt-rule:   var(--color-baby-rule);
  --lt-ink-hi: var(--color-baby-ink);
  --lt-ink-mid:var(--color-baby-mid);
  --lt-accent: var(--color-baby-accent);
  --lt-field:      #FFFFFF;
  --lt-field-line: #A9C4E2;
}
.light h1,.light h2,.light h3{color:var(--lt-ink-hi)}
.light .eyebrow{color:var(--lt-ink-mid)}
.light .eyebrow::after{background:var(--lt-rule)}
.light .lead{color:var(--lt-ink-mid)}
.split{display:grid;gap:clamp(3rem,5.6vw,6rem);align-items:center}
@media(min-width:960px){.split{grid-template-columns:1.05fr .95fr}}
/* reverse without direction:rtl — that leaks into punctuation and scrollbars */
@media(min-width:960px){
  .split--rev>*:first-child{order:2}
  .split--rev>*:last-child{order:1}
}
.figure{position:relative;overflow:hidden;border-radius:var(--radius);aspect-ratio:5/4}
.figure img{width:100%;height:100%;object-fit:cover}
/* Dark is the DEFAULT for these, light is the variant — not the other way
   round. They used to hardcode light colours because the story block was the
   only light section that existed; now any section can flip, so each needs a
   dark state or it renders light-on-light when the theme says "None". */
.badge{display:inline-flex;align-items:center;gap:.6rem;padding:.55rem 1rem;
  border:1px solid var(--accent);border-radius:100px;color:var(--accent);
  font-size:.75rem;font-weight:500;letter-spacing:.06em;text-transform:uppercase}
.light .badge{border-color:var(--lt-accent);color:var(--lt-accent)}
/* Six cells, five values — the blank leads rather than trails, so the block
   ends flush instead of on a half-empty row. */
.values{display:grid;gap:1px;background:var(--x-line);margin-top:var(--block);
  border:1px solid var(--x-line);border-radius:var(--radius);overflow:hidden}
@media(min-width:900px){.values{grid-template-columns:repeat(3,1fr)}}

/* Six values fill 3x2 exactly, so no span is needed. */
.light .values{background:var(--lt-line);border-color:var(--lt-line)}
@media(min-width:560px){.values{grid-template-columns:repeat(2,1fr)}}
/* Minimal, non-displacing hover: an accent rule wipes in from the left and the
   label warms up. No movement, no resize, no shadow. */
.value{background:var(--x-base);padding:1.75rem 1.9rem;position:relative;
  transition:background var(--dur-fast) var(--ease)}
.value::before{content:"";position:absolute;left:0;top:0;bottom:0;width:2px;
  background:var(--accent);transform:scaleY(0);transform-origin:top;
  transition:transform var(--dur-fast) var(--ease)}
.value:hover{background:var(--x-raised)}
.value:hover::before{transform:scaleY(1)}

/* Number and title on one line — the number is a marker, not a heading, so
   giving it its own row made the card taller than its content warranted. */
.value__hd{display:flex;align-items:baseline;gap:.75rem;margin-bottom:.7rem}
.value b{font-family:var(--f-display);font-weight:500;color:var(--t-hi);
  font-size:1rem;letter-spacing:-.014em;line-height:1.25;
  transition:color var(--dur-fast) var(--ease)}
.value:hover b{color:var(--accent)}
.value span{display:block;font-size:.85rem;line-height:1.58;color:var(--t-mid);max-width:34ch}

.light .value{background:var(--lt-1)}
.light .value:hover{background:var(--lt-2)}
.light .value b{color:var(--lt-ink-hi)}
.light .value:hover b{color:var(--lt-accent)}
.light .value span{color:var(--lt-ink-mid)}

/* same treatment for the spec/fact lists on dark */
.fact{position:relative;transition:background var(--dur-fast) var(--ease)}
.fact::before{content:"";position:absolute;left:0;top:0;bottom:0;width:2px;
  background:var(--accent);transform:scaleY(0);transform-origin:top;
  transition:transform var(--dur-fast) var(--ease)}
.fact:hover{background:var(--x-raised)}
.fact:hover::before{transform:scaleY(1)}

/* and the step cards */
/* Scale from the centre, jump clear of every sibling. 1.0705 is the design's
   own ratio; 300ms ease-out is its transition. The shadow and the radius scale
   with the card for free — that is exactly what the Figma frames show, because
   they were scaled the same way. */
@media (hover:hover) and (pointer:fine){
  .step:hover,.step:focus-within{transform:scale(1.0705);z-index:5}
}
/* Keyboard users get the same lift on touch devices, where :hover never fires
   but focus still does. */
.step:focus-within{transform:scale(1.0705);z-index:5}

/* ── Below the four-across breakpoint: a snap scroller ───────────────────────
   Four overlapping cards cannot hold at 360px, and the design is desktop-only.
   A snap row keeps the card shape and the one-at-a-time reading the design is
   built around, instead of flattening them into stacked blocks.

   The overlap goes: cards that overlap inside a scroller read as a rendering
   fault rather than a dealt hand. And the row is full-bleed — it cancels the
   wrap's gutter and re-adds it as scroll padding, so a card can sit flush to
   the screen edge while still resting at the gutter when snapped. */
@media (max-width:859px){
  .steps__grid{
    max-width:none;margin-inline:calc(var(--gutter) * -1);
    padding-inline:var(--gutter);padding-block:.5rem;
    overflow-x:auto;overscroll-behavior-x:contain;
    scroll-snap-type:x mandatory;scroll-padding-inline:var(--gutter);
    scrollbar-width:none;
  }
  .steps__grid::-webkit-scrollbar{display:none}
  .step{flex:0 0 min(76%,300px);scroll-snap-align:start}
  .step + .step{margin-left:clamp(.7rem,2.5vw,1rem)}
  /* No scale in a scroller — a transform would be clipped by overflow-x. */
  .step:focus-within{transform:none}
}

@media (prefers-reduced-motion:reduce){
  .step{transition:none}
  /* Still identify the hovered card — the cue just stops moving. */
  .step:hover,.step:focus-within{transform:none;
    box-shadow:0 4px 34px rgba(0,0,0,.25),0 0 0 2px var(--step-blue)}
}

/* ═══ INSIGHT CARDS ═══ */
.igrid{display:grid;gap:1px;background:var(--x-line);margin-top:var(--block);
  border:1px solid var(--x-line)}
@media(min-width:820px){.igrid{grid-template-columns:repeat(3,1fr)}}
.icard{background:var(--x-base);padding:var(--card);
  display:flex;flex-direction:column;gap:1rem;min-height:300px;
  transition:background var(--dur-fast) var(--ease)}
.icard:hover{background:var(--x-raised)}
.icard__meta{display:flex;gap:.9rem;font-size:.6875rem;letter-spacing:.16em;
  text-transform:uppercase;color:var(--t-low);font-weight:500}
.icard__meta b{color:var(--accent);font-weight:500}
.icard h3{font-size:1.1875rem;line-height:1.25;margin-top:.4rem}
.icard p{font-size:.9rem;color:var(--t-low)}
.icard__go{margin-top:auto;font-size:.8125rem;color:var(--t-mid);display:flex;
  align-items:center;gap:.5rem;transition:.4s var(--ease)}
.icard:hover .icard__go{color:var(--accent);gap:.85rem}

/* ═══ FACTS / SPEC LIST ═══ */
.facts{display:grid;gap:1px;background:var(--x-line);border:1px solid var(--x-line);margin-top:var(--block)}
@media(min-width:640px){.facts{grid-template-columns:repeat(2,1fr)}}
.fact{background:var(--x-base);padding:1.7rem 1.8rem}
.fact dt{font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--t-low);font-weight:500}
.fact dd{margin-top:.6rem;color:var(--t-hi);font-family:var(--f-display);font-weight:500;font-size:1rem}

/* ═══ CTA ═══ */
.cta{background:var(--x-void);position:relative;overflow:hidden;padding-block:clamp(6rem,16vh,13rem)}
.cta::before{content:"";position:absolute;inset:0;
  background:radial-gradient(70% 90% at 50% 118%, color-mix(in srgb,var(--accent-deep) 46%,transparent), transparent 66%)}
.cta__in{position:relative;text-align:center}
.cta h2{font-size:clamp(2.4rem,6.2vw,6rem);letter-spacing:-0.042em;max-width:16ch;margin-inline:auto}
.cta .lead{margin:var(--stack) auto 0;text-align:center}
.cta__actions{display:flex;gap:.85rem;justify-content:center;flex-wrap:wrap;margin-top:var(--block)}

/* ═══ FORM ═══ */
/* Flex column, not grid. Both lay a single column out identically, but the
   form now carries a reserved min-height (app.js, tallest step) and grid
   would share that slack out across EVERY auto row — the progress bar, the
   fieldset and the buttons would each get a third of it and the whole form
   would drift as the steps changed. In a flex column the slack goes where an
   auto margin puts it, which is .fnav: the buttons sit on the floor of the
   reserved box and stay put from step to step. */
.form{display:flex;flex-direction:column;gap:1.2rem;max-width:640px}
@media(min-width:640px){.form__row{display:grid;grid-template-columns:1fr 1fr;gap:1.2rem}}
.field{display:grid;gap:.5rem}
/* EVERY field label, whatever it introduces. This used to be `.field label`
   only, which covered text, email and select — but a radio or checkbox group
   cannot use <label> (there is no single control to point at), so it emits
   <span class="field__lab"> and got none of it: 15.2px / 300 / sentence case
   against 11px / 500 / uppercase for its neighbours. Two treatments for one
   role, three fields apart. "texts are incosistent."

   NOT eyebrows any more — "remove the uppercase text from form. keep them
   nomral." Uppercase and 2.2px of tracking is the site's SECTION eyebrow, a
   label for a whole band of the page; on eleven field labels in a column it
   was doing a different job and shouting while it did it. They read as
   sentence-case labels now, one step down from the input text they introduce,
   at --t-mid rather than --t-low because losing the caps loses some presence
   and they should not go quiet as well. */
.field label:not(.choice),.field .field__lab{
  font-size:.875rem;letter-spacing:normal;text-transform:none;
  color:var(--t-mid);font-weight:500}
.field input,.field textarea,.field select{
  background:var(--x-raised);border:1px solid var(--x-line);border-radius:var(--radius);
  padding:.9rem 1rem;color:var(--t-hi);font-family:var(--f-body);font-size:.9375rem;
  transition:border-color var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease);
}
.field input:focus,.field textarea:focus,.field select:focus{
  outline:none;border-color:var(--accent);background:var(--x-base)}
.field textarea{min-height:150px;resize:vertical}

/* ═══ FOOTER ═══ */
.foot{background:var(--x-void);border-top:1px solid var(--x-line);padding-block:clamp(4rem,7.5vh,6rem)}
.foot__grid{display:grid;gap:3.2rem}
@media(min-width:800px){.foot__grid{grid-template-columns:1.4fr repeat(3,1fr)}}
.foot h2{font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;
  color:var(--t-low);font-weight:500;margin-bottom:1rem;font-family:var(--f-body)}
.foot ul{display:grid;gap:.6rem}
.foot a{font-size:.875rem;color:var(--t-mid);transition:color .3s var(--ease)}
.foot a:hover{color:var(--accent)}
.foot__bar{margin-top:var(--block);padding-top:1.8rem;border-top:1px solid var(--x-line-soft);
  display:flex;flex-wrap:wrap;gap:1rem;justify-content:space-between;font-size:.75rem;color:var(--t-low)}

/* ═══ THEME PANEL ════════════════════════════════════════════════════════════
   Four controls, a reset, and nothing else. Opens with T (or the corner pill).
   Every control writes a CSS custom property on :root — no component knows the
   panel exists. Choices persist in localStorage and are re-applied by an inline
   script in <head> BEFORE first paint, so a chosen theme never flashes.
   ═════════════════════════════════════════════════════════════════════════ */
.tp{
  position:fixed;right:1.1rem;bottom:1.1rem;z-index:401;width:min(280px,calc(100vw - 2.2rem));
  background:color-mix(in srgb,var(--x-raised) 96%,transparent);
  backdrop-filter:blur(22px) saturate(150%);
  border:1px solid var(--x-line);border-radius:calc(var(--radius) + 4px);
  padding:1.15rem;
  opacity:0;visibility:hidden;transform:translateY(10px) scale(.98);transform-origin:bottom right;
  transition:opacity var(--dur-fast) var(--ease),transform var(--dur-fast) var(--ease),
             visibility var(--dur-fast);
  box-shadow:0 24px 60px -20px rgb(0 0 0 / .7);
}
.tp.open{opacity:1;visibility:visible;transform:none}
.tp__hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:1.1rem}
.tp__hd b{font-family:var(--f-body);font-size:.625rem;font-weight:500;
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;color:var(--t-low)}
.tp__x{color:var(--t-low);font-size:1rem;line-height:1;padding:.2rem}
.tp__x:hover{color:var(--t-hi)}

.tp__row{margin-bottom:1.05rem}
.tp__row:last-of-type{margin-bottom:.4rem}
.tp__lbl{display:flex;justify-content:space-between;align-items:baseline;
  font-family:var(--f-body);font-size:.625rem;font-weight:500;
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;
  color:var(--t-low);margin-bottom:.5rem}
.tp__lbl span{color:var(--accent);font-variant-numeric:tabular-nums;letter-spacing:.04em}

/* segmented control */
.tp__seg{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:1px;
  background:var(--x-line);border:1px solid var(--x-line);border-radius:var(--radius);overflow:hidden}
.tp__seg button{
  background:var(--x-base);padding:.45rem .2rem;font-size:.6875rem;font-weight:500;
  color:var(--t-low);transition:background var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease);
}
.tp__seg button:hover{color:var(--t-hi)}
.tp__seg button[aria-pressed="true"]{background:var(--accent);color:var(--x-void)}

/* swatches */
.tp__sw{display:flex;gap:.5rem}
.tp__sw button{
  width:28px;height:28px;border-radius:var(--radius);border:1px solid var(--x-line);
  transition:transform var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease);
}
.tp__sw button:hover{transform:scale(1.09)}
.tp__sw button[aria-pressed="true"]{border-color:var(--t-hi);box-shadow:0 0 0 2px var(--x-raised),0 0 0 3px var(--t-hi)}

/* range */
.tp__row input[type=range]{
  -webkit-appearance:none;appearance:none;width:100%;height:2px;background:var(--x-line);
  border-radius:2px;outline:none;
}
.tp__row input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;width:14px;height:14px;border-radius:50%;
  background:var(--accent);cursor:pointer;border:0;
}
.tp__row input[type=range]::-moz-range-thumb{
  width:14px;height:14px;border-radius:50%;background:var(--accent);cursor:pointer;border:0;
}
.tp__reset{
  width:100%;margin-top:.7rem;padding:.5rem;border:1px solid var(--x-line);
  border-radius:var(--radius);font-size:.6875rem;font-weight:500;color:var(--t-low);
  transition:color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease);
}
.tp__reset:hover{color:var(--t-hi);border-color:var(--t-low)}
.tp__hint{margin-top:.7rem;font-size:.625rem;color:var(--t-low);text-align:center;opacity:.7}
.tp__hint kbd{font-family:var(--f-body);border:1px solid var(--x-line);border-radius:3px;
  padding:.05rem .3rem;font-size:.9em}

/* No keyboard, no chord, no panel. */
@media (hover:none),(pointer:coarse){ .tp{display:none} }

/* ═══ ROUNDED-MODE FOLLOW-THROUGH ════════════════════════════════════════════
   Things that were deliberately square when --radius was 2px and need to
   round in sympathy once it isn't. */
.wcard,.figure,.phead__media,.cap__peek,.band{border-radius:var(--radius)}
.marq img{border-radius:calc(var(--radius) * .3)}
.stats,.facts,.igrid,.plans,.values,.rates{border-radius:var(--radius);overflow:hidden}
.field input,.field textarea,.field select{border-radius:var(--radius)}

/* ═══ COLOUR: accent presence beyond the one-accent rule ═════════════════════
   The v1 build used Sky Blue about twice per viewport. Client asked for more
   brand colour without losing the dark base — these are tints, not fills. */
.icard:hover{background:color-mix(in srgb,var(--accent) 7%,var(--x-raised))}
.plan--feat{background:color-mix(in srgb,var(--accent-deep) 14%,var(--x-raised))}
.cta::before{
  background:radial-gradient(70% 90% at 50% 118%,
    color-mix(in srgb,var(--accent-deep) 58%,transparent),transparent 66%);
}
.section--tint{
  background:linear-gradient(180deg,
    color-mix(in srgb,var(--accent-deep) 13%,var(--x-base)),
    var(--x-base) 62%);
}

/* ═══ LIGHT SECTION — FULL COVERAGE ══════════════════════════════════════════
   .light was originally only ever applied to the story block, so it only
   restyled headings, eyebrow and lead. Now that any marked section can become
   an "eyebreaker", every component that can appear inside one needs a light
   counterpart. Without these a light section renders dark-on-light text. */

.light h1,.light h2,.light h3,.light h4{color:var(--lt-ink-hi)}
.light .eyebrow{color:var(--lt-ink-mid)}
.light .eyebrow::after{background:var(--lt-rule)}
.light .lead,.light .prose p{color:var(--lt-ink-mid)}
.light .btn{border-color:var(--lt-line);color:var(--lt-ink-hi)}
/* P02 correction, found while doing P18. The line above is (0,2,0) and
   out-ranked .btn--solid's own (0,1,0) colour, so every solid button inside a
   light section came out BLUE PLATE / BLACK LABEL — "Continue", "Start the
   conversation", "Open in your mail app". The rule is blue background -> white
   text, everywhere. */
.light .btn--solid{color:var(--t-on-accent)}
.light .btn:hover{color:var(--t-on-accent)}      /* blue plate  -> white */
.light .btn--solid:hover{color:var(--x-void)}    /* white plate -> black */

/* steps: no .light overrides. After P05 the cards are white with their own
   ink in every theme, and the rail those rules also covered is gone. The
   section is a plain .section on both pages, so nothing here was ever lit. */

/* grids that draw themselves with 1px gaps over a background colour */
.light .stats,.light .facts,.light .igrid,.light .plans,.light .rates{
  background:var(--x-light-line);border-color:var(--x-light-line)}
.light .stat,.light .fact,.light .icard,.light .plan,.light .rate{background:var(--lt-1)}
.light .icard:hover{background:color-mix(in srgb,var(--accent) 9%,var(--lt-1))}
.light .stat__l,.light .icard p,.light .fact dd,.light .rate dd{color:var(--lt-ink-mid)}
.light .stat__n,.light .plan__name,.light .plan__price,.light .fact dd,.light .rate dd{
  color:var(--t-on-light-hi)}
.light .stat__k,.light .fact dt,.light .rate dt,.light .icard__meta{color:var(--lt-ink-mid)}
.light .plan ul{border-color:var(--lt-line)}
.light .plan li{color:var(--lt-ink-mid)}
.light .icard__go{color:var(--lt-ink-hi)}

/* capability rows */
.light .cap{border-color:var(--lt-line)}
.light .cap__t{color:var(--lt-ink-hi)}
.light .cap__d{color:var(--lt-ink-mid)}
.light .cap__go{border-color:var(--lt-line);color:var(--lt-ink-mid)}

/* marquee logos are white PNGs — invert them on a light field */
.light .marq img{filter:grayscale(1) invert(1)}


/* ═══ VIDEO — click-to-play facade ═══════════════════════════════════════════
   Poster + play button; app.js swaps in the iframe on click. Eleven
   autoloading players would each pull ~1MB of chrome and set a cookie before
   anyone pressed anything.
   ═════════════════════════════════════════════════════════════════════════ */
.vgrid{display:grid;gap:clamp(1.2rem,2.4vw,2rem);margin-top:var(--block)}
@media(min-width:760px){.vgrid{grid-template-columns:repeat(2,1fr)}}
@media(min-width:1240px){.vgrid{grid-template-columns:repeat(3,1fr)}}

.vfig{display:flex;flex-direction:column;gap:1rem}
.vfig__frame{position:relative;aspect-ratio:16/9;overflow:hidden;
  border-radius:var(--radius);background:var(--x-raised);
  border:1px solid var(--x-line);display:grid;place-items:center}
.vfig__frame img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  filter:saturate(.92) contrast(1.02);
  transition:transform 1200ms var(--ease),filter var(--dur-fast) var(--ease)}
.vfig:hover .vfig__frame img{transform:scale(1.04)}
.vfig__veil{position:absolute;inset:0;
  background:linear-gradient(180deg,transparent 30%,color-mix(in srgb,var(--x-void) 62%,transparent))}

.vfig__play{position:relative;width:62px;height:62px;border-radius:50%;
  background:color-mix(in srgb,var(--x-void) 55%,transparent);
  border:1px solid color-mix(in srgb,var(--t-hi) 55%,transparent);
  backdrop-filter:blur(8px);display:grid;place-items:center;
  transition:background var(--dur-fast) var(--ease),transform var(--dur-fast) var(--ease),
             border-color var(--dur-fast) var(--ease)}
.vfig__play svg{width:26px;height:26px;fill:var(--t-hi);margin-left:3px;
  transition:fill var(--dur-fast) var(--ease)}
.vfig:hover .vfig__play{background:var(--accent);border-color:var(--accent);transform:scale(1.07)}
.vfig:hover .vfig__play svg{fill:var(--x-void)}

/* no link yet — say so rather than offer a dead button */
.vfig__soon{position:relative;padding:.5rem .9rem;border-radius:100px;
  border:1px dashed color-mix(in srgb,var(--t-hi) 32%,transparent);
  font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--t-mid);font-weight:500;
  background:color-mix(in srgb,var(--x-void) 55%,transparent);backdrop-filter:blur(8px)}

/* the iframe app.js injects */
.vfig__frame iframe{position:absolute;inset:0;width:100%;height:100%;border:0;z-index:2}
.vfig.is-playing .vfig__veil,.vfig.is-playing .vfig__play{opacity:0;pointer-events:none}

.vfig__cap{display:flex;flex-direction:column;gap:.5rem}
.vfig__t{font-family:var(--f-display);font-weight:500;color:var(--t-hi);
  font-size:clamp(1rem,1.35vw,1.2rem);letter-spacing:-0.02em;line-height:1.25}
.vfig__note{font-family:var(--f-body);font-size:.72rem;color:var(--t-low);font-weight:400}
.vfig__chal{font-size:.875rem;color:var(--t-mid)}
.vfig__tags{font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--t-low);font-weight:500}

.light .vfig__frame{background:var(--lt-2);border-color:var(--lt-line)}
.light .vfig__t{color:var(--lt-ink-hi)}
.light .vfig__chal{color:var(--lt-ink-mid)}
.light .vfig__tags,.light .vfig__note{color:var(--lt-ink-mid)}

/* ═══ VIDEO PAGE ═══════════════════════════════════════════════════════════
   Banner is the film itself, letterboxed into the same grade as every other
   page head so it reads as art direction rather than an embed dropped in.
   Poster-first: the iframe is built on click (app.js §8b), so no vendor
   player and no third-party cookie loads until someone asks to watch. */
.vhero{position:relative;padding-top:clamp(8rem,18vh,13rem);
  padding-bottom:clamp(3rem,6vh,5rem);overflow:hidden;isolation:isolate}
.vhero::before{content:"";position:absolute;inset:0;z-index:0;
  background:radial-gradient(72% 88% at 12% 0%,
    color-mix(in srgb,var(--accent-deep) 28%,transparent),transparent 64%)}
.vhero>.wrap{position:relative;z-index:3}
.vhero__kicker{font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--accent);font-weight:500}
.vhero h1{max-width:20ch;margin-top:var(--stack);
  font-size:clamp(1.8rem,3.6vw,3.6rem)}

.vstage{position:relative;aspect-ratio:16/9;overflow:hidden;
  border-radius:var(--radius);background:var(--x-raised);
  border:1px solid var(--x-line);display:grid;place-items:center}
.vstage--big{margin-top:var(--block);
  box-shadow:0 40px 90px -50px color-mix(in srgb,var(--x-void) 92%,transparent)}
.vstage img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  filter:saturate(.94) contrast(1.02)}
.vstage__veil{position:absolute;inset:0;
  background:linear-gradient(180deg,color-mix(in srgb,var(--x-void) 22%,transparent) 0%,
    transparent 38%,color-mix(in srgb,var(--x-void) 55%,transparent))}
.vstage__play{position:relative;width:clamp(64px,7vw,92px);aspect-ratio:1;border-radius:50%;
  background:color-mix(in srgb,var(--x-void) 52%,transparent);
  border:1px solid color-mix(in srgb,var(--t-hi) 55%,transparent);
  backdrop-filter:blur(10px);display:grid;place-items:center;cursor:pointer;
  transition:background var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}
.vstage__play svg{width:38%;height:38%;fill:var(--t-hi);margin-left:6%;
  transition:fill var(--dur-fast) var(--ease)}
.vstage__play:hover{background:var(--accent);border-color:var(--accent);transform:scale(1.06)}
.vstage__play:hover svg{fill:var(--x-void)}
.vstage__label{position:absolute;left:1.1rem;bottom:1rem;z-index:3;
  font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--t-mid);font-weight:500}
.vstage__soon{position:relative;padding:.55rem 1rem;border-radius:100px;
  border:1px dashed color-mix(in srgb,var(--t-hi) 32%,transparent);
  font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--t-mid);font-weight:500;
  background:color-mix(in srgb,var(--x-void) 55%,transparent);backdrop-filter:blur(8px)}
.vstage iframe{position:absolute;inset:0;width:100%;height:100%;border:0;z-index:4}
.vstage.is-playing .vstage__veil,
.vstage.is-playing .vstage__play,
.vstage.is-playing .vstage__label{opacity:0;pointer-events:none}

.vtags{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:var(--stack)}
.vtags span{padding:.34rem .8rem;border-radius:100px;border:1px solid var(--x-line);
  font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--t-mid);font-weight:500}
.vpage__chal{margin-top:var(--stack);color:var(--t-mid);font-size:.95rem}
.vpage__chal b{color:var(--t-hi);font-weight:500;margin-right:.4rem}

.vextras{display:grid;gap:var(--card);margin-top:var(--block)}
@media(min-width:900px){.vextras{grid-template-columns:1fr 1fr}}
.vextra h3{margin-bottom:.9rem;font-size:clamp(1rem,1.3vw,1.15rem)}

.vpager{display:grid;gap:1px;background:var(--x-line);border:1px solid var(--x-line);
  border-radius:var(--radius);overflow:hidden}
@media(min-width:720px){.vpager{grid-template-columns:1fr 1fr}}
.vpager>a{display:flex;flex-direction:column;gap:.4rem;padding:1.5rem 1.7rem;
  background:var(--x-base);transition:background var(--dur-fast) var(--ease)}
.vpager>a:hover{background:var(--x-raised)}
.vpager__next{text-align:right}
.vpager span{font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--t-low);font-weight:500}
.vpager b{font-family:var(--f-display);font-weight:500;color:var(--t-hi);
  font-size:clamp(.98rem,1.25vw,1.15rem);letter-spacing:-0.02em;line-height:1.25}

a.vfig__t{transition:color var(--dur-fast) var(--ease)}
a.vfig__t:hover{color:var(--accent)}

.light .vstage,.light .vpager>a{background:var(--lt-2)}
.light .vstage,.light .vpager,.light .vtags span{border-color:var(--lt-line)}
.light .vpager{background:var(--lt-line)}
.light .vpager b,.light .vpage__chal b{color:var(--lt-ink-hi)}
.light .vpager span,.light .vtags span,.light .vpage__chal{color:var(--lt-ink-mid)}

/* ═══ MULTI-STEP CONTACT FORM ══════════════════════════════════════════════
   Three short steps rather than one long column. Perceived effort drops even
   though more is being asked, and step traversal is something most spam
   scripts never do — so the shape is itself a mild filter. */
/* Contact layout.
   THE BUG: .split sets align-items:center, and the form column changes height
   between steps (step 2 is much taller than step 1). Centring a short aside
   against a changing-height form means the aside JUMPS vertically every time
   you press Continue — which is what "not centered vertically" actually looks
   like. Centring is the wrong instruction here; the two columns should both
   start at the top and the aside should hold its place.
   The aside is sticky so it stays with you as the form grows, offset by the
   fixed nav height plus a little air. */
/* ── P18: ONE BOX, SPLIT DOWN THE MIDDLE ─────────────────────────────────
   Rigon: "Form and image can be witihn same box, and divided in the middle."
   So the form and the photograph are two halves of a single card sharing one
   border and one radius, 1fr / 1fr, rather than a 1.3/.7 split of loose
   columns with a sticky floating aside.

   The photograph is absolutely positioned inside its half and covers it, so
   the two halves are always exactly as tall as each other and the picture
   grows and shrinks with the form as the steps change height — no aspect
   ratio to pick, and nothing to jump. `overflow:hidden` is what makes the
   image meet the card's rounded corners; native select popups are in the top
   layer and are not clipped by it.

   The card is --lt-2 (the cells colour) rather than --lt-1, because the fields
   inside it are white: a white card would leave the inputs invisible except
   for their 1px border, which is the exact problem the --lt-field pair was
   introduced to solve. */
/* Dark is the resting state and light is the variant, the way round every
   other component here is built. */
.fbox{display:grid;border:1px solid var(--x-line);border-radius:var(--radius);
  background:var(--x-raised);overflow:hidden}
@media(min-width:960px){.fbox{grid-template-columns:1fr 1fr}}
.fbox__form{padding:clamp(1.6rem,3.2vw,3.2rem)}
.fbox__form .form{max-width:none}
/* min-width:0 down the whole chain. A grid item defaults to min-width:auto,
   which means it refuses to shrink below its content's min-content width — and
   a <select>'s min-content is its WIDEST OPTION ("Executive engagement
   program"). At 390px that made the form column 379px inside a 342px card, so
   the progress row and the choice buttons ran under the card's own
   overflow:hidden and were sliced off. Nothing here changes layout at any
   width where it already fitted; it only allows shrinking where it did not. */
.fbox > *,
.fbox__form .form,.fbox__form .fstep,.fbox__form .fstep__body,
.fbox__form .field,.fbox__form .field > *{min-width:0}
.fbox__form select,.fbox__form input,.fbox__form textarea{max-width:100%}
/* The card is --x-raised and so were the inputs, which left them invisible
   against it — the same problem --lt-field was introduced to solve on the
   light side, arriving on the dark side the moment the section went dark.
   The fields drop to --x-base, so they read as wells cut into the card. The
   option buttons were already --x-base and needed nothing. */
.fbox__form .field input,
.fbox__form .field textarea,
.fbox__form .field select{background:var(--x-base)}
.fbox__form .field input:focus,
.fbox__form .field textarea:focus,
.fbox__form .field select:focus{background:var(--x-base)}
.fbox__media{position:relative;min-height:16rem;order:-1}
@media(min-width:960px){.fbox__media{order:0;min-height:100%}}
.fbox__media img{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;display:block}
.light .fbox{background:var(--lt-2);border-color:var(--lt-line)}

/* ── the contact section's WHITE variant ────────────────────────────────────
   It is data-lightable="1", which is the ivory priority — syncLight only ever
   hands .light--blue to priority 2 — so it can never be given the baby-blue
   class. But plain .light was not neutral either: --lt-2 is #E8EDF2 and
   --lt-line is #D8DEE4, both blue-grey, and the section's own background is a
   gradient that ends in a mix of them. So this redefines the six it reads and
   paints a FLAT colour over that gradient.

   Three neutral steps, which is the whole ask: the section is the greyish
   white, the card is white on top of it, and the inputs step back down to
   greyish so they read as wells inside the card rather than disappearing into
   it — the same relationship the dark side has between --x-base, --x-raised
   and the fields. */
.sec--form.light{
  --lt-1:      var(--color-form-card);
  --lt-2:      var(--color-form-field);
  --lt-line:   var(--color-form-line);
  --lt-rule:   var(--color-form-line);
  --lt-field:      var(--color-form-field);
  --lt-field-line: var(--color-form-field-line);
  background:var(--color-form-bg);
}
.sec--form.light .fbox{background:var(--color-form-card);
  border-color:var(--color-form-line)}
/* Out-ranks the dark .fbox__form rule above (0,4,1 against 0,2,1). */
.sec--form.light .fbox__form .field input,
.sec--form.light .fbox__form .field textarea,
.sec--form.light .fbox__form .field select,
.sec--form.light .fbox__form .field input:focus,
.sec--form.light .fbox__form .field textarea:focus,
.sec--form.light .fbox__form .field select:focus{
  background:var(--color-form-field)}

.fprog{display:flex;align-items:center;justify-content:space-between;gap:1rem;
  padding-bottom:1.1rem;margin-bottom:var(--stack);
  border-bottom:1px solid var(--x-line)}
.fprog__dots{display:flex;gap:.45rem}
.fprog__dots i{width:26px;height:3px;border-radius:1px;background:var(--x-line);
  transition:background var(--dur-fast) var(--ease)}
.fprog__dots i.on{background:var(--accent)}
/* Sentence case with the labels: it is the only other uppercase string left
   in the form, and leaving it would have made it the loudest thing there. */
.fprog__txt{font-size:.8125rem;letter-spacing:normal;text-transform:none;
  color:var(--t-low);font-weight:500}
.fprog__txt b{color:var(--t-hi);font-weight:500}

.fstep{border:0;padding:0;margin:0}
/* Block, not flex. As a flex row the number sat BESIDE the title, which
   pushed the title and subtext right by the number's width plus the gap — so
   the header began at a different x from every field below it. Stacked, the
   number, the title, the subtext and the first field label all start on the
   same line. */
.fstep__hd{display:block;margin-bottom:var(--stack);padding:0}
.fstep__hd b{display:block;font-family:var(--f-display);font-weight:500;
  color:var(--t-hi);font-size:clamp(1.15rem,1.6vw,1.5rem);letter-spacing:-0.02em}
.fstep__hd i{display:block;font-style:normal;color:var(--t-mid);
  font-size:.9rem;line-height:1.55;margin-top:.35rem;max-width:46ch}
.fstep__body{display:flex;flex-direction:column;gap:1.25rem}

.field__lab,.form label{display:block}
.field__lab i,.form label i{color:var(--accent);font-style:normal;margin-left:.25rem}
.field__help{display:block;font-size:.8rem;color:var(--t-low);margin-top:-.2rem;
  margin-bottom:.5rem}
.field__err{display:none;font-size:.8rem;color:#E5714F;margin-top:.4rem}
.field.is-bad .field__err{display:block}
.field.is-bad input,.field.is-bad select,.field.is-bad textarea{border-color:#E5714F}

.choices{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.6rem}
.choice{position:relative;cursor:pointer}
.choice input{position:absolute;opacity:0;width:0;height:0}
/* Was 100px — a lozenge. "full rounded should be removed", so these take the
   brand radius like every other surface on the site. */
/* .choice IS a <label>, so it was picking up the field-label eyebrow by
   accident and the options rendered "NEXT 3 MONTHS" — 11px, uppercase, 2.2px
   of tracking. They are values the visitor picks, not labels introducing
   something, so they read at control size in the case they were written in,
   matching the text in the inputs beside them. */
.choice span{display:block;padding:.7rem 1.05rem;border-radius:var(--radius);
  border:1px solid var(--x-line);background:var(--x-base);color:var(--t-mid);
  font-family:var(--f-body);font-size:.9375rem;font-weight:400;
  letter-spacing:normal;text-transform:none;
  transition:border-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease)}
.choice:hover span{border-color:var(--t-low);color:var(--t-hi)}
.choice input:checked + span{border-color:var(--accent);color:var(--t-hi);
  background:color-mix(in srgb,var(--accent) 14%,transparent)}
.choice input:focus-visible + span{outline:2px solid var(--accent);outline-offset:2px}

/* Honeypot. Off-canvas rather than display:none — some bots skip hidden
   fields, far fewer skip ones that are merely positioned away. */
.hp{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}

/* margin-top:auto pins this to the bottom of the reserved height;
   padding-top keeps the minimum air above it on the tall step, where there is
   no slack left to absorb. */
.fnav{display:flex;gap:.7rem;align-items:center;
  margin-top:auto;padding-top:var(--block)}
.fnav .btn[hidden]{display:none}
.arw--back{display:inline-block;margin-right:.15rem}
.form__note{margin-top:1rem;font-size:.85rem;color:#E5714F}
.form__note[hidden]{display:none}

.fdone{padding:var(--card) 0}
.fdone[hidden]{display:none}
.fdone h2{color:var(--t-hi);max-width:20ch}
.fdone p{margin-top:.8rem;color:var(--t-mid);max-width:44ch}

/* Short mode — theme panel drops every field not marked data-short.
   Attribute on <html> so the pre-paint inline script can set it before
   first paint and the fields never flash in and out. */
html[data-formlen="short"] .form--steps .field:not([data-short]){display:none}
html[data-formlen="short"] .fstep:has(.field[data-short]) .fstep__hd i{display:none}

.light .fprog{border-color:var(--lt-line)}
.light .fprog__dots i{background:var(--lt-line)}
.light .fprog__txt b,.light .fstep__hd b,.light .fdone h3{color:var(--lt-ink-hi)}
.light .fstep__hd i,.light .fdone p{color:var(--lt-ink-mid)}
.light .choice span{background:var(--lt-field, #FFFFFF);
  border-color:var(--lt-field-line, #BCC5D0);color:var(--lt-ink-mid)}
.light .choice input:checked + span{color:var(--lt-ink-hi)}
.tp__note{font-size:.72rem;line-height:1.5;color:var(--t-low);margin-top:.5rem}

/* Multi-select reads differently from single-select: a checked checkbox keeps
   its tick, a checked radio simply fills. Without this the two look identical
   and people tick one option thinking the rest will clear. */
.choices--checkbox .choice input:checked + span::before{content:"✓ ";color:var(--accent)}
.choices--radio .choice input:checked + span{font-weight:500}

/* Embedding disabled by the rights holder — the control leaves the site, so it
   is an <a>, not a <button>, and it says where it goes. Same circle as play:
   the affordance is still "watch this", only the destination differs. */
.vstage__play--out,.vfig__play--out{text-decoration:none}
.vstage.is-out .vstage__label{opacity:1}
.vfig__play--out{display:grid}

/* Social links: mark on the left, label on the same line. inline-flex with a
   baseline-safe centre — the label is .875rem and the glyph 16px, so aligning
   on the text baseline would sit the icon low. */
.soc{display:inline-flex;align-items:center;gap:.55rem}
.soc svg{width:16px;height:16px;flex:none;opacity:.72;
  transition:opacity var(--dur-fast) var(--ease)}
.soc:hover svg{opacity:1}

/* ═══ PILLARS: 3x2 ════════════════════════════════════════════════════════
   Five pillars + one link cell. Four used to fit 4-up and 2-up exactly; five
   orphans a cell at every breakpoint, so the grid went to three columns —
   which also keeps .stat__n at full display size instead of shrinking it to
   survive a 20% column. */
@media(min-width:1100px){.stats--3{grid-template-columns:repeat(3,1fr)}}

/* The cards are links now — WEB CONTENT.pdf: "These should all connect to our
   work." Reset the anchor so it still lays out as a block. */
a.stat{display:block;color:inherit;text-decoration:none}
/* .stat__n must stay flex — the % / K / + suffix is a <small> aligned on the
   figure's baseline. A blanket display:block here silently dropped it to the
   bottom of the line box. */
a.stat .stat__n{display:flex;align-items:baseline}
a.stat .stat__l,a.stat .stat__k{display:block}

/* The 6th cell. Same surface as the others so it reads as part of the grid
   rather than a button dropped into it. */
.stat--go{display:grid;place-items:center;min-height:100%}
.stat__go{display:inline-flex;align-items:center;gap:.7rem;
  font-family:var(--f-display);font-weight:500;letter-spacing:-0.02em;
  font-size:clamp(1.05rem,1.5vw,1.35rem);color:var(--t-hi)}
.stat--go .arw{transition:transform var(--dur-fast) var(--ease)}
.stat--go:hover .arw{transform:translateX(5px)}
.light .stat__go{color:var(--lt-ink-hi)}

/* ═══ PROJECT GALLERY ══════════════════════════════════════════════════════
   Uniform tiles, no spans. The first pass mixed `wide` and `tall` spans over an
   arbitrary photo count, which is what leaves the ragged holes a grid cannot
   close — CSS grid will not reflow a 2-column item backwards to plug a gap, and
   `dense` only helps when a later item happens to fit.

   The fix is arithmetic, not CSS: TWELVE photographs against 2 / 3 / 4 columns.
   12 divides by all three, so every row fills completely at every breakpoint and
   there is no trailing orphan. Keep the count a multiple of 12's divisors. */
.gal{display:grid;gap:clamp(.5rem,1vw,1rem);margin-top:var(--block);
  grid-template-columns:repeat(2,1fr)}
@media(min-width:760px){.gal{grid-template-columns:repeat(3,1fr)}}
@media(min-width:1240px){.gal{grid-template-columns:repeat(4,1fr)}}

/* The About gallery. No heading above it, so no gap to clear either, and the
   section drops its top padding entirely: what is left between the photograph
   and the tiles is the .phead's own 85px bottom padding, near enough the 70px
   (--block) that already separates the lead from that photograph. So the grid
   reads as the next element of the page-head block rather than as a new
   section — "that big image stays, and under that we show the grid". */
.gal--bare{margin-top:0}
.section--galtop{padding-block-start:0}
.gal__item{position:relative;overflow:hidden;border-radius:var(--radius);
  aspect-ratio:4/3;background:var(--x-raised);border:1px solid var(--x-line);
  padding:0;cursor:zoom-in;display:block;
  transition:border-color var(--dur-fast) var(--ease)}
.gal__item img{width:100%;height:100%;object-fit:cover;display:block;
  filter:saturate(.96);
  transition:transform 900ms var(--ease),filter var(--dur-fast) var(--ease)}
.gal__item:hover img{transform:scale(1.05);filter:saturate(1.04)}
.gal__item:hover{border-color:var(--accent)}
.gal__item:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.light .gal__item{background:var(--lt-2);border-color:var(--lt-line)}

/* ═══ LIGHTBOX ═════════════════════════════════════════════════════════════
   Built on <dialog>, so the browser supplies the top layer, the focus trap,
   inert-ing of the page behind, and Esc-to-close. Hand-rolling those is where
   most lightboxes get accessibility wrong. */
.lb{border:0;padding:0;max-width:100vw;max-height:100vh;width:100%;height:100%;
  background:transparent;color:var(--t-hi);overflow:hidden}
.lb::backdrop{background:color-mix(in srgb,var(--color-void,#0A0E12) 92%,transparent);
  backdrop-filter:blur(6px)}
.lb__stage{position:absolute;inset:0;display:grid;place-items:center;
  padding:clamp(3rem,7vw,5rem) clamp(1rem,5vw,5rem)}
.lb__stage img{max-width:100%;max-height:100%;width:auto;height:auto;
  object-fit:contain;border-radius:var(--radius);display:block}
.lb__cap{position:absolute;left:0;right:0;bottom:clamp(.9rem,2.5vw,1.6rem);
  text-align:center;font-size:.85rem;color:var(--t-mid);
  padding-inline:clamp(1rem,5vw,5rem)}
.lb__count{display:block;font-size:var(--fs-eyebrow);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;
  color:var(--t-low);font-weight:500;margin-top:.45rem}
.lb__btn{position:absolute;top:50%;transform:translateY(-50%);
  width:48px;height:48px;border-radius:var(--radius);
  background:color-mix(in srgb,var(--x-void) 55%,transparent);
  border:1px solid color-mix(in srgb,var(--t-hi) 30%,transparent);
  backdrop-filter:blur(8px);color:var(--t-hi);display:grid;place-items:center;
  cursor:pointer;transition:background var(--dur-fast) var(--ease)}
.lb__btn:hover{background:var(--accent);color:var(--x-void);border-color:var(--accent)}
.lb__btn svg{width:22px;height:22px}
.lb__prev{left:clamp(.5rem,2vw,1.6rem)}
.lb__next{right:clamp(.5rem,2vw,1.6rem)}
.lb__close{position:absolute;top:clamp(.9rem,2.5vw,1.6rem);
  right:clamp(.9rem,2.5vw,1.6rem);width:44px;height:44px;transform:none}
.lb__btn:disabled{opacity:.3;cursor:default}
@media(max-width:620px){
  .lb__btn{width:42px;height:42px}
  .lb__prev{left:.4rem}.lb__next{right:.4rem}
}
@media(prefers-reduced-motion:reduce){
  .gal__item img{transition:none}
  .gal__item:hover img{transform:none}
}

/* ═══ FORM CONTROLS ON A LIGHT SECTION ═════════════════════════════════════
   The contact section is data-lightable, so it turns ivory whenever light
   sections are on — but the inputs were still painted --x-raised (#1A242F)
   with white text. Dark boxes on an ivory panel do not read as a style, they
   read as a rendering fault.

   This is the exact failure SKILLS.md §3 warns about: a component authored
   while its section had a fixed background hardcodes that background, then
   breaks the first time the section flips. .badge and .values both did it.
   Every control below now reads the --lt-* variables like the rest of the
   light system. */
.light .field label,
.light .field__lab{color:var(--lt-ink-mid)}
.light .field__help{color:var(--lt-ink-mid);opacity:.85}
.light .field input,
.light .field textarea,
.light .field select{
  background:var(--lt-field, #FFFFFF);
  border-color:var(--lt-field-line, #BCC5D0);
  color:var(--lt-ink-hi);
}
.light .field input::placeholder,
.light .field textarea::placeholder{color:var(--lt-ink-mid);opacity:.7}
.light .field input:focus,
.light .field textarea:focus,
.light .field select:focus{
  border-color:var(--lt-accent);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--lt-accent) 18%, transparent);
}
/* select renders its own dropdown chrome from the text colour on some
   platforms; setting both keeps the arrow visible on a pale field. */
.light .field select{color-scheme:light}
.light .field.is-bad input,
.light .field.is-bad textarea,
.light .field.is-bad select{border-color:#B4442A}
.light .field__err,
.light .form__note{color:#B4442A}

/* Contact photograph. Squarer than the 4:5 it was — a tall portrait crop
   beside a three-step form left a long empty column on the right at desktop
   width. Class, not an inline style: inline beats every class rule and is
   what made 13 sections unthemeable at v5.1. */
.figure--square{aspect-ratio:1/1}
@media(max-width:999px){.figure--square{aspect-ratio:4/3}}

/* ═══ SUCCESS FALLBACK ═════════════════════════════════════════════════════
   mailto opens the visitor's mail client — unless they haven't got one wired
   up, which is common on webmail-only desktops. In that case the click does
   nothing at all and, without this panel, they would read a confirmation
   having sent us absolutely nothing. So the composed message is always
   printed, with the address in plain text and a copy button. */
.fdone__fallback{margin-top:var(--stack);padding-top:1.4rem;
  border-top:1px solid var(--x-line)}
.fdone__to{font-size:.85rem;color:var(--t-mid);margin-bottom:.9rem}
.fdone__to a{color:var(--accent)}
.fdone__msg{margin:0 0 1.1rem;padding:1.1rem 1.2rem;border-radius:var(--radius);
  background:var(--x-raised);border:1px solid var(--x-line);
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:.8125rem;line-height:1.6;color:var(--t-mid);
  white-space:pre-wrap;word-break:break-word;
  max-height:15rem;overflow:auto}
.light .fdone__fallback{border-color:var(--lt-line)}
.light .fdone__to{color:var(--lt-ink-mid)}
.light .fdone__to a{color:var(--lt-accent)}
.light .fdone__msg{background:var(--lt-field, #FFFFFF);
  border-color:var(--lt-field-line, #BCC5D0);color:var(--lt-ink-mid)}
.fdone__acts{display:flex;flex-wrap:wrap;gap:.7rem;align-items:center}
.gal__demo{margin-top:.9rem;font-size:.85rem;color:var(--t-low)}
.light .gal__demo{color:var(--lt-ink-mid)}

/* ═══ HERO CROSSFADE ═══════════════════════════════════════════════════════
   Frames are stacked and cropped to identical dimensions, so the transition is
   pure opacity — nothing moves, nothing reflows, and the parallax transform on
   .hero__media still applies to the whole stack rather than to one image.
   The fade is long (1.6s) on purpose: a quick cut reads as a glitch on a
   cinematic hero, a slow dissolve reads as intent. */
/* Specificity note: `.hero__media img` (0,1,1) beats `.hero__frame` (0,1,0), so
   these MUST be written as `.hero__media .hero__frame` or the shared sizing
   silently wins and the frames sit at a different height from each other.
   height:112% and object-position match `.hero__media img` exactly — the
   parallax translate depends on that overflow existing. */
.hero__media .hero__frame{
  position:absolute;inset:0;
  width:100%;height:112%;object-fit:cover;object-position:center 45%;
  opacity:0;transition:opacity 1600ms var(--ease);
}
.hero__media .hero__frame.is-on{opacity:1}
@media(prefers-reduced-motion:reduce){
  /* No rotation at all — app.js also bails out, this is the belt to its braces.
     A repeating crossfade is exactly the kind of unrequested motion the
     reduced-motion preference exists to stop. */
  .hero__media .hero__frame{transition:none}
  .hero__media .hero__frame:not(.is-on){display:none}
}

/* ═══ TESTIMONIALS ═════════════════════════════════════════════════════════
   Featured quote as type, then a static grid. The scroll-snap slider was
   dropped at v5.3: static grids outperform carousels because visitors rarely
   click through, and 8 of 11 quotes sat unseen behind an arrow press.

   Cards are uniform now. They used to size to their own text via --qw, which
   worked while each card had a metric headline giving it internal hierarchy;
   with that gone, varied widths just read as ragged. */
/* ═══ TESTIMONIALS — P11 / P11.1 ═════════════════════════════════════════════
   TWO SECTIONS, not one component restyled. Rigon: "there should be two
   seperate sections with left and middle align. and theme toggle changes the
   section, do not reuse the code exactly." Both render; the theme shows one.
   display:none takes the hidden one out of the accessibility tree with it, so
   the duplicated heading is never announced twice.

   They share the CARD and the arrows. Everything about arrangement is in two
   independent blocks below, free to be tuned without touching each other.

   THE CARD NEVER FADES. Only its text does. An earlier pass faded the whole
   figure, which took the white background down with it — "I explicitly told
   to don change the opacity of the bg color, just the text should be less
   visible." The cards are opaque white at every depth; --tf drives the text.
   ═════════════════════════════════════════════════════════════════════════ */
html[data-talign="left"]   .tsec--center{display:none}
html:not([data-talign="left"]) .tsec--left{display:none}

/* ── the card, shared ────────────────────────────────────────────────────── */
/* THE card width. Fixed at 26rem for as long as 26rem fits, and only when the
   container drops below ~520px does it fall back to 80% of it — which is
   exactly "card width should not be dynamic ... on mobile it is 80% to give
   space to other cards to be visible", but as one continuous value instead of
   a breakpoint, so there is no width at which the card jumps.

   cqw, not vw: the stages below declare themselves query containers, so this
   is 80% OF THE STAGE, which is what the neighbours have to fit inside. vw
   would have measured the wrong box and been wrong by the gutter. */
.tsec{--tcw:min(26rem,80cqw)}
.tq{
  position:absolute;margin:0;
  /* Roomier again. The quote has grown to ~290 characters and the old
     1.6-2.4rem was letting it run close to the corners; this gives it a real
     margin and, with the longer text, is most of why the cards are taller. */
  padding:clamp(1.8rem,3.2vw,3.1rem);
  /* THE measured height, shared by every card in the deck. It was only being
     applied to the stage, so the cards still sized themselves individually and
     the deck was a row of different heights inside one tall box. app.js
     measures the tallest and publishes --th; this is what makes every card
     take it. */
  height:var(--th,auto);
  /* The quote takes the space that is left and centres in it; the attribution
     is the last flex child and sits on the floor of the padding box. That is
     what keeps every avatar on the same line across the deck whatever the
     quote length. */
  display:flex;flex-direction:column;justify-content:flex-start;gap:1rem;
  background:var(--step-card);border-radius:var(--radius);
  box-shadow:0 6px 40px rgba(0,0,0,.32);
  transition:transform var(--dur-page) var(--ease);
}
/* The fade lives HERE, on the contents, never on the figure. */
.tq blockquote,.tq figcaption{opacity:var(--tf,1);
  transition:opacity var(--dur-page) var(--ease)}
/* `margin-block:auto` is what centres the quote and pins the attribution — NOT
   flex centring. Making the blockquote a flex container turns its own text
   nodes and every <b class="tq__hi"> into separate flex items, and the quote
   comes out in columns: "…awareness by | 50% | and boosting…". Auto margins
   absorb the free space above and below the quote instead, leaving it a normal
   block whose text flows, and pushing figcaption onto the floor of the padding
   box. */
.tq blockquote{margin:auto 0;flex:none;
  color:var(--step-ink);line-height:1.58;letter-spacing:-.01em;
  font-weight:300;text-wrap:pretty;text-align:center}
/* THREE steps. The four-bucket version was scrapped because the quotes ran
   232-469 characters and the extremes looked like different components; the
   quotes are 232-335 now, and five of them are already the client's full text
   with nothing left to add, so a gentle three-step scale is all it takes to
   level them. */
.tq[data-size="l"] blockquote{font-size:clamp(.95rem,1.15vw,1.15rem)}
.tq[data-size="m"] blockquote{font-size:clamp(.9rem,1.05vw,1.05rem)}
.tq[data-size="s"] blockquote{font-size:clamp(.85rem,.97vw,.97rem)}
.tq__hi{color:var(--accent);font-weight:500}
/* One word of the section heading in the accent (content.json ->
   testimonials.titleAccent). The only coloured word in any heading on the
   site, and deliberate. */
.thead__hi{color:var(--accent)}
.tq figcaption{display:flex;flex-direction:column;align-items:center;gap:.4rem;
  text-align:center;flex:none}
.tq__ava{display:block;color:var(--accent)}
.tq__ava svg{width:26px;height:26px;display:block}
.tq__role{font-size:.75rem;letter-spacing:.04em;color:var(--step-ink);opacity:.66}
.tq[data-k="0"]{cursor:default}
.tq:not([data-k="0"]){cursor:pointer}

/* ── arrows, shared ──────────────────────────────────────────────────────── */
.tdc__nav,.tdl__nav{display:flex;gap:.7rem;margin-top:clamp(1.4rem,2.4vw,2.2rem)}
.tdc__nav{justify-content:center}
.tdl__nav{justify-content:flex-start}
.tnav{width:44px;height:44px;flex:none;display:grid;place-items:center;
  border-radius:50%;background:transparent;color:var(--t-hi);
  border:1px solid color-mix(in srgb,var(--t-hi) 34%,transparent);cursor:pointer;
  transition:background var(--dur-hover) var(--ease),border-color var(--dur-hover) var(--ease),
             color var(--dur-hover) var(--ease)}
.tnav svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.6;
  stroke-linecap:round;stroke-linejoin:round}
.tnav:hover,.tnav:focus-visible{background:var(--accent);border-color:var(--accent);
  color:var(--t-on-accent)}

/* ══ ARRANGEMENT 1 — CENTRED ═════════════════════════════════════════════════
   The measured Figma fan, symmetrical about the middle. Heading and subtitle
   centre with it.
   ═════════════════════════════════════════════════════════════════════════ */
.tsec--center .wrap > h2,
.tsec--center .wrap > .lead{text-align:center;margin-inline:auto}
.tdc{margin-top:var(--block)}
/* NOT full-bleed. Rigon: "the cards container should not be full width, or it
   can be full width but cards should be closer to each other and indipendet
   form width." The card is a fixed width and every offset below is a
   percentage OF THE CARD, so the whole fan is a fixed pixel object that stops
   growing with the viewport — which is the "independent from width" half —
   and the container is back inside .wrap, which is the other half. */
.tdc__stage{position:relative;height:var(--th,auto);min-height:16rem;
  overflow-x:clip;container-type:inline-size}
.tdc .tq{top:50%;left:50%;width:var(--tcw);
  transform:translate(-50%,-50%) translateX(var(--tx,0)) scale(var(--ts,1));
  z-index:var(--tz,1)}
.tdc .tq[data-k="0"] {--ts:1;    --tf:1;   --tz:5}
.tdc .tq[data-k="-1"],.tdc .tq[data-k="1"]{--ts:.869;--tf:.62;--tz:4}
.tdc .tq[data-k="-2"],.tdc .tq[data-k="2"]{--ts:.664;--tf:.34;--tz:3}
/* TWO offsets per ring, and the smaller one wins.

   .58 / .94 of the card is the fan at rest: a fixed pixel object that stops
   growing with the viewport, which is "cards should be closer to each other
   and indipendet form width".

   The second term is the offset at which that ring's OUTER EDGE lands exactly
   on the container edge. A ring at scale s is s x --tcw wide, so its centre
   can be at most 50cqw - (s/2) x --tcw out before it starts hanging over the
   side: .4345 is .869/2 for the first ring, .332 is .664/2 for the second.

   min() of the two means the fan holds its fixed shape on a wide screen and
   then, the moment the screen is too narrow for that shape, closes up to sit
   flush inside the container instead of being sliced by the clip. Rigon: "the
   second I make the screen smaller the overflow hidden is cutting the cards,
   we should see the cards edge on containers edge. overflow hidden is to
   prevent bugs." So the clip is now only ever a safety net - at every width
   there is nothing left for it to cut. */
.tdc .tq{--t1:min(.58 * var(--tcw),50cqw - .4345 * var(--tcw));
         --t2:min(.94 * var(--tcw),50cqw - .332 * var(--tcw))}
.tdc .tq[data-k="-1"]{--tx:calc(-1 * var(--t1))} .tdc .tq[data-k="1"]{--tx:var(--t1)}
.tdc .tq[data-k="-2"]{--tx:calc(-1 * var(--t2))} .tdc .tq[data-k="2"]{--tx:var(--t2)}
.tdc .tq[data-k="far"],
.tdc .tq[data-k="3"],.tdc .tq[data-k="-3"]{--ts:.6;--tf:0;--tz:1;
  opacity:0;pointer-events:none}

/* ══ ARRANGEMENT 2 — LEFT ════════════════════════════════════════════════════
   The SAME mechanism as the centred deck, at Rigon's instruction — "middle
   verisons animation is great, apply the same logic to left aligned on. do not
   over engineer keep them simple as they are."

   So: one transform per slot, fixed offsets as a percentage of the card's own
   width, exactly like the centred fan. The measured --tstep and the slot
   arithmetic that produced it are gone; the run still fills the section
   because the card is sized in vw, so the offsets scale with it.

   The steps shrink as the cards travel right, which is what makes the run
   read as a hand dealt out rather than a row.

   ONE run length, and every slot is a fraction of it. --trun is the offset of
   the LAST slot: 1.95x the card while that fits, and otherwise exactly the
   offset that lands that card's right edge on the container edge (a card at
   scale .75 sitting at left:0 reaches tx + .875 x --tcw). The slots in front
   of it keep the fractions of the run they always had — .359 / .656 / .872 of
   1.95 — so the run holds its shape and simply closes up as the container
   narrows instead of being sliced by the clip. This is the centred deck's
   min() applied to a one-sided run: "the centered version repossivenes is
   perfect. make it similar in left version as well."
   ═════════════════════════════════════════════════════════════════════════ */
.tdl{margin-top:var(--block)}
.tdl__stage{position:relative;height:var(--th,auto);min-height:16rem;
  overflow-x:clip;container-type:inline-size}
.tdl .tq{top:50%;left:0;width:var(--tcw);
  transform:translateY(-50%) translateX(var(--tx,0)) scale(var(--ts,1));
  z-index:var(--tz,1)}
.tdl .tq{--trun:min(1.95 * var(--tcw),100cqw - .875 * var(--tcw))}
.tdl .tq[data-k="0"]{--tx:0;                        --ts:1;   --tf:1;   --tz:6}
.tdl .tq[data-k="1"]{--tx:calc(.359 * var(--trun)); --ts:.93; --tf:.55; --tz:5}
.tdl .tq[data-k="2"]{--tx:calc(.656 * var(--trun)); --ts:.86; --tf:.38; --tz:4}
.tdl .tq[data-k="3"]{--tx:calc(.872 * var(--trun)); --ts:.80; --tf:.26; --tz:3}
.tdl .tq[data-k="4"]{--tx:var(--trun);              --ts:.75; --tf:.18; --tz:2}
/* Everything past the last slot waits underneath the active card. */
.tdl .tq[data-hide],.tdl .tq[data-k="far"]{--tx:0;--ts:.72;--tf:0;--tz:1;
  opacity:0;pointer-events:none}

/* ── small screens ───────────────────────────────────────────────────────────
   Nothing here touches width or spacing any more — the min() pairs above
   handle that continuously. All that is left is how many slots are worth
   showing at all: three either way.
   ═════════════════════════════════════════════════════════════════════════ */
@media(max-width:900px){
  .tdc .tq[data-k="-2"],.tdc .tq[data-k="2"]{opacity:0;pointer-events:none;--tf:0}
  /* Three slots, so the run ends at slot 2 and is measured to it: 1.28x the
     card while that fits, otherwise flush (scale .86 reaches tx + .93x). .547
     is .70/1.28 — the same shape, one slot shorter. */
  .tdl .tq{--trun:min(1.28 * var(--tcw),100cqw - .93 * var(--tcw))}
  .tdl .tq[data-k="1"]{--tx:calc(.547 * var(--trun))}
  .tdl .tq[data-k="2"]{--tx:var(--trun)}
  .tdl .tq[data-k="3"],.tdl .tq[data-k="4"]{opacity:0;pointer-events:none;--tf:0}
}

/* The fan is wider than the section by design — the neighbours are supposed to
   run off the edge. Clip so that never becomes page-wide horizontal scroll.
   overflow-x:clip rather than hidden: it does not make a scroll container, so
   nothing here can trap a scroll or clip the vertical shadow. */
.tsec{overflow-x:clip}

@media (prefers-reduced-motion:reduce){
  .tq,.tq blockquote,.tq figcaption{transition:none}
}

.tgrid{display:grid;gap:clamp(.9rem,1.6vw,1.4rem);margin-top:var(--block)}
@media(min-width:680px){.tgrid{grid-template-columns:repeat(2,1fr)}}
@media(min-width:1080px){.tgrid{grid-template-columns:repeat(3,1fr)}}

.tcard{
  margin:0;display:flex;flex-direction:column;gap:1rem;
  background:var(--q-surface, #E2EDFA);
  border:1px solid var(--q-line, #AFCBE9);
  border-radius:var(--radius);padding:var(--card);
  transition:border-color var(--dur-fast) var(--ease);
}
.tcard:hover{border-color:var(--q-edge, #2A5C9E)}
.tcard blockquote{font-size:.9375rem;line-height:1.62;color:var(--q-mid, #40566B)}
.tcard figcaption{
  margin-top:auto;padding-top:1rem;border-top:1px solid var(--q-line, #AFCBE9);
  font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--q-mid, #40566B);font-weight:500;
}

/* About: every quote. Multi-column, not grid — lengths still run 128 to 469
   characters and columns flow that variance instead of stranding a short cell
   beside a tall one. break-inside stops a card splitting across a column. */
.twall{columns:1;column-gap:clamp(.9rem,1.6vw,1.4rem)}
@media(min-width:680px){.twall{columns:2}}
@media(min-width:1080px){.twall{columns:3}}
.tcard--wall{break-inside:avoid;margin-bottom:clamp(.9rem,1.6vw,1.4rem)}

/* Zero-height marker at the top of <main>. app.js observes it to decide when
   the nav leaves the banner and takes its plate. Occupies no layout: the
   negative margin cancels its own height. */
.nav-sentinel{display:block;height:1px;margin-bottom:-1px;pointer-events:none;visibility:hidden}

/* Visually hidden, still read aloud. Used where a section needs a heading for
   document structure but the design deliberately has none on screen. */
.vh{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
