/* ═══════════════════════════ FONTS ═══════════════════════════
   Formerly assets/css/fonts.css, merged here 2026-08-14: two render-blocking
   stylesheets made a two-link critical chain and the fonts file was 1.7KB —
   one request fewer beats the tidiness of a separate file. */
/* Brand fonts, self-hosted (latin subset).
   Bitcount Mono Single (TYPETR, SIL OFL) + DM Mono are the real brand faces.
   Neue Haas Unica is the licensed body face, subset from the supplied W1G TTFs. */
@font-face {
  /* Bitcount Mono Single, Regular Square — the exact cut the designer specified
     on 2026-08-13 ("not the medium, not the single bold... specifically single
     regular square"). Licensed from TYPETR under the SIL OFL (see
     Bitcount-SIL-OFL.html alongside), so it is self-hosted like the others
     rather than loaded from Adobe's CDN.

     This is a STATIC cut, not the variable family — so no font-variation-settings
     anywhere. The square element shape is baked into the file. */
  font-family: 'Bitcount Mono Single';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/BitcountMonoSingle-RegularSquare.ce2abe02.woff2') format('woff2');
}
@font-face {
  font-family: 'DM Mono';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/DMMono-400-italic.4e8273ae.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('/assets/fonts/DMMono-300-normal.e128450f.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/DMMono-400-normal.8beac5b8.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/DMMono-500-normal.0a2d0f89.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Neue Haas Unica';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/NeueHaasUnica-400-italic.efc4ed6b.woff2') format('woff2');
}
@font-face {
  font-family: 'Neue Haas Unica';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/NeueHaasUnica-400-normal.36eb0111.woff2') format('woff2');
}
@font-face {
  font-family: 'Neue Haas Unica';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/NeueHaasUnica-500-normal.1fcec089.woff2') format('woff2');
}
@font-face {
  font-family: 'Neue Haas Unica';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/assets/fonts/NeueHaasUnica-700-normal.c7bc2099.woff2') format('woff2');
}

/* ═══════════════════════════ TOKENS ═══════════════════════════ */
:root {
  /* palette — sampled from MASRI_HOME.pdf */
  --ink:            #2e374c;
  --ink-deep:       #252d3d;
  --ink-black:      #040c14;
  --muted:          #3d4354;
  --sage:           #abc0b9;   /* dark backgrounds only — 1.5:1 on the light bands */
  --sage-ink:       #4d6d5f;   /* same hue, darkened for light backgrounds (4.4:1+) */
  --lavender:       #adbaf5;   /* dark backgrounds only — 1.9:1 on white */
  --lavender-ink:   #5f6cbb;   /* same hue, darkened for light backgrounds (4.8:1) */
  --mint:           #bddcd9;
  --mint-light:     #c8e5e0;
  --pale-lavender:  #dfe7f9;
  --pale-sage:      #ebefeb;
  --card:           #2d364b;
  --white:          #fff;

  /* type — all three are the real brand faces, self-hosted. */
  /* Typography per the designer's website review (Propaganda monthly, 2026-08-13):
     "Headline text and body should be DM Mono, use the pixel font for small
     button text" and "the main headline should also be the regular DM Mono —
     it shouldn't be this pixelated". So DM Mono carries display, headings AND
     body; Bitcount Mono Single is demoted to small button text only.
     He also asked for tighter tracking on DM Mono — hence --track-tight,
     applied wherever DM Mono is set at size. */
  --font-display: "DM Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-mono:    "DM Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  /* Designer 2026-08-24: body moves to the clean sans serif (Neue Haas Unica).
     Reverses the 8/13 all-DM-Mono call — DM Mono keeps display and mono
     accents, NHU carries body copy. */
  --font-body:    "Neue Haas Unica", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  /* Bitcount Mono Single Regular Square — buttons and nav only. */
  /* Bitcount Mono Single, Regular Square — licensed from TYPETR (SIL OFL) and
     self-hosted. This is the exact cut the designer specified; the Google
     "Bitcount Single" stand-in is gone. Static cut, so no variation axes. */
  --font-pixel:   "Bitcount Mono Single", ui-monospace, monospace;
  /* Kept as the fallback stack only; Neue Haas Unica is no longer set on the
     site (see --font-body). Removing the @font-face would strand the file, so
     the token stays until the designer confirms NHU has no web role. */
  --font-sans:    "Neue Haas Unica", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --track-tight:  -.022em;

  /* layout */
  --shell:    1380px;
  --gutter:   clamp(20px, 4.4vw, 72px);
  --header-h: 88px;

  --ease: cubic-bezier(.22, .61, .36, 1);
}

/* ═══════════════════════════ RESET ═══════════════════════════ */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* Anchor targets must clear the fixed header, or every in-page jump
   lands with its heading hidden behind the bar. */
:target,
#main, #about, #services, #results, #approach, #industries, #contact {
  scroll-margin-top: calc(var(--header-h) + clamp(12px, 2.5vw, 40px));
}

body {
  margin: 0;
  font-family: var(--font-body);
  letter-spacing: 0; /* sans body wants natural spacing */
  font-size: clamp(15px, 1.15vw, 17px);
  line-height: 1.62;
  color: var(--ink);
  background: var(--pale-sage);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  /* Double-tap zoom, off. The layout is already sized to the viewport, so a
     double tap only ever fired by accident and zoomed to whatever block was
     under the finger — worst around the header, where taps cluster.
     `manipulation` drops the double-tap gesture and the 300ms tap delay that
     rides along with it. Panning and PINCH zoom are untouched, so WCAG 1.4.4
     still passes. Do NOT swap this for user-scalable=no or maximum-scale=1 in
     the viewport meta: those kill pinch zoom and fail the same criterion. */
  touch-action: manipulation;
}

img, svg { display: block; max-width: 100%; }
img { height: auto; }
h1, h2, h3, p, dl, dd, figure, blockquote { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }
button, input, textarea { font: inherit; color: inherit; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--white); color: var(--ink);
  padding: 12px 20px; border-radius: 0 0 8px 0; font-family: var(--font-mono);
}
.skip-link:focus { left: 0; }

:where(a, button, input, textarea):focus-visible {
  outline: 2px solid var(--lavender);
  outline-offset: 3px;
  border-radius: 4px;
}

main:focus { outline: none; }

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* ═══════════════════════════ TYPE ═══════════════════════════ */
.h2 {
  font-family: var(--font-mono);
  /* Website plan 2026-10-19 #1 (needs designer sign-off): two H2 tiers so the
     page has a hierarchy, at Regular weight instead of the 2026-08-24 Light.
     Key sections add .h2--key; the final CTA title is sized the same below. */
  font-weight: 400;
  font-size: clamp(26px, 2.8vw, 40px);
  text-wrap: balance;
  line-height: 1.42;
  letter-spacing: var(--track-tight);
  color: var(--ink);
}
.h2--center { text-align: center; }
.h2--key { font-size: clamp(30px, 3.6vw, 52px); }
.h2 strong { font-weight: 500; } /* DM Mono tops out at 500: no synthetic bold */

/* On light sections sage must be the darkened variant, or it is unreadable. */
.is-sage     { color: var(--sage-ink); }
.is-lavender { color: var(--lavender-ink); }
.is-muted    { color: var(--muted); }

/* the pale sage is only legible against the dark bands */
.services .is-sage,
.testimonials .is-sage { color: var(--sage); }
.band-dark .is-lavender { color: var(--lavender); }

.link {
  font-family: var(--font-mono);
  color: var(--sage-ink);
  transition: color .2s var(--ease), opacity .2s var(--ease);
}
.link--underline { text-decoration: underline; text-underline-offset: 5px; }
.link:hover { color: var(--ink); }
.link--sage:hover { color: var(--ink); }

.chevron-list li { position: relative; padding-left: 1.1em; }
.chevron-list li::before {
  content: ">";
  position: absolute; left: 0; top: 0;
  color: inherit; opacity: .85;
}

/* ═══════════════════════════ BUTTONS ═══════════════════════════ */
.btn {
  --btn-bg: var(--white);
  --btn-fg: var(--ink);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--font-pixel);
  -webkit-font-smoothing: auto;
  font-size: clamp(13px, 1vw, 15px);
  letter-spacing: .04em;
  text-transform: uppercase; /* Bitcount buttons always shout (designer 2026-08-24) */
  /* Asymmetric on purpose: Bitcount's glyphs sit high in their line box, so a
     centred line box reads as text riding high in the pill (designer, 8/14).
     Same 28px vertical total, biased 2px down. */
  padding: 16px 30px 12px;
  border: 0; border-radius: 999px;
  background: var(--btn-bg); color: var(--btn-fg);
  cursor: pointer;
  transition: transform .22s var(--ease), box-shadow .22s var(--ease), background .22s var(--ease);
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 10px 26px rgba(20, 28, 44, .22); }
.btn:active { transform: translateY(0); }
.btn--lg   { padding: 19px 38px 15px; }
.btn--dark { --btn-bg: var(--ink); --btn-fg: var(--white); }
.btn--mint { --btn-bg: var(--mint); --btn-fg: var(--ink); }

/* ═══════════════════════════ HEADER ═══════════════════════════ */
.site-header {
  position: fixed; inset: 0 0 auto 0; z-index: 100;
  transition: background .3s var(--ease), box-shadow .3s var(--ease);
}
.site-header.is-stuck {
  /* No backdrop-filter. The header is fixed and full-width, so blurring here
     made the browser re-blur everything passing behind it on EVERY scroll
     frame — the biggest single source of scroll jank on the page.
     With no blur the ground has to be opaque. At .93 the 7% that got through
     was not a tint, it was legible body copy sliding under the wordmark. */
  background: #2e374c;
  box-shadow: 0 1px 0 rgba(255, 255, 255, .08);
}
.header__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 24px; height: var(--header-h);
}

/* Official lockup, inlined once as a sprite so `currentColor` still applies. */
.svg-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

.brand { display: inline-flex; align-items: center; color: var(--white); }
.brand__logo {
  height: clamp(22px, 1.9vw, 28px); /* designer 2026-08-24: smaller top logo */
  width: auto;
  fill: currentColor;
}
.brand--lg .brand__logo { height: clamp(32px, 3vw, 42px); }

.nav { display: flex; align-items: center; gap: clamp(20px, 3vw, 46px); }
.nav__link {
  font-family: var(--font-pixel);
  -webkit-font-smoothing: auto;   /* the body's `antialiased` thins the bits */
  letter-spacing: .04em;
  font-size: 15px; color: var(--white);
  opacity: .92; transition: opacity .2s var(--ease);
}
.nav__link:hover { opacity: 1; text-decoration: underline; text-underline-offset: 6px; }

/* Website plan #42: on phones the nav (and its Get in Touch) folds into the
   menu, so a compact CTA stays in the sticky header. Desktop keeps the nav. */
.header__cta { display: none; white-space: nowrap; }
@media (max-width: 380px) {
  .header__inner .header__cta { padding: 10px 13px 7px; font-size: 11px; letter-spacing: .02em; }
  .header__inner { gap: 14px; }
}

.nav-toggle {
  display: none;
  width: 44px; height: 44px; padding: 0;
  background: none; border: 0; cursor: pointer;
}
.nav-toggle span {
  display: block; width: 24px; height: 2px; margin: 5px auto;
  background: var(--white); transition: transform .25s var(--ease), opacity .25s var(--ease);
}
/* Bars sit 10px apart centre-to-centre (2px bar + 5px margins), so each one
   travels 5px to meet in the middle. 7px overshot and left the X split by 4px. */
.nav-toggle[aria-expanded="true"] span:first-child { transform: translateY(5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:last-child  { transform: translateY(-5px) rotate(-45deg); }
.nav-toggle:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; border-radius: 8px; }

.mobile-nav {
  /* A disclosure, not a modal (8/16). The panel hangs off the bottom of the
     header and is only as tall as its own links, so the page behind stays
     visible and keeps scrolling. That removes the whole modal apparatus: no
     body scroll lock, no focus trap, no inert siblings, no dvh arithmetic.
     Opaque with a hairline and a drop shadow so it reads as a solid object.
     The 8/14 complaint was never the drop-down itself, it was a translucent
     strip floating over a page that kept moving underneath it. */
  position: absolute; top: 100%; left: 0; right: 0;
  display: flex; flex-direction: column; gap: 0;
  /* Safety valve for short landscape screens: the panel scrolls itself rather
     than running past the bottom of the viewport. */
  max-height: calc(100dvh - var(--header-h) - 16px);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 4px var(--gutter) 20px;
  background: #2e374c;
  border-top: 1px solid rgba(255, 255, 255, .14);
  box-shadow: 0 18px 40px rgba(18, 26, 42, .38);
  /* The hamburger animated over 250ms while the panel snapped in with
     display:none. Both move together now. main.js drops [hidden] on boot
     and drives .is-open, so no-JS still gets the [hidden] fallback. */
  visibility: hidden; opacity: 0; transform: translateY(-10px);
  transition: opacity .22s var(--ease), transform .22s var(--ease), visibility 0s linear .22s;
}
.mobile-nav.is-open {
  visibility: visible; opacity: 1; transform: none;
  transition: opacity .22s var(--ease), transform .22s var(--ease), visibility 0s;
}
.mobile-nav[hidden] { display: none; }
.mobile-nav__link {
  font-family: var(--font-pixel);
  -webkit-font-smoothing: auto;
  letter-spacing: .04em;
  color: var(--white); font-size: 19px;
  /* Row-sized tap targets, not bare text lines: 56px tall each. The old
     gap:6px sat between the rows and chopped the divider into segments. */
  display: flex; align-items: center; min-height: 56px;
  padding: 14px 4px;
  border-bottom: 1px solid rgba(255, 255, 255, .12);
}
.mobile-nav__link:last-of-type { border-bottom: 0; }
.mobile-nav .btn { align-self: stretch; margin-top: 18px; min-height: 52px; }
/* At page top the header is transparent; a panel hanging from an invisible bar
   looked detached. Opening the menu makes the bar itself solid. */
/* Matches .mobile-nav exactly. A 97% bar above a 100% panel showed as a seam. */
.site-header.is-open { background: #2e374c; }
/* No scroll lock. The panel no longer owns the screen, so the page behind it
   stays scrollable and the chat launcher (bottom-right) never collides with
   it (top-anchored). Both workarounds retired with the modal. */

/* ═══════════════════════════ HERO ═══════════════════════════ */
.hero {
  position: relative;
  padding-top: var(--header-h);
  /* Holds the ink to ~40% so the lede is still on dark; the pale end is
     compressed into the last 15%, which is where the logo carousel sits. */
  background: linear-gradient(180deg,
    #2e374c 0%, #2e374c 16%, #3c4555 30%, #525b68 44%,
    #707884 58%, #959da0 71%, #bcc4c0 83%, #dde3de 92%, #ebefeb 100%);
}
.hero__grid-overlay,
.pillars__grid-overlay {
  position: absolute; inset: 0; pointer-events: none;
  background-image: radial-gradient(circle, currentColor 1px, transparent 1.4px);
  background-size: 76px 76px;
  color: rgba(255, 255, 255, .16);
  mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
}
.pillars__grid-overlay { color: rgba(46, 55, 76, .14); mask-image: linear-gradient(180deg, #000 0%, transparent 62%); }

.hero__inner {
  position: relative; z-index: 1;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr);
  align-items: center; gap: clamp(24px, 4vw, 64px);
  min-height: clamp(440px, 58vh, 640px);
  padding-block: clamp(22px, 2.8vw, 44px) clamp(16px, 2vw, 30px);
}
.hero__title {
  /* Was Bitcount with its square-bit variation axes. The designer was
     explicit on 2026-08-13: "the main headline should also be the regular DM
     Mono — it shouldn't be this pixelated." */
  font-family: var(--font-display);
  font-weight: 300; /* designer 2026-08-24 */
  font-size: clamp(34px, 4.35vw, 63px);
  line-height: 1.24;
  letter-spacing: var(--track-tight);
  color: var(--white);
  margin-bottom: clamp(16px, 2.2vw, 32px);
}
.hero__lede {
  /* 42ch, not 30ch: the three-service line is 39 characters and now sets its
     own line, so the measure has to hold it. */
  max-width: 42ch;
  text-wrap: pretty;
  color: rgba(255, 255, 255, .94);
  font-size: clamp(15px, 1.25vw, 19px);
  margin-bottom: clamp(20px, 2.6vw, 36px);
}
/* The three services and the closing line are two separate beats. Run together
   at one weight the reader had to parse where the list ended. DM Mono ships
   300/400/500, so 500 is the top of the range: heavier than the tail, and no
   synthetic bold. <strong> would otherwise resolve to 700 and get faked. */
.hero__lede-lead {
  display: block;
  font-weight: 500;
  color: var(--white);
  margin-bottom: .3em;
}
.hero__note { margin-top: 14px; font-size: 14px; color: rgba(255, 255, 255, .82); }
.hero__art { margin-right: calc(var(--gutter) * -.5); position: relative; }
.hero__art img { width: 100%; filter: drop-shadow(0 30px 60px rgba(18, 26, 42, .28)); }
/* Duplicate of the art that exists only to carry the drop-shadow: it rasters
   ONCE (static filter) and the build fades its opacity in, where animating
   drop-shadow on the img re-rastered the whole art every frame. With JS the
   img's own shadow moves here permanently; without JS this stays invisible
   and the img keeps its base shadow. */
.hero__shadow {
  position: absolute; inset: 0;
  background-image: url("/assets/img/hero-mark-1400.webp");
  background-size: 100% 100%;
  filter: drop-shadow(0 30px 60px rgba(18, 26, 42, .28));
  opacity: 0;
  pointer-events: none;
}

/* ── Ambient video ─────────────────────────────────────────────────────
   Section stills upgrade to a quietly moving version. The still stays the base
   layer and the video fades in only once it is genuinely playing, so a slow
   connection, blocked autoplay or a decode failure all degrade to exactly the
   design that was already there.

   CRITICAL: each video MUST inherit whatever mask its still carries. Shipping
   one without a mask rendered it as a hard-edged rectangle sitting on top of
   the background instead of being part of it. */
.amb { position: relative; }
.amb__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 1.1s var(--ease);
  pointer-events: none;
}
.amb.is-playing .amb__video { opacity: 1; }
/* Each of these mirrors its still exactly — same feather, same flip. */
.industries__media .amb__video {
  -webkit-mask-image: radial-gradient(72% 78% at 76% 48%, #000 34%, transparent 100%);
          mask-image: radial-gradient(72% 78% at 76% 48%, #000 34%, transparent 100%);
}
.services__banner .amb__video {
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, .22) 14%, #000 34%);
          mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, .22) 14%, #000 34%);
  transform: scaleX(-1);
  object-position: 50% 72%;
}
/* The still keeps drifting underneath as the fallback; the video does not need
   it, so it is suppressed once the video is up. */
.amb.is-playing img { animation: none; }

/* ── Header mark: periodic re-build ────────────────────────────────────
   The mark stays alive up top, but PERIODICALLY rather than continuously: it
   sits in a position:fixed bar, and anything animating every frame there
   repaints the header over whatever is scrolling behind it — the same class of
   cost as the backdrop-filter removed earlier for scroll jank.

   A signal sweeps through the glyph every 9s (and on hover/focus), masked to
   the mark itself so it reads as current running through the grid. The element
   is ~120x20px so the mask is free at this size, and between pulses nothing is
   animating at all. */
.site-header .brand__logo {
  -webkit-mask-image: linear-gradient(100deg,
    #000 0%, #000 38%, rgba(0,0,0,.3) 47%, #000 56%, #000 100%);
          mask-image: linear-gradient(100deg,
    #000 0%, #000 38%, rgba(0,0,0,.3) 47%, #000 56%, #000 100%);
  -webkit-mask-size: 280% 100%;
          mask-size: 280% 100%;
  -webkit-mask-position: 120% 0;
          mask-position: 120% 0;
}
.js .site-header .brand__logo { animation: markPulse 9s linear infinite; }
.site-header .brand:hover .brand__logo,
.site-header .brand:focus-visible .brand__logo { animation: markPulse 1.1s linear; }

@keyframes markPulse {
  /* One sweep, then a long flat tail — real work happens in ~12% of the cycle. */
  0%   { -webkit-mask-position: 120% 0; mask-position: 120% 0; }
  12%  { -webkit-mask-position: -60% 0; mask-position: -60% 0; }
  100% { -webkit-mask-position: -60% 0; mask-position: -60% 0; }
}

/* ── Hero build: the logo assembles from its own pieces ─────────────────
   The glass render, cut into its module tiles by main.js, pops in raked
   left-to-right; when the last tile lands the logo simply IS there — the
   real <img> fades on invisibly underneath (pixel-identical mosaic) and the
   pre-rastered shadow underlay blooms. The lattice rules that used to rake
   over the build are gone: over the finished tiles they read as lines
   scribbled across the logo (user, 8/14). Without JS nothing here renders
   and the img stands alone with its own shadow. */
/* The render's own tiles: positions, sizes, and background slices are all
   set inline by main.js (measured pixels, since background-position
   percentages don't compose with fractional geometry). 123 occupied tiles,
   computed offline from the image's alpha map — every visible pixel of the
   render falls inside one, so the finished mosaic is pixel-identical to the
   <img> beneath it. */
/* The tiles live OUTSIDE the masked .hero__swoosh (main.js appends this to
   .hero directly): 123 animating layers inside a hero-sized masked subtree
   forced the compositor to re-mask everything every frame — measured 160 to
   370ms frame gaps through the build. Out here each tile is a small
   independent layer animating only transform and opacity. No will-change:
   pre-promoting 123 image-slice layers at page load was its own cost. */
.hero__swoosh-tiles { position: absolute; pointer-events: none; }
.hero__swoosh-tiles b {
  position: absolute;
  background-repeat: no-repeat;
  opacity: 0;
}

.js .hero__art img { opacity: 0; }

/* Timeline (~2s total): tiles rake in .15-.85s and the mosaic is whole by
   ~1.1s; the real <img> fades on at 1.4s underneath (invisible: the tiles
   above are pixel-identical) while the shadow underlay blooms, then the
   tiles fade over the finished logo. Everything animates transform/opacity
   only.

   The art container gets NO entrance animation under JS: floatIn kept the
   box translated 46px down at 96% scale while the tiles were being measured,
   so the mosaic assembled in the displaced spot and the real img then
   appeared in the final one — the logo visibly jumped after the build. The
   tile assembly IS the entrance now. */
.js .hero__art { animation: none; }
.js .hero__swoosh-tiles b {
  animation: swooshTileIn .34s cubic-bezier(.25,.8,.3,1) forwards;
  animation-delay: var(--d, .3s);
}
/* The img carries no shadow under JS — the underlay owns it permanently. */
/* The instant the last tile settles (~1.35s) the real img comes on and the
   tiles leave — holding the mosaic longer just parks its boundaries over the
   finished logo (user, 8/14: "get rid of the line overlay completely"). */
.js .hero__art img { filter: none; animation: swooshMark .25s ease 1.38s forwards; }
.js .hero__shadow { animation: swooshMark .4s ease 1.42s forwards; }
.js .hero__swoosh-tiles { animation: swooshFade .35s ease 1.5s forwards; }

/* Arrival by scale-DOWN: a tile growing INTO place leaves dark slivers of
   background between it and its settled neighbours until it lands — inside
   the forming M they read as black grid lines (user, 8/14). Shrinking from
   oversize overlaps the neighbours instead, so the mosaic never gaps. */
@keyframes swooshTileIn {
  from { opacity: 0; transform: scale(1.3); }
  to   { opacity: 1; transform: none; }
}
@keyframes swooshMark { to { opacity: 1; } }
@keyframes swooshFade { to { opacity: 0; } }

/* scaleX(-1) is part of the design and must ride along in every keyframe.
   Deliberately small: a wider range pulled the image's bright horizon into the
   bottom of the crop and showed as a pale band against the dark section. */
@keyframes chessDrift {
  from { transform: scale(1.03) translate3d( 0.5%, 0.3%, 0); }
  to   { transform: scale(1.08) translate3d(-0.6%, -0.4%, 0); }
}
@keyframes duelDrift {
  from { transform: scaleX(-1) scale(1.04) translate3d(-0.4%, 0.15%, 0); }
  to   { transform: scaleX(-1) scale(1.08) translate3d( 0.4%, -0.15%, 0); }
}


/* client logo strip */
.logo-strip { position: relative; z-index: 1; padding-bottom: clamp(20px, 2.4vw, 38px); }
/* ── carousel ──────────────────────────────────────────────────
   Two identical tracks scroll as one; translating the pair by exactly
   -50% lands copy 2 where copy 1 started, so the loop is seamless. The
   second copy is aria-hidden and its links are unfocusable. */
.logo-marquee {
  position: relative;
  overflow: hidden;
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.logo-marquee__inner {
  display: flex;
  width: max-content;
  animation: marquee 46s linear infinite;
  will-change: transform;
}
.logo-marquee:hover .logo-marquee__inner,
.logo-marquee:focus-within .logo-marquee__inner { animation-play-state: paused; }

.logo-marquee__track {
  display: flex; align-items: center;
  gap: clamp(38px, 5vw, 84px);
  padding-right: clamp(38px, 5vw, 84px);
  flex: none;
}
.logo-marquee__track > li { flex: none; }

@keyframes marquee { to { transform: translateX(-50%); } }

/* Real client marks arrive in five different colours and aspect ratios.
   brightness(0) flattens each to the ink tone; the height tiers even out
   the optical weight of a stacked lockup vs a 10:1 wordmark. */
.logo-strip__logo {
  --logo-h: clamp(24px, 2.4vw, 32px);
  display: flex; align-items: center; justify-content: center;
  height: clamp(46px, 4.6vw, 66px);
  filter: brightness(0);
  opacity: .58;
  transition: opacity .25s var(--ease), transform .25s var(--ease);
}
.logo-strip__logo:hover { opacity: .95; transform: translateY(-2px); }
.logo-strip__logo.is-stacked { --logo-h: clamp(42px, 4.2vw, 60px); }
.logo-strip__logo.is-wide    { --logo-h: clamp(15px, 1.5vw, 21px); }
/* A lockup that is a filled block rather than line-art carries far more optical
   weight at the same height, so it gets its own smaller, lighter tier. */
/* A filled crest carries far more optical weight than line-art at the same
   height — smaller tier and lighter still (design audit, 8/14). */
.logo-strip__logo.is-dense   { --logo-h: clamp(26px, 2.6vw, 36px); opacity: .32; }
.logo-strip__logo.is-dense:hover { opacity: .66; }

.logo-strip__logo :is(img, svg) {
  max-height: var(--logo-h);
  max-width: 100%;
  width: auto; height: auto;
  object-fit: contain;
}
.logo-strip__logo svg { height: var(--logo-h); color: var(--ink); }

/* ═══════════════════════════ PILLARS ═══════════════════════════ */
.pillars {
  position: relative;
  padding-block: clamp(16px, 2vw, 32px) clamp(8px, 1vw, 16px);
  background: linear-gradient(180deg,
    #ebefeb 0%, #fdfdfd 22%, #fefefe 30%,
    #f3f5f6 46%, #e7ebed 66%, #dcdfe2 86%, #dadfe1 100%);
}
.pillar {
  display: grid; align-items: center;
  gap: clamp(20px, 4vw, 70px);
  padding-block: clamp(6px, 0.8vw, 12px);
}
.pillar--media-left {
  /* The art carries this row, so it takes the larger column and a tighter
     gutter. At .72fr it rendered 400px inside a 501px column and read as a
     small picture marooned in white. */
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: clamp(28px, 5vw, 72px);
}
.pillar--stacked    { grid-template-columns: minmax(0, 1fr); max-width: 900px; margin-inline: auto;
                      position: relative; z-index: 2; }
.pillar--stacked .pillar__body { max-width: 66ch; }

.pillar__media img {
  /* 900x1352 art. Capping its HEIGHT letterboxed it inside a much wider column,
     so it read as small; capping the column and letting it fill at its natural
     ratio gives the same section height budget with a far larger image. */
  width: 100%;
  /* 15% smaller than the natural column fit, per the user (8/14). */
  max-width: clamp(255px, 44vw, 629px);
  margin-inline: auto;
  /* Alive like the duel and chess shots (user, 8/14). Tinier range than
     theirs: this art has soft transparent edges, and a wide drift visibly
     slides them against the band. Suppressed by .amb.is-playing once the
     real loop is up, like the other stills. */
  animation: threatsDrift 44s ease-in-out infinite alternate;
  will-change: transform;
  transition: opacity 1.1s var(--ease);
}
@keyframes threatsDrift {
  from { transform: scale(1.02) translate3d(-0.3%, 0.2%, 0); }
  to   { transform: scale(1.055) translate3d(0.35%, -0.25%, 0); }
}
/* The threats loop. Generated from a capture of the art on its RENDERED page
   ground (band gradient + grid dots baked in, no alpha — the old clip's dark
   vignette was our alpha-carrying upload flattening onto black at the
   generator, not the model), then cropped back to the img's exact box. It
   still cannot just cover full-bleed: the img sits centered inside a wider
   grid column, so the video is sized to that same box. The feather is only
   insurance for the baked ground drifting from the live band gradient at
   other viewport sizes — it starts outside the art. The still hands off
   completely while playing (the art moves; layering ghosts). */
.pillar__media .amb__video {
  left: 0; right: 0;
  width: 100%; max-width: clamp(255px, 44vw, 629px);
  margin-inline: auto;
  height: 100%;
  object-fit: cover;
  -webkit-mask-image: radial-gradient(75% 75% at 50% 50%, #000 66%, transparent 98%);
          mask-image: radial-gradient(75% 75% at 50% 50%, #000 66%, transparent 98%);
}
.pillar__media.is-playing img { opacity: 0; animation: none; }
.pillar__body { display: grid; gap: 18px; }
.pillar__body .chevron-list { display: grid; gap: 2px; }
.pillar__note { max-width: 54ch; text-wrap: pretty; }
.pillar__body .link { justify-self: start; }

/* ═══════════════════════════ SERVICES ═══════════════════════════ */
/* In the comp the banner is not a block below this text — it bleeds in from the
   right while the "unstoppable" copy is still running down the left, so there is
   no empty band between them at all. The stacked pillar is capped at 900px wide,
   so pulling the banner up occupies space that was empty anyway. */
/* Pull-up stays at its proven strength: pushing to -300px drove the banner
   box into the pillar copy once its background went opaque — the stronger
   bleed needs the designer's asset, not more overlap (user, 8/14). */
/* Overlap stays inside the video's faint entry fade: at -240px the pillar
   copy's ink text ran onto mid-strength fog and went muddy (8/14). */
.services {
  position: relative; background: #dadfe1;
  margin-top: clamp(-150px, -10.5vw, -60px);
  /* Near the footage's real aspect (2000x1493 ~ 74.6vw) so the scene shows
     whole, capped at a viewport; the copy sits at the BOTTOM of that frame
     ("move things down like 2 inch" — user, 8/14). Trimmed from 72vw/94vh:
     the frame's top third is empty fog, and at full aspect it opened a
     ~500px dead band between the pillar copy and the title (user, 8/14:
     "still not resolved"). The trim comes off the sky via object-position
     72% — lion and fighter keep today's floor clearance. On phones content
     outgrows the min-height and flex-end simply stops mattering. */
  min-height: min(66vw, 88vh);
  display: flex; flex-direction: column; justify-content: flex-end;
}
.services__banner {
  /* Full-bleed footage: the banner fills the WHOLE section behind the copy.
     The old layout stacked a fixed-height banner above an opaque ink body,
     which read as a razor cut between video and a slab of dark blue (user,
     8/14: "full video showing, less dark blue, the text can hover over it"). */
  position: absolute; inset: 0;
  line-height: 0;
  overflow: hidden;
}
.services__banner img {
  width: 100%; height: 100%; object-fit: cover;
  /* Near-full-frame: with the section at the footage's aspect there is no
     horizontal crop; whatever vertical trim the viewport cap forces comes
     off the empty sky, keeping lion and fighter whole (user, 8/14:
     "it's not the full vid"). */
  object-position: 50% 72%;
  animation: duelDrift 40s ease-in-out infinite alternate;
  will-change: transform;
  /* The entry fade must reach full strength BELOW the pillar copy that
     overlaps this section, not through it: at 12% the video hit full fog
     ~100px in, exactly where "With Masri..." and Learn More sit, slicing a
     visible fog edge through the ink text (user, 8/14: "still not
     resolved"). Full strength at 34% keeps that copy on a <=22% wash. */
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, .22) 14%, #000 34%);
          mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, .22) 14%, #000 34%);
}
/* Some of the dark blue back — a light ink wash over the frame, far from the
   old opaque slab. It MUST fade in with the video: a flat fill painted from
   the section's very top edge while the video beneath was still masked out,
   drawing a razor line straight through the overlapped pillar copy (user,
   8/14: "this looks horrid"). Same lesson as the banner ground before it:
   nothing opaque may start where a mask is still transparent. */
.services__banner::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg,
    rgba(46, 55, 76, 0) 10%, rgba(46, 55, 76, .18) 36%, rgba(46, 55, 76, .18) 100%);
}
.services__body {
  /* Sits ON the footage. The scrim is all the ink that remains: transparent
     through the video's entry fade, deepening only enough for white copy,
     with the pixel-anchored white hand-off at the bottom joint unchanged.
     The top padding clears the section's overlap into the pillars plus the
     video's entry fade before any text starts. */
  position: relative;
  /* Copy back down near the frame's floor, per the 8/14 dial-in rounds. */
  padding-block: clamp(130px, 10vw, 180px) clamp(120px, 8.5vw, 164px);
  /* Bottom pad must clear the 104px bottom-anchored light hand-off below —
     at 56-72px the "Explore all services" CTA sat ON the wash (audit
     2026-08-27), the desktop twin of the designer's mobile call-out. */
  /* Ink-BLUE scrim behind the copy. The title starts only ~23% into this
     body, and the previous ramp (.42 at 38%) left it on ~.25 ink over
     ~190-luma fog — measured 160 behind the glyphs, white text at ~2.3:1
     (user, 8/14: "still not resolved"). The ramp now front-loads: >=.5 by
     the title line, cards at .7+, floor at .9. */
  background:
    linear-gradient(to top,
      #fdfdfd 0, #ecefec 24px, #d3dcd7 56px, rgba(194,207,203,0) 104px),
    linear-gradient(180deg,
      rgba(46, 55, 76, 0) 0%, rgba(38, 47, 66, .5) 18%,
      rgba(28, 37, 56, .68) 42%, rgba(18, 26, 42, .8) 66%,
      rgba(12, 18, 32, .9) 100%);
  /* Insurance for white copy over the footage's bright fog patches. */
  text-shadow: 0 1px 14px rgba(4, 12, 20, .55);
}
.services__title { color: var(--white); }
.services__lede {
  max-width: 52ch; margin: clamp(14px, 1.7vw, 24px) auto 0;
  text-align: center; color: rgba(255, 255, 255, .88);
  text-wrap: pretty;
}
.service-cards {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(24px, 4vw, 64px);
  margin-top: clamp(18px, 2.2vw, 34px);
  text-align: center;
}
.service-card__title {
  font-family: var(--font-mono); font-weight: 500;
  font-size: clamp(18px, 1.7vw, 24px);
  color: var(--white); margin-bottom: 16px;
}
.service-card p { color: rgba(255, 255, 255, .92); max-width: 30ch; margin-inline: auto; text-wrap: pretty; }
.services__cta { margin-top: clamp(16px, 1.9vw, 28px); text-align: center; }
/* Each card links to its own section of /services. */
.service-card__link { display: inline-block; margin-top: 14px; color: rgba(255, 255, 255, .92); }
.service-card__link:hover { color: var(--white); }
/* The CTA now sits on the dark ground with the cards, not on the fade. */
.services__cta .link { color: rgba(255, 255, 255, .92); }
.services__cta .link:hover { color: var(--white); }

/* ═══════════════════════════ OUTCOMES + BETTER (one continuous band) ═══════ */
.band-outcomes {
  background:
    radial-gradient(74% 46% at 50% 64%,
      rgba(255,255,255,.96) 0%, rgba(252,253,251,.62) 42%, rgba(255,255,255,0) 76%),
    linear-gradient(180deg,
      #fff 0%, #fff 15%, #eceeed 21%, #d4d9d7 28%, #b6bbba 36%,
      #a1a5a6 44%, #96989a 52%, #9a9d9f 60%, #acb1b1 69%,
      #c4c9c9 79%, #e6e9e8 89%, #f8f9f9 97%, #fff 100%);
}

.outcomes {
  padding-bottom: clamp(12px, 1.6vw, 26px);
}
.outcomes__media { max-width: 1180px; margin: 0 auto; padding-inline: var(--gutter); }
.outcomes__media img { width: 100%; display: block; transition: opacity 1.1s var(--ease); }
/* The outcomes loop was generated from the still flattened onto pure white, at
   the still's exact 1600x696 aspect. The page behind the art is NOT white — a
   soft gray bloom (measured 196-229) — so the opaque frame would sit in a pale
   box. Multiply makes the white ground neutral: the ink art darkens whatever
   the page paints, which is how the transparent still composites. Only content
   BRIGHTER than the local page ground is lost to multiply — this art has none
   on white. The still hands off completely (art moves; layering ghosts). */
.outcomes__media .amb__video {
  left: var(--gutter); right: var(--gutter);
  width: auto;
  mix-blend-mode: multiply;
}
.outcomes__media.is-playing img { opacity: 0; }
.outcomes__title { margin-top: clamp(8px, 1.4vw, 20px); }
.stats {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(24px, 3vw, 48px);
  /* Desktop had the same squeezed proof block the designer's mobile pass
     fixed: 21px under a 40px title while the art above carried 150px of air
     (audit 2026-08-27). */
  margin-top: clamp(24px, 2.4vw, 44px);
  text-align: center;
}
.stat__value {
  font-family: var(--font-mono); font-weight: 500;
  /* The numbers are the proof; at caption scale the band had no spine
     (design audit, 8/14). */
  /* Website plan #3: the largest numbers on the page. "20 min" (not
     "20 Minutes") is what lets 64px fit four across. */
  font-size: clamp(40px, 4.4vw, 64px);
  line-height: 1.15;
  color: var(--ink-black); letter-spacing: .01em;
}
/* Mid-page CTA so a convinced reader does not have to scroll to the form. */
.cta-band {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 14px 24px; margin-top: clamp(28px, 3.4vw, 52px); text-align: center;
}
.cta-band__text { font-family: var(--font-mono); color: var(--ink); font-size: clamp(15px, 1.25vw, 19px); text-wrap: balance; }
.stat__label { margin: 10px 0 0; color: var(--muted); font-size: clamp(13px, 1.05vw, 15px); }

/* ═══════════════════════════ TECHNICALLY, BETTER ═══════════════════════════ */
.better {
  position: relative;
  padding-block: clamp(26px, 3.2vw, 50px) clamp(26px, 3.2vw, 50px);
}
.better__title { margin-bottom: clamp(8px, 1vw, 14px); }
.better__lede { text-align: center; color: var(--muted); margin-bottom: clamp(22px, 2.6vw, 40px); }
.features {
  display: grid; gap: clamp(18px, 2.2vw, 34px);
  max-width: 960px; margin-inline: auto;
}
.feature {
  display: grid; grid-template-columns: clamp(104px, 13vw, 200px) minmax(0, 1fr);
  align-items: center; gap: clamp(18px, 3vw, 52px);
}
.feature__icon {
  height: clamp(74px, 7.4vw, 112px);
  width: auto; max-width: 100%;
  object-fit: contain;
  justify-self: center;
}
.feature__title {
  font-family: var(--font-body); font-weight: 500;
  font-size: clamp(18px, 1.7vw, 24px);
  color: var(--ink); margin-bottom: 12px;
}
.feature__body p { color: #3a4457; max-width: 58ch; }
.better__cta { margin-top: clamp(22px, 2.6vw, 42px); text-align: center; }

/* ═══════════════════════════ INDUSTRIES ═══════════════════════════ */
.industries {
  position: relative; overflow: hidden;
  padding-block: clamp(28px, 3.4vw, 54px) clamp(34px, 4.2vw, 66px);
  background: linear-gradient(180deg,
    #fff 0%, #fff 24%, #f2f4fb 38%, #dfe7f9 50%, #d4dbec 60%,
    #bac3d6 70%, #98a0b4 80%, #6e778c 89%, #455069 95%, #2e374c 100%);
}
.industries__media {
  position: absolute; top: clamp(20px, 5vw, 90px); right: 0;
  width: min(66%, 1040px); pointer-events: none;
}
.industries__media img {
  width: 100%;
  mask-image: radial-gradient(72% 78% at 76% 48%, #000 34%, transparent 100%);
  /* Same treatment as the duel banner, for the same reason: four image-to-video
     attempts either produced no visible motion (0.047 mean frame delta) or
     re-staged the shot entirely — thick cloud, sharpened subject, wrong
     palette. A slow drift on the still cannot go off-model because it IS the
     still. */
  animation: chessDrift 46s ease-in-out infinite alternate;
  will-change: transform;
}
.industries__intro {
  position: relative; z-index: 1;
  max-width: 640px;
  padding-top: clamp(12px, 2vw, 34px);
  display: grid; gap: clamp(14px, 1.9vw, 26px);
}
.industries__intro p { color: var(--ink); }
.chips {
  position: relative; z-index: 1;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(12px, 1.6vw, 24px);
  margin-top: clamp(22px, 2.8vw, 44px);
}
.chips li {
  display: grid; place-items: center; text-align: center;
  min-height: clamp(64px, 6vw, 96px);
  padding: 14px 12px;
  font-family: var(--font-mono);
  font-size: clamp(12px, 1vw, 15px);
  color: var(--ink-deep);
  /* Twelve chips meant twelve backdrop-blur surfaces recompositing whenever
     this section was on screen. Raised the alpha instead of blurring. */
  background: rgba(255, 255, 255, .62);
  border: 1px solid rgba(255, 255, 255, .5);
  border-radius: 14px;
  transition: transform .22s var(--ease), background .22s var(--ease);
}
.chips li:hover { transform: translateY(-3px); background: rgba(255, 255, 255, .68); }

/* ═══════════ TESTIMONIALS + CONTACT (one continuous band) ═══════════ */
.band-dark {
  position: relative; overflow: hidden;
  background: var(--ink);
}

/* The dark-to-mint handover is anchored to this section's BOTTOM in pixels,
   which is exactly the testimonials/contact boundary. A percentage ramp on the
   shared band kept drifting as the two sections' heights changed with the
   viewport, putting the contact heading back on dark ground. The bottom padding
   is fixed for the same reason: the cards must always clear the pale end. */
.testimonials {
  position: relative;
  padding-block: clamp(28px, 3.4vw, 54px) 180px;
  background:
    linear-gradient(to top,
      #bddcd9 0, #acc9c6 42px, #88a6a7 90px, #547080 136px, rgba(19,31,52,0) 190px),
    linear-gradient(180deg, #2e374c 0%, #26314a 42%, #1a2740 74%, #131f34 100%);
}
.testimonials .shell { position: relative; z-index: 1; }
.testimonials__title { margin-bottom: clamp(24px, 3vw, 46px); }
/* Testimonials ride the same marquee as the client logos. Text needs far more
   time on screen than a logo mark does, so the duration is roughly half the
   speed — about 33px/s — and hover/focus pauses it the same way. */
.quote-marquee {
  position: relative;
  overflow: hidden;
  mask-image: linear-gradient(90deg, transparent, #000 4%, #000 96%, transparent);
}
.quote-marquee__inner {
  display: flex;
  width: max-content;
  animation: marquee 72s linear infinite;
  will-change: transform;
}
.quote-marquee:hover .quote-marquee__inner,
.quote-marquee:focus-within .quote-marquee__inner { animation-play-state: paused; }

/* Was a 3x2 grid. As a flex track the cards need an explicit width, because
   width:max-content on the rail would otherwise let each card size to its own
   text and the columns would visibly jitter in width as they scroll past. */
.quotes {
  display: flex; align-items: stretch;
  gap: clamp(20px, 2.6vw, 40px);
  padding-right: clamp(20px, 2.6vw, 40px);
  flex: none;
}
.quote {
  flex: none;
  width: clamp(288px, 25vw, 372px);
  display: flex; flex-direction: column;
  padding: clamp(24px, 2.6vw, 40px);
  border-radius: 12px;
  background: linear-gradient(160deg, rgba(255,255,255,.09), rgba(255,255,255,.03));
  border: 1px solid rgba(255, 255, 255, .07);
}
/* The <figure> carries the column, not the <li>. Wrapping the quote in a figure
   for HTML validity left the <li> with a single child, so space-between had
   nothing to distribute and the attributions stopped sitting on a common line. */
.quote > figure {
  flex: 1;
  display: flex; flex-direction: column; justify-content: space-between;
  gap: clamp(20px, 2.6vw, 38px);
}
/* Attributions share a bottom line; the quote itself centres in whatever
   space its card affords, so a three-line quote no longer hollows out the
   middle of a tall card (design audit, 8/14). */
.quote blockquote { margin-block: auto; }
.quote blockquote { color: var(--sage); font-size: clamp(13px, 1.05vw, 15px); line-height: 1.6; }
.quote__by { margin-top: auto; }
.quote__name { font-family: var(--font-mono); letter-spacing: var(--track-tight); color: var(--lavender); font-size: clamp(14px, 1.15vw, 17px); }

/* ═══════════════════════════ CONTACT ═══════════════════════════ */
.contact {
  position: relative;
  padding-block: clamp(12px, 1.6vw, 24px) clamp(32px, 4vw, 62px);
  /* Starts on the exact colour the section above ends on, so there is no seam. */
  background:
    radial-gradient(115% 85% at 58% 28%, rgba(216, 240, 235, .85) 0%, rgba(189, 220, 217, 0) 62%),
    linear-gradient(180deg, #bddcd9 0%, #bddcd9 56%, #b4d0cd 84%, #b0cbc8 100%);
}
.contact__title { font-size: clamp(30px, 3.6vw, 52px); max-width: 30ch; margin-inline: auto; }
.contact__lede { text-align: center; margin-top: clamp(14px, 1.8vw, 26px); color: var(--ink); }
.contact__grid {
  display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1fr);
  gap: clamp(28px, 5vw, 90px);
  margin-top: clamp(22px, 2.6vw, 42px);
  max-width: 1120px; margin-inline: auto;
}
.contact__details { display: grid; gap: 12px; align-content: start; padding-top: 8px; justify-items: start; }
/* The left column held two short lines against a tall form (audit, 8/14). */
.contact__addr {
  font-family: var(--font-mono);
  font-size: clamp(14px, 1.15vw, 17px);
  color: var(--ink-deep); opacity: .8;
  line-height: 1.75; margin-top: 6px;
}
.contact__line {
  font-family: var(--font-mono);
  font-size: clamp(16px, 1.5vw, 22px);
  color: var(--ink-deep);
  transition: opacity .2s var(--ease);
}
.contact__line:hover { opacity: .68; text-decoration: underline; text-underline-offset: 5px; }
/* The phone and email links are the page's two most important mobile actions
   but measured 26px tall; pad the hit area without moving the layout. */
a.contact__line { display: inline-block; padding-block: 9px; margin-block: -9px; }

.form { display: grid; gap: 14px; max-width: 520px; }
.field { display: grid; }
.field__label {
  font-size: 12px; color: #3f4a56; margin-bottom: 6px;
  padding-left: 4px; opacity: 0; height: 0; overflow: hidden;
}
/* Float label: the placeholder is the only visible label, so the moment a
   user types the field loses all identification. Reveal the real label while
   the field is focused or holds a value. */
.field:focus-within .field__label,
.field:has(.field__input:not(:placeholder-shown)) .field__label {
  opacity: 1; height: auto;
}
.field__input {
  width: 100%;
  padding: 16px 22px;
  /* 16px, not 15: mobile Safari auto-zooms on focus of any input under 16px
     and leaves the page zoomed after blur. */
  font-size: 16px;
  color: var(--ink);
  background: rgba(255, 255, 255, .42);
  border: 1px solid rgba(255, 255, 255, .5);
  border-radius: 12px;
  transition: background .2s var(--ease), border-color .2s var(--ease);
}
.field__input::placeholder { color: #4b5560; opacity: .8; }
.field__input:focus { background: rgba(255, 255, 255, .72); border-color: rgba(255, 255, 255, .9); }
.field__input--area { min-height: 160px; resize: vertical; border-radius: 14px; }
.field__input[aria-invalid="true"] { border-color: #b4453f; background: rgba(255, 236, 234, .6); }
.field__error { margin: 6px 0 0 4px; font-size: 12.5px; color: #8c2f2a; }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.form__turnstile { margin-top: 2px; min-height: 0; overflow-x: auto; }
.form .btn { justify-self: start; margin-top: 6px; min-width: 200px; }
.form__status { margin: 4px 0 0; font-size: 14px; min-height: 1.4em; }
.form__status.is-error { color: #8c2f2a; }
.form__status.is-ok    { color: #1f5b4a; }
/* Sets the visitor's expectation for a reply, right under the button. */
.form__reply { font-size: 14px; color: var(--muted); }
/* Invisible Turnstile: required privacy disclosure near SEND. */
.form__privacy {
  margin: 2px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
  max-width: 48ch;
}
.form__privacy a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.form__privacy a:hover { color: var(--ink); }

/* ═══════════════════════════ FOOTER ═══════════════════════════ */
/* The comp's footer keeps its mint top most of the way down, which leaves the
   contact block sitting on ~#9eb7b7 at ~1.1:1. The ramp darkens faster here so
   every line of footer copy clears 4.5:1. */
.site-footer {
  padding-block: clamp(32px, 4vw, 62px) clamp(32px, 4vw, 60px);
  background: linear-gradient(180deg,
    #b0cbc8 0%, #97adb0 9%, #718590 18%, #566672 30%,
    #45515e 45%, #38414c 62%, #2a313a 80%, #22252a 100%);
  /* No border-top. A gradient is sized to the PADDING box but painted into the
     BORDER box, and background-repeat defaults to repeat — so a 1px top border
     showed the tail of the next tile (#22252a, the gradient's darkest stop)
     rather than the 12% tint it asked for, drawing a near-black hairline across
     a seam whose two sides are otherwise identical (#b0cbc8 above, #afcac7
     below). The bands are meant to flow continuously; nothing to separate. */
  color: rgba(255, 255, 255, .95);
}
.site-footer__inner {
  display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(32px, 5vw, 72px);
}
.site-footer__brand { display: grid; gap: clamp(20px, 2.6vw, 34px); justify-items: start; }
.site-footer__brand .brand { color: var(--white); }
.socials { display: flex; gap: 16px; }
.socials a {
  display: grid; place-items: center; width: 38px; height: 38px;
  color: rgba(255, 255, 255, .82); transition: color .2s var(--ease), transform .2s var(--ease);
}
.socials svg { width: 38px; height: 38px; }
.socials a:hover { color: var(--white); transform: translateY(-2px); }

.site-footer__heading {
  font-family: var(--font-mono); font-weight: 400;
  font-size: clamp(14px, 1.15vw, 17px);
  color: var(--white); margin-bottom: 20px;
}
.site-footer__address { font-style: normal; display: grid; gap: 2px; }
.site-footer__address a, .site-footer__links a, .site-footer__address span {
  font-family: var(--font-mono); font-size: clamp(13px, 1.05vw, 15px); line-height: 1.7;
}
/* Bare 13px lines were ~22px tap targets, under the WCAG 2.5.8 24px floor —
   real mistap risk between the three stacked legal links on phones. */
.site-footer__address a, .site-footer__links a { display: inline-block; padding-block: 4px; }
.site-footer__links { display: grid; gap: 8px; }
.site-footer__links a { text-decoration: underline; text-underline-offset: 4px; }
.site-footer__address a:hover, .site-footer__links a:hover { color: var(--white); }

/* ═══════════════════════════ MOTION ═══════════════════════════ */
/* Only hide content when JS is present to un-hide it. Without the .js class
   (script blocked, error before the observer runs) everything stays visible. */
.js .reveal { opacity: 0; transform: translateY(26px); }
.reveal.is-in {
  opacity: 1; transform: none;
  transition: opacity .8s var(--ease), transform .8s var(--ease);
  transition-delay: var(--reveal-delay, 0s);
}

/* directional variants */
.reveal--left  { transform: translateX(-34px); }
.reveal--right { transform: translateX(34px); }
.reveal--scale { transform: scale(.94); }
/* The runner is baked into a flat raster, so there is no leg cycle to drive.
   What does read as motion from a single frame is travel: the scene enters
   from the left and decelerates, in the direction he is already facing, so
   the existing speed streaks do the work. */
.reveal--run { transform: translateX(-6%) scale(.97); }
.reveal--left.is-in, .reveal--right.is-in, .reveal--scale.is-in { transform: none; }
.reveal--run.is-in {
  transform: none;
  transition: opacity 1s var(--ease), transform 1.5s cubic-bezier(.16, .84, .28, 1);
}

/* children stagger in one after another */
.js .stagger > * { opacity: 0; transform: translateY(22px); }
.stagger.is-in > * {
  opacity: 1; transform: none;
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}
.stagger.is-in > *:nth-child(1) { transition-delay: .05s; }
.stagger.is-in > *:nth-child(2) { transition-delay: .13s; }
.stagger.is-in > *:nth-child(3) { transition-delay: .21s; }
.stagger.is-in > *:nth-child(4) { transition-delay: .29s; }
.stagger.is-in > *:nth-child(5) { transition-delay: .37s; }
.stagger.is-in > *:nth-child(6) { transition-delay: .45s; }
.stagger.is-in > *:nth-child(7) { transition-delay: .53s; }
.stagger.is-in > *:nth-child(8) { transition-delay: .61s; }
.stagger.is-in > *:nth-child(9) { transition-delay: .69s; }
.stagger.is-in > *:nth-child(10) { transition-delay: .77s; }
.stagger.is-in > *:nth-child(11) { transition-delay: .85s; }
.stagger.is-in > *:nth-child(12) { transition-delay: .93s; }

/* hero entrance — runs on load, not on scroll */
@keyframes rise { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: none; } }
@keyframes floatIn { from { opacity: 0; transform: translateY(46px) scale(.96); } to { opacity: 1; transform: none; } }
@keyframes bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-14px); } }

.hero__title { animation: rise .9s var(--ease) both .1s; }
.hero__lede  { animation: rise .9s var(--ease) both .25s; }
.hero__copy .btn { animation: rise .9s var(--ease) both .4s; }
.hero__note { animation: rise .9s var(--ease) both .5s; }
.hero__art   { animation: floatIn 1.2s var(--ease) both .2s; }
.hero__art img { animation: bob 7s ease-in-out infinite 1.4s; }
.logo-strip  { animation: rise .9s var(--ease) both .55s; }

/* ── icon rows ─────────────────────────────────────────────────────────
   The icons are the most characterful thing in this section, so they get
   their own entrance rather than riding the row's fade: each one arrives
   slightly scaled-down and rotated, and settles. Independent of the text,
   which keeps the row from feeling like one flat block sliding in. */
.js .feature__icon {
  opacity: 0;
  transform: scale(.82) rotate(-4deg);
}
.feature.is-in .feature__icon {
  opacity: 1;
  transform: none;
  transition:
    opacity .7s var(--ease) .12s,
    transform .8s cubic-bezier(.2, .9, .3, 1.2) .12s;
}
/* then breathe, so the section is never completely static */
.feature.is-in .feature__icon { animation: iconFloat 6s ease-in-out infinite 1.2s; }
.feature:nth-child(2n).is-in .feature__icon { animation-delay: 2s; }
.feature:nth-child(3n).is-in .feature__icon { animation-delay: 3.4s; }
@keyframes iconFloat {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -6px; }
}

/* the stats count in rather than just appearing */
.stat__value { font-variant-numeric: tabular-nums; }

/* service cards get a light sweep as they arrive */
.service-card { position: relative; overflow: hidden; }
/* A whisper of structure: at wide widths the three white columns swam on the
   dark ramp with nothing anchoring the grid (design audit, 8/14). */
.service-card { padding-top: 20px; border-top: 1px solid rgba(255, 255, 255, .16); }
.service-card::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(105deg, transparent 35%, rgba(255,255,255,.10) 50%, transparent 65%);
  transform: translateX(-120%);
  pointer-events: none;
}
.stagger.is-in .service-card::after {
  animation: sweep 1.5s var(--ease) forwards;
}
.service-card:nth-child(2)::after { animation-delay: .18s; }
.service-card:nth-child(3)::after { animation-delay: .36s; }
@keyframes sweep { to { transform: translateX(120%); } }

/* industry chips settle in with a touch of scale, not just a fade */
.js .chips > li { transform: translateY(18px) scale(.96); }
.chips.is-in > li { transform: none; }

/* subtle lift on the cards/rows that already have hover affordances */
.service-card, .quote, .feature { transition: transform .35s var(--ease); }
.service-card:hover, .quote:hover { transform: translateY(-4px); }
.feature:hover .feature__icon { transform: scale(1.09) rotate(2deg); animation-play-state: paused; }


.stat { transition: transform .35s var(--ease); }
.stat:hover { transform: translateY(-3px); }

/* ── back to top ─────────────────────────────────────────────── */
/* Stacked above the chat launcher, which is fixed 60px at a 24px inset
   with a near-max z-index — sharing the corner would bury this button. */
.to-top {
  --chat-size: 60px;
  --chat-inset: 24px;
  position: fixed;
  right: calc(var(--chat-inset) + (var(--chat-size) - 48px) / 2);
  bottom: calc(var(--chat-inset) + var(--chat-size) + 12px);
  z-index: 90;
  display: grid; place-items: center;
  width: 48px; height: 48px; padding: 0;
  border: 1px solid rgba(255, 255, 255, .28);
  border-radius: 50%;
  /* No backdrop-filter — same lesson as the header: a fixed element blurring
     whatever scrolls behind it re-blurs every frame. Near-opaque ink instead. */
  background: rgba(46, 55, 76, .94);
  color: var(--white);
  cursor: pointer;
  opacity: 0; visibility: hidden; transform: translateY(14px) scale(.9);
  transition: opacity .3s var(--ease), transform .3s var(--ease), visibility .3s, background .2s var(--ease);
}
.to-top.is-visible { opacity: 1; visibility: visible; transform: none; }
body.chat-open .to-top { opacity: 0; visibility: hidden; pointer-events: none; }
.to-top:hover { background: var(--ink); transform: translateY(-3px); }
.to-top svg { width: 18px; height: 18px; }

@media (prefers-reduced-motion: reduce) {
  /* No auto-scroll — expose it as a normal horizontally scrollable row. */
  .logo-marquee { overflow-x: auto; mask-image: none; scrollbar-width: thin; }
  .logo-marquee__inner { animation: none; }
  .logo-marquee__track[aria-hidden="true"] { display: none; }
  .quote-marquee { overflow-x: auto; mask-image: none; scrollbar-width: thin; }
  .quote-marquee__inner { animation: none; }
  .quotes[aria-hidden="true"] { display: none; }

  .reveal, .reveal.is-in,
  .stagger > *, .stagger.is-in > * { opacity: 1; transform: none; transition: none; }
  .hero__title, .hero__lede, .hero__copy .btn, .hero__note, .hero__art, .hero__art img, .logo-strip { animation: none; }
  .btn:hover, .chips li:hover, .socials a:hover,
  .service-card:hover, .quote:hover, .stat:hover, .to-top:hover { transform: none; }
  .feature:hover .feature__icon { transform: none; }
  .js .feature__icon, .feature.is-in .feature__icon { opacity: 1; transform: none; animation: none; transition: none; }
  .stagger.is-in .service-card::after { animation: none; }
  .js .chips > li, .chips.is-in > li { transform: none; }
  .reveal--run, .reveal--run.is-in { transform: none; transition: none; }
  /* No logo build: the mark is just there, scaffold hidden. */
  .hero__swoosh-tiles { display: none; }
  /* No build: the img keeps its own static shadow, the underlay stays off. */
  .hero__shadow { display: none; }
  .amb__video { display: none; }
  .services__banner img { animation: none; transform: scaleX(-1); }
  .industries__media img { animation: none; }
  .pillar__media img { animation: none; }
  .js .site-header .brand__logo,
  .site-header .brand:hover .brand__logo { animation: none; -webkit-mask-image: none; mask-image: none; }
  .mobile-nav { transition: none; }
  .js .hero__art img { opacity: 1; animation: none; filter: drop-shadow(0 30px 60px rgba(18, 26, 42, .28)); }
  .to-top { transition: opacity .01s; }
}

/* ═══════════════════════════ RESPONSIVE ═══════════════════════════ */
@media (max-width: 1000px) {
  .nav { display: none; }
  .nav-toggle { display: block; }
  .header__cta { display: inline-flex; margin-left: auto; margin-right: -8px; padding: 11px 18px 8px; font-size: 12px; }
  .hero__inner { grid-template-columns: 1fr; text-align: left; min-height: 0; padding-top: 24px; }
  .hero__art { max-width: 520px; margin-inline: auto; }
  .pillar--media-left { grid-template-columns: 1fr; }
  .pillar__media { max-width: 420px; margin-inline: auto; }
  .service-cards { grid-template-columns: 1fr; gap: 40px; }
  .stats { grid-template-columns: repeat(2, 1fr); }
  .chips { grid-template-columns: repeat(3, 1fr); }
  .contact__grid { grid-template-columns: 1fr; }
  .form { max-width: none; }
  .industries__media { width: 78%; opacity: .55; }
  .site-footer__inner { grid-template-columns: 1fr 1fr; }
  .site-footer__brand { grid-column: 1 / -1; }
}

/* Ultra-wide: the duel's focal pair sat right-of-centre leaving a third of
   dead fog on the left (design audit, 8/14). The image is scaleX(-1) flipped,
   so a lower source-x slides the pair displayed-leftward. */
@media (min-width: 1680px) {
  /* With the section at the footage's aspect there is no horizontal crop to
     re-center — only the vertical sky-trim bias carries over. */
  .services__banner img,
  .services__banner .amb__video { object-position: 50% 72%; }
}

@media (max-width: 1000px) {
  /* .services__banner is inset:0, so it stretches to the section's REAL height.
     Below 1000px .service-cards goes single-column and the section runs ~1200px
     tall against a ~390px width. object-fit:cover then crops the 2000x1494
     frame to its middle ~26%, which throws BOTH the lion and the fighter off
     screen: the phone shows fog.

     The frame gets its own band, sized to the source aspect so nothing crops,
     and the copy sits BELOW it on solid ink. The overlap the desktop layout
     uses cannot work at 390px: the scene is only ~293px tall there, the two
     figures fill 36-91% of it, and any scrim strong enough to carry white
     display type over pale fog erases them. Below 1000px, stop fighting the
     photo and give it its own room. */
  .services {
    /* No pull at all. -20px was still enough to cover the last Learn More in
       .pillars, which only carries clamp(8px,1vw,16px) of bottom padding: 8px
       at 393px. The ink clipped the link in half. The section above now opens
       its own bottom padding instead. */
    margin-top: 0;
    background: #141c2b;
    min-height: 0;
    display: block;
  }
  /* Clearance for the last Learn More, which sits directly above the band. */
  .pillars { padding-bottom: clamp(40px, 9vw, 64px); }
  .services__banner { inset: 0 0 auto; height: 74.7vw; }
  .services__banner img,
  .services__banner .amb__video {
    object-position: 50% 50%;
    /* No top fade. The frame's own sky is pale and the section above ends
       pale, so a fade to ink only drew a dark smudge across the join. */
    -webkit-mask-image: none;
    mask-image: none;
  }
  /* One job left: dissolve the bottom edge into the section ground. Without
     this, overflow:hidden clips the frame and the cut reads as a seam. */
  .services__banner::after {
    background: linear-gradient(180deg, #141c2b00 0, #141c2b00 76%, #141c2b 100%);
  }
  .services__body {
    /* On solid ink the desktop fog-insurance text-shadow has no job left —
       it rendered as a soft rectangular plate behind every line (audit
       2026-08-27). */
    text-shadow: none;
    /* Bottom pad opened up so the CTA is not sitting on the light hand-off. */
    padding-block: calc(74.7vw + clamp(20px, 5vw, 40px)) clamp(76px, 15vw, 104px);
    /* .services__body paints ABOVE .services__banner: both are positioned and
       the body is later in the DOM. So the ink layer has to stay transparent
       for the full height of the band or it covers the photo. It picks up at
       exactly 74.7vw, where the band has already dissolved to #141c2b, so the
       hard stop is invisible. Below that it ramps, which puts the depth back
       that the flat slab lost. */
    background:
      linear-gradient(to top, #fdfdfd 0, #e9ede9 16px, #ccd6d2 34px, #c2cfcb00 60px),
      linear-gradient(180deg, #141c2b00 0, #141c2b00 74.7vw,
                      #141c2b 74.7vw, #101827 74%, #0c1220 100%);
  }

  /* One axis for the whole section. The desktop layout centres everything,
     which works across three columns. Stacked into one narrow column it turned
     into centre-ragged monospace: a fixed-width face with no left edge to
     return to, four-line blocks ragged on both sides. The display line stays
     as it is, everything else gets a hard left edge, matching the About
     section above it. */
  .services__title.h2--center { text-align: left; }
  .services__lede { text-align: left; max-width: 46ch; margin-inline: 0; }
  .services__cta { text-align: left; margin-top: clamp(22px, 5vw, 34px); }
  .service-cards { text-align: left; gap: 34px; counter-reset: svc; }
  .service-card { counter-increment: svc; padding-top: 18px; }
  .service-card p { max-width: 40ch; margin-inline: 0; }
  /* Numbered so the three read as a set instead of one undifferentiated
     column of hairlines. Counter, so no markup change. */
  .service-card__title { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
  .service-card__title::before {
    content: counter(svc, decimal-leading-zero);
    font-size: .72em;
    color: rgba(255, 255, 255, .54);
    letter-spacing: .06em;
  }
}

@media (max-width: 640px) {
  :root { --header-h: 72px; }
  .feature { grid-template-columns: 88px minmax(0, 1fr); gap: 16px; }
  .chips { grid-template-columns: repeat(2, 1fr); }
  .stats { gap: 30px 20px; }
  /* "20 Minutes" wrapped to two lines at the 30px clamp floor in a 165px
     column, leaving row 2 ragged. A flat 26px still wraps below 390px
     (158.6px of glyphs vs 130/150px columns at 320/360) — scale with the
     viewport instead; verified single-line at 320/360/390/414. */
  .stat__value { font-size: clamp(28px, 9vw, 40px); }
  /* The page ended 35px after the last tap target — abrupt against the
     otherwise generous rhythm. */
  .site-footer { padding-bottom: 56px; }
  .site-footer__inner { grid-template-columns: 1fr; }
  /* The chat launcher drops to a 16px inset below 640px (embed.js media query);
     the back-to-top button rides on the same variable so they stay stacked. */
  .to-top { --chat-inset: 16px; }
}

/* ═══ CALM PASS (2026-08-24) ═══
   The designer's new direction: the site keeps its imagery and layout but
   stops MOVING. Retired: the header mark pulse, the hero art bob (under JS,
   where the tile build owns the art's motion), the drifting stills in the
   dark bands, the floating feature icons, the service-card sweep, and the
   ambient video loops (main.js + pages.js). Kept: the one-shot entrances —
   rise/floatIn, and the hero TILE BUILD itself, restored at the user's
   request (2026-08-24); so no overrides on .hero__art/.hero__shadow/
   .hero__swoosh-tiles here — the build choreography rules stand. */
.site-header .brand__logo,
.js .site-header .brand__logo,
.site-header .brand:hover .brand__logo,
.site-header .brand:focus-visible .brand__logo {
  animation: none; -webkit-mask-image: none; mask-image: none;
}
.pillar__media img,
.services__banner img,
.industries__media img {
  animation: none;
  /* The drifts are gone, so the will-change that served them must go too:
     it kept three full-width stills (the duel frame is 2000px) permanently
     promoted as GPU layers — texture memory that showed up as scroll jank
     on phones ("navigation is not buttery smooth", user 2026-08-27). */
  will-change: auto;
}
.feature.is-in .feature__icon { animation: none; }
.stagger.is-in .service-card::after { animation: none; opacity: 0; }

/* ═══ MOBILE BREATHING ROOM (designer video, 2026-08-27) ═══
   "On mobile you need to work a little bit more on the spacing … a little bit
   more white space in between the sections. It's a little bit cluttered."
   Five call-outs, all phone-width; desktop spacing already read fine. */
@media (max-width: 1000px) {
  /* "The LET'S TALK button should be a little bit lower." */
  .hero__copy .btn { margin-top: 14px; }
  /* "Between Learn More and this section should be way more space" — the two
     pillar blocks sat 6px + 6px apart at 390px. */
  .pillar { padding-block: clamp(22px, 5vw, 36px); }
  /* "Add more space on top of this piece of text" — the services title started
     20px under the duel band. The band join stays pinned at 74.7vw (the ink
     gradient's hard stop below), only the dark run-in above the title grows.
     "Bring the black line beneath Explore all services further down" — the
     bottom pad carries the ink past the CTA before the light hand-off. */
  .services__body {
    padding-block: calc(74.7vw + clamp(40px, 10vw, 64px)) clamp(108px, 22vw, 144px);
  }
  /* "More spacing on top of the ZERO and 107." */
  .stats { margin-top: clamp(28px, 7vw, 40px); }
  /* Same-eye follow-ups (audit 2026-08-27): the two joins the pass missed. */
  /* Stats → "Technically, better" was the tightest boundary left (38px where
     every other handover gets 100px+). */
  .better { padding-top: clamp(48px, 11vw, 64px); }
  /* The closing CTA band opened 12px under the section above it, vs the
     90-160px run-ins every other band now gets. */
  .contact { padding-top: 48px; }
}

/* Touch has no :hover to pause the quote marquee and its overflow is hidden —
   a phone user gets drifting quotes with no control. Reuse the reduced-motion
   treatment: a plain swipeable rail (the peeking second card invites it). */
@media (pointer: coarse) {
  .quote-marquee { overflow-x: auto; mask-image: none; scrollbar-width: thin; }
  .quote-marquee__inner { animation: none; }
  .quotes[aria-hidden="true"] { display: none; }
}
