/* ==========================================================================
   Prerna Maheshwari — main.css
   Theme: warm cream editorial x deep forest green x bright lime accent.
   Inspired by assets/reference/Upword-copy-... (aesthetic language only).
   ========================================================================== */

/* Self-hosted fonts (2026-08-01) — replaces the Google Fonts <link> in
   header.php: removes a third-party DNS/TLS round trip that page render
   was blocked on, and fixes a real bug found in the process — EB Garamond
   has no weight-300 file on Google Fonts (its real range is 400-800), so
   the old `wght@0,300` request was silently dropped and every heading was
   falling back to Times New Roman. Every font-weight:300 reference to
   --v3-font-display below is now 400, the family's actual lightest real
   weight. inter-variable.woff2 is a true variable font (100-900 axis) —
   one file covers both the 400 and 500 weights used on this site. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/fonts/inter-variable.woff2') format('woff2');
}
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/eb-garamond-400.woff2') format('woff2');
}
@font-face {
  font-family: 'EB Garamond';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/eb-garamond-400italic.woff2') format('woff2');
}
@font-face {
  font-family: 'Parisienne';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/parisienne-400.woff2') format('woff2');
}

/* Pre-v3 tokens (2026-08-01: pruned to just what's still live). The v1
   cream/green/lime palette and its component classes (buttons, section
   backgrounds, the old Articles card styling) were fully removed here —
   they were only reachable through sections no longer in the DOM (the
   whole site is v3 now except Articles, which is hidden pending Phase 2
   and will get its own v3 treatment when that starts, not these). What's
   left below is genuinely still referenced: body's base color/font and
   a couple of hairline/focus rules that never got a --v3- equivalent. */
:root {
  --ink:          #17201a;
  --ink-soft:     #3c453e;

  --line-light:   rgba(23, 32, 26, 0.12);

  --font-sans:  'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
}

/* ==========================================================================
   v3 design tokens — "Editorial Atmosphere" (docs/DESIGN-SYSTEM.md)
   ========================================================================== */
:root {
  --v3-canvas:          #f5f5f4;
  --v3-canvas-soft:     #fafaf9;
  --v3-surface:         #ffffff;
  --v3-surface-strong:  #f0efed;
  --v3-surface-dark:    #0c0a09;

  --v3-ink:             #0c0a09;
  --v3-primary:         #292524;
  --v3-primary-active:  #0c0a09;
  --v3-body:            #4e4e4e;
  --v3-muted:           #777169;
  --v3-muted-soft:      #a8a29e;
  --v3-on-primary:      #ffffff;
  --v3-on-dark:         #ffffff;
  --v3-on-dark-soft:    #a8a29e;

  --v3-hairline:        #e7e5e4;
  --v3-hairline-soft:   #f0efed;
  --v3-hairline-strong: #d6d3d1;

  /* Atmospheric gradient stops — decoration only, see the orb rules
     comment above .orb below. Meaning-map (docs/DESIGN-SYSTEM.md §1):
     peach=corporate training, lavender=teacher workshops, sky=campus/
     college programs, mint=interview prep, rose=seminars & keynotes. */
  --v3-orb-mint:        #a7e5d3;
  --v3-orb-peach:       #f4c5a8;
  --v3-orb-lavender:    #c8b8e0;
  --v3-orb-sky:         #a8c8e8;
  --v3-orb-rose:        #e8b8c4;

  --v3-font-display: 'EB Garamond', 'Times New Roman', serif;
  --v3-font-body:    'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --v3-radius-xs:   4px;
  --v3-radius-md:   8px;
  --v3-radius-lg:   12px;
  --v3-radius-xl:   16px;
  --v3-radius-xxl:  24px;
  --v3-radius-pill: 999px;
}

/* Every h1/h2/h3 inside a v3-migrated section gets the editorial-serif
   weight-400 voice automatically (EB Garamond's real lightest weight —
   it has no 300 cut, see the @font-face comment at the top of this
   file). Exact size/line-height/tracking per level is set by each
   component per the type table in docs/DESIGN-SYSTEM.md §2, not here. */
.v3-section h1, .v3-section h2, .v3-section h3 {
  font-family: var(--v3-font-display);
  font-weight: 400;
  color: var(--v3-ink);
}

/* ---- Orbs (docs/DESIGN-SYSTEM.md §1 "Orb rules (critical)") ----
   Soft radial-gradient blooms, decoration only: absolutely positioned,
   aria-hidden, pointer-events:none, large + heavily diffused, placed
   behind content via z-index/negative offset — never a container. Base
   rule here; each usage sets --orb-color, size, and position inline or
   via a modifier class. Drift animation in js/main.js respects
   prefers-reduced-motion (frozen, not hidden, per the spec). */
.orb {
  position: absolute;
  z-index: 0;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(closest-side, var(--orb-color, var(--v3-orb-peach)) 0%, transparent 70%);
  opacity: 0.7;
  filter: blur(2px);
}
@media (prefers-reduced-motion: no-preference) {
  .orb { animation: orb-drift 32s var(--ease) infinite alternate; }
}
@keyframes orb-drift {
  0%   { transform: translate(0, 0) scale(1); }
  100% { transform: translate(3%, -4%) scale(1.05); }
}

/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}
body, h1, h2, h3, p, figure, blockquote, dl, dd { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
img, picture { max-width: 100%; display: block; }
input, button, textarea, select { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
button { cursor: pointer; }

body {
  /* v3 canvas (#f5f5f4) — close enough to v1 cream (#f4efe1) that this is
     safe to change globally now, before every section is migrated: it
     only shows through in the gaps around fixed elements, not inside any
     section (each section still sets its own explicit background). */
  background: var(--v3-canvas);
  color: var(--ink-soft);
  font-family: var(--font-sans);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* ---- Typography ---- */
h1, h2, h3 {
  font-family: var(--v3-font-display);
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--ink);
}

h1 { font-size: 72px; line-height: 1.08; }
h2 { font-size: 44px; line-height: 1.15; }
h3 { font-size: 22px; line-height: 1.3; }

em { font-style: italic; font-weight: 400; }

p { max-width: 62ch; }

.eyebrow {
  display: block;
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-soft);
  margin-bottom: 18px;
}

/* ---- Layout ---- */
.wrap {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 32px;
}

.section-pad { padding: 120px 0; }

.section-head {
  max-width: 640px;
  margin-bottom: 56px;
}
.section-head p { margin-top: 16px; }

.nav-cta { padding: 12px 22px; font-size: 13.5px; }

/* ---- Header (v3 — docs/DESIGN-SYSTEM.md §6.1) ----
   Canvas background always (not transparent-over-hero like v1 — v3's
   hero is canvas-colored too, so header and hero read as one continuous
   surface at rest); a hairline bottom border fades in only once scrolled,
   for definition once content is moving under the fixed bar. */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  height: 64px;
  display: flex;
  align-items: center;
  background: var(--v3-canvas);
  border-bottom: 1px solid transparent;
  transition: border-color 0.3s var(--ease);
}
.site-header.is-scrolled {
  border-bottom-color: var(--v3-hairline);
}

.site-header__inner {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Signature-style brand mark (2026-07-31) — 'Parisienne', self-hosted
   (see the @font-face block at the top of this file); not used for any
   other text on the site. Sized larger than regular nav text (script
   faces need the extra size to stay legible) but kept to a single
   weight, matching how script fonts are normally distributed. */
.wordmark {
  font-family: 'Parisienne', cursive;
  font-size: 34px;
  color: var(--v3-ink);
  line-height: 1;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: 32px;
}
.site-nav a:not(.nav-cta) {
  font-family: var(--v3-font-body);
  font-size: 15px;
  font-weight: 500;
  color: var(--v3-ink);
  opacity: 0.72;
  transition: opacity 0.2s var(--ease);
}
.site-nav a:not(.nav-cta):hover,
.site-nav a:not(.nav-cta):focus-visible { opacity: 1; }

.nav-toggle {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  min-width: 44px;
  min-height: 44px;
  padding: 8px;
  background: none;
  border: 0;
}
.nav-toggle span { width: 22px; height: 1.5px; background: var(--v3-ink); }

/* ---- Buttons v3 (docs/DESIGN-SYSTEM.md §6.14 — exact spec) ---- */
.btn-primary-v3,
.btn-outline-v3 {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 40px;
  padding: 0 20px;
  border-radius: var(--v3-radius-pill);
  font-family: var(--v3-font-body);
  font-size: 15px;
  font-weight: 500;
  transition: background 0.2s var(--ease), border-color 0.2s var(--ease), color 0.2s var(--ease);
}
/* Glossy "squishy pill" treatment (2026-07-31, docs/DESIGN-SYSTEM.md
   §6.14) — scoped to the primary button only; .btn-outline-v3 and
   .text-link-v3 are untouched on purpose, for contrast against this.
   Three-stop vertical gradient derived from our own tokens (no new
   colors introduced): a light-mixed tint at top, --v3-primary in the
   middle (the token's normal resting shade), --v3-primary-active at the
   bottom (already our defined "darker/pressed" shade — reused here
   rather than inventing a fourth color). Kept deliberately subtle: an
   80/20 mix rather than a bright highlight, since the brief was "glossy
   but understated, not candy-like." Contrast checked at the lightest
   stop (the top, the worst case for white text): ~8.5:1, comfortably
   clear of the 4.5:1 AA floor for 15px/600 text. position/z-index/
   overflow here support the ::before gloss-arc pseudo-element below —
   negative z-index on a stacking context THIS element establishes
   (position:relative + z-index:0) is what keeps the arc behind the
   label text without needing to touch the button's HTML/text at all. */
.btn-primary-v3 {
  position: relative;
  z-index: 0;
  overflow: hidden;
  isolation: isolate;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--v3-primary) 80%, white) 0%,
    var(--v3-primary) 45%,
    var(--v3-primary-active) 100%);
  color: var(--v3-on-primary);
  font-weight: 600;
  box-shadow: 0 8px 20px -8px rgba(12, 10, 9, 0.35);
  transition: filter 0.2s var(--ease), box-shadow 0.2s var(--ease), transform 0.15s var(--ease);
}
/* Soft gloss arc — a lighter, semi-transparent highlight confined to
   the top of the pill, suggesting a convex glossy surface. Negative
   z-index (see note above) keeps it behind the label text without any
   markup change. */
.btn-primary-v3::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: 2px 2px auto 2px;
  height: 46%;
  border-radius: 999px 999px 45% 45% / 999px 999px 100% 100%;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.24), transparent);
  opacity: 0.85;
  transition: opacity 0.2s var(--ease);
  pointer-events: none;
}
.btn-primary-v3:hover, .btn-primary-v3:focus-visible {
  filter: brightness(1.08);
}
.btn-primary-v3:hover::before, .btn-primary-v3:focus-visible::before {
  opacity: 1;
}
.btn-primary-v3:active {
  transform: scale(0.97);
  filter: brightness(0.98);
}
@media (prefers-reduced-motion: reduce) {
  .btn-primary-v3 {
    transition: none;
  }
  .btn-primary-v3::before {
    transition: none;
  }
}
.btn-outline-v3 {
  background: transparent;
  color: var(--v3-ink);
  border: 1px solid var(--v3-hairline-strong);
}
.btn-outline-v3:hover, .btn-outline-v3:focus-visible {
  border-color: var(--v3-ink);
}
.text-link-v3 {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--v3-font-body);
  font-size: 15px;
  font-weight: 500;
  color: var(--v3-ink);
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
  transition: border-color 0.2s var(--ease);
}
.text-link-v3:hover, .text-link-v3:focus-visible { border-color: var(--v3-ink); }

/* ---- Primary CTA shine (docs/DESIGN-SYSTEM.md §6.14 exception) ----
   Approved 2026-07-31 for the Variant B hero CTA only (see
   hero-variant-ghost-text.php) — a rotating conic-gradient border shine
   + faint dot texture + hover glow, chosen over a quieter single-sweep
   alternative that was built for comparison and removed once V1 won.

   Uses an orb hue as a rotating border/glow. This is a DELIBERATE,
   SCOPED EXCEPTION to §1's "orb colors never as button fills/text/
   borders" rule, confirmed and documented there — not a license to
   copy --shine onto another button without re-reading that note first.

   @property is declared unconditionally (harmless no-op where
   unsupported); the ring/animation itself is gated behind
   `@supports at-rule(@property)` so browsers that can't animate the
   angle don't get a static/janky ring — they just see the plain pill. */
@property --gradient-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}
/* Heartbeat scale pulse (2026-08-01), layered on top of the shine ring
   below — registered so the browser tweens it smoothly between keyframe
   stops instead of jumping (plain custom properties don't interpolate
   without @property). See the transform note on .btn-primary-v3--shine
   for why this drives `transform` through a variable rather than
   animating `transform` directly. */
@property --heartbeat-scale {
  syntax: '<number>';
  inherits: false;
  initial-value: 1;
}

@supports at-rule(@property) {
  .btn-primary-v3--shine {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    border: 1.5px solid transparent;
    /* Padding-box layer now carries the same glossy gradient as the
       base .btn-primary-v3 (2026-07-31) instead of a flat fill, so the
       hero CTA shows both approved treatments composed together: the
       glossy pill fill plus this rotating border ring. */
    background:
      linear-gradient(180deg,
        color-mix(in srgb, var(--v3-primary) 80%, white) 0%,
        var(--v3-primary) 45%,
        var(--v3-primary-active) 100%) padding-box,
      conic-gradient(from var(--gradient-angle),
        transparent 0deg,
        var(--v3-orb-peach) 30deg,
        transparent 80deg,
        transparent 280deg,
        var(--v3-orb-peach) 330deg,
        transparent 360deg
      ) border-box;
    box-shadow: 0 0 0 0 rgba(244, 197, 168, 0);
    /* Two independent loops: the ring keeps rotating (--gradient-angle)
       while a tiny lub-dub scale nudge (--heartbeat-scale) runs in
       parallel. `transform` itself stays a plain, unanimated
       declaration that reads the custom property — so
       .btn-primary-v3:active's press-scale (a normal declaration with
       higher specificity: class+pseudo-class beats class) still wins
       cleanly on click instead of fighting a running animation that
       targets `transform` directly. */
    animation: btn-shine-rotate 6s linear infinite, btn-heartbeat 2.4s ease-in-out infinite;
    transform: scale(var(--heartbeat-scale));
    transition: box-shadow 0.4s var(--ease);
  }
  .btn-primary-v3--shine::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background-image: radial-gradient(rgba(255, 255, 255, 0.4) 1px, transparent 1.4px);
    background-size: 7px 7px;
    -webkit-mask-image: radial-gradient(ellipse at 50% 50%, black 5%, transparent 70%);
    mask-image: radial-gradient(ellipse at 50% 50%, black 5%, transparent 70%);
    opacity: 0.3;
    pointer-events: none;
  }
  .btn-primary-v3--shine::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(115deg, transparent 40%, rgba(255, 255, 255, 0.32) 50%, transparent 60%);
    transform: translateX(-140%);
    opacity: 0;
    pointer-events: none;
  }
  .btn-primary-v3--shine:hover,
  .btn-primary-v3--shine:focus-visible {
    /* Order matches the animation list above: ring, then heartbeat.
       Both quicken together on hover — the heartbeat noticeably so
       (2.4s -> 1.8s), reading as a little excitement on approach. */
    animation-duration: 2.2s, 1.8s;
    box-shadow: 0 0 22px 2px rgba(244, 197, 168, 0.4);
  }
  .btn-primary-v3--shine:hover::after,
  .btn-primary-v3--shine:focus-visible::after {
    animation: btn-shine-sweep 0.9s var(--ease) forwards;
  }
  .btn-primary-v3--shine.is-offscreen {
    animation-play-state: paused;
  }
  @keyframes btn-shine-rotate {
    to { --gradient-angle: 360deg; }
  }
  /* Lub-dub: two quick nudges then a rest, not a smooth sine pulse —
     that pause is what reads as "heartbeat" rather than "breathing". */
  @keyframes btn-heartbeat {
    0%, 32%, 100% { --heartbeat-scale: 1; }
    8% { --heartbeat-scale: 1.035; }
    16% { --heartbeat-scale: 1; }
    24% { --heartbeat-scale: 1.025; }
  }
  @keyframes btn-shine-sweep {
    0% { transform: translateX(-140%); opacity: 0; }
    15% { opacity: 1; }
    100% { transform: translateX(140%); opacity: 0; }
  }
}

@media (prefers-reduced-motion: reduce) {
  .btn-primary-v3--shine {
    /* The rotating ring and heartbeat pulse are both motion — the glossy
       fill itself is static, so it stays (matches .btn-primary-v3's own
       reduced-motion behavior, which keeps its static gradient too and
       only removes hover/active transition easing). */
    animation: none;
    background: linear-gradient(180deg,
      color-mix(in srgb, var(--v3-primary) 80%, white) 0%,
      var(--v3-primary) 45%,
      var(--v3-primary-active) 100%);
    border-color: transparent;
    box-shadow: none;
    transform: none;
  }
  .btn-primary-v3--shine::before,
  .btn-primary-v3--shine::after {
    display: none;
  }
}

/* ---- Hero (v3 — docs/DESIGN-SYSTEM.md §6.2) ----
   Canvas band, left-aligned display-mega headline, one orb bloom behind
   it, photo as a framed rounded-xl plate beside the text (not full-bleed,
   not blended — see §6.4 photo-card rules, shared with About/Methodology
   once those sections migrate). */
.hero {
  position: relative;
  background: var(--v3-canvas);
  padding: calc(64px + 72px) 0 96px;
  overflow: hidden;
}
.hero__grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 0.95fr 1.05fr;
  gap: 56px;
  align-items: center;
}
.hero__text {
  position: relative;
  max-width: 540px;
}
.hero__orb {
  width: 520px;
  height: 520px;
  top: -170px;
  left: -160px;
}
.hero h1 {
  position: relative;
  z-index: 1;
  /* Fluid clamp replacing the old 3-step breakpoint jump (64/40/34px) —
     same min/max as before (34px mobile floor, 64px desktop ceiling,
     reaching it exactly at a 1280px viewport), just interpolated
     continuously instead of jumping at 880px/560px. */
  font-size: clamp(2.125rem, 5vw, 4rem);
  line-height: 1.05;
  letter-spacing: -1.9px;
  margin-bottom: 24px;
}
.hero h1 em { font-style: italic; }
.hero__lede {
  position: relative;
  z-index: 1;
  font-family: var(--v3-font-body);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0.16px;
  color: var(--v3-body);
  max-width: 48ch;
  margin-bottom: 32px;
}
.hero__ctas {
  position: relative;
  z-index: 1;
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 24px;
}
.hero__badges {
  position: relative;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.badge-pill-v3 {
  display: inline-flex;
  align-items: center;
  padding: 8px 16px;
  border-radius: var(--v3-radius-pill);
  background: var(--v3-surface-strong);
  font-family: var(--v3-font-body);
  font-size: 13px;
  font-weight: 500;
  color: var(--v3-muted);
}

/* ---- Hero entrance choreography ----
   Pure CSS, plays once on load (hero is always above the fold, so it needs
   no IntersectionObserver — unlike the .reveal system used further down
   the page). Each element arrives in sequence rather than all at once. */
.hero-in {
  opacity: 0;
  transform: translateY(22px);
  animation: hero-rise 0.9s var(--ease) forwards;
}
.hero-in--2 {
  animation-name: hero-headline-in;
  animation-duration: 1s;
  animation-delay: 0.1s;
}
.hero-in--3 { animation-delay: 0.4s; }
.hero-in--4 { animation-delay: 0.58s; }
.hero-in--5 { animation-delay: 0.74s; }
.hero-in--scale {
  opacity: 0;
  transform: translateY(16px) scale(0.97);
  animation: hero-rise-scale 1.1s var(--ease) 0.2s forwards;
}

@keyframes hero-rise {
  to { opacity: 1; transform: translateY(0); }
}
@keyframes hero-headline-in {
  from { opacity: 0; transform: translateY(28px); filter: blur(6px); }
  to { opacity: 1; transform: translateY(0); filter: blur(0); }
}
@keyframes hero-rise-scale {
  to { opacity: 1; transform: translateY(0) scale(1); }
}

.hero__scroll-cue {
  position: absolute;
  z-index: 1;
  left: 50%;
  bottom: 64px;
  transform: translateX(-50%);
  opacity: 0;
  animation: hero-rise 0.9s var(--ease) 1.1s forwards;
}
.hero__scroll-line {
  display: block;
  position: relative;
  width: 1px;
  height: 44px;
  overflow: hidden;
  background: var(--v3-hairline-strong);
}
.hero__scroll-line::after {
  content: '';
  position: absolute;
  left: 0; top: -44px;
  width: 100%;
  height: 44px;
  background: linear-gradient(to bottom, transparent, var(--v3-ink) 55%, transparent);
  animation: hero-scroll-drop 2.2s ease-in-out infinite;
}
@keyframes hero-scroll-drop {
  0% { transform: translateY(0); }
  100% { transform: translateY(88px); }
}

/* ---- Hero photo grid — shuffling tiles (docs/DESIGN-SYSTEM.md §6.2,
   colors/type/radius tokens unchanged; only the hero's right column
   layout/behavior is new here) ----
   3x3 grid of persistent tile elements; js/hero-tiles.js reorders their
   DOM position on an interval and animates the resulting position delta
   via the Web Animations API (FLIP technique) — this CSS only supplies
   the static grid, sizing, and framing, no animation values live here. */
.hero__tiles-wrap {
  position: relative;
  z-index: 1;
  margin-inline-start: auto;
  max-width: 500px;
}
.hero__tiles {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  gap: 6px;
  aspect-ratio: 1;
}
.hero__tile {
  position: relative;
  border-radius: var(--v3-radius-lg);
  overflow: hidden;
  background: var(--v3-surface-strong);
  will-change: transform;
}
/* <picture> is inline by default and won't stretch to fill a grid cell —
   the same bug already caught twice elsewhere in this project (hero
   photo, carousel cards). Fixing it here from the start this time. */
.hero__tile picture {
  display: block;
  width: 100%;
  height: 100%;
}
.hero__tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.hero__tiles-caption {
  margin-block-start: 12px;
  font-family: var(--v3-font-body);
  font-size: 14px;
  color: var(--v3-muted);
  text-align: end;
}

/* ---- Hero Variant B — ghost-text editorial comparison ----
   Kept separate from .hero / .hero__tiles so Variant A remains intact.
   Deliberate exception (2026-07-30, per direct request to match a
   reference's atmospheric-gradient energy more closely): the flat
   --v3-canvas background is replaced with a warm gradient wash for this
   variant only — deeper than the sitewide "soft orb, canvas otherwise"
   rule normally allows. Built from our own peach identity color, not an
   arbitrary borrowed hue, and scoped to Variant B exactly like the
   cutout-photo exception above it. Documented in DESIGN-SYSTEM.md §6.2.

   Rebuilt (second pass) as a single centered composition instead of a
   two-column split: height is pinned to exactly 100dvh minus the fixed
   header, so header + hero always fill the first screen with no scroll
   required — 100dvh (not vh) so mobile browser chrome resizing doesn't
   cause a jump. Every child is absolutely positioned against this fixed
   box (ghost name, photo, proof card, bottom content), which is what
   guarantees nothing can overflow it regardless of content length. */
.hero-ghost {
  position: relative;
  isolation: isolate;
  height: 100dvh;
  overflow: hidden;
  background: var(--v3-canvas);
}
/* Safety net for short viewports (2026-08-01) — landscape phones, a
   short/split-screen browser window, or a phone's on-screen keyboard
   shrinking the visible area. Every child here (ghost name, photo, proof
   card, headline+CTA band) is absolutely positioned against this box's
   100dvh height; none of the width breakpoints above account for height,
   so a short-but-wide viewport could pack all of that into very little
   vertical room with nowhere for overflow to go. Rather than trying to
   re-choreograph the whole composition for a case that's hard to verify
   visually, this only removes the failure mode where content becomes
   truly inaccessible: min-height keeps every child's normal position,
   and overflow:visible means anything that doesn't fit spills below and
   scrolls into view instead of being silently clipped. */
@media (max-height: 560px) {
  .hero-ghost {
    height: auto;
    min-height: 100dvh;
    overflow: visible;
  }
}

/* Gradient wash: sits IN FRONT of the photo (z-index above the cutout),
   not just as a page background behind it — a bottom-half band rather
   than a corner-to-corner diagonal. Built as a mesh of several soft
   radial blooms (our own orb hues: peach/rose/terracotta) layered
   together instead of one flat linear sweep — a single linear gradient
   reads flat/predictable; overlapping radials at different positions
   and sizes give it depth and an organic edge, much closer to what
   actually reads as "cool" in the reference, while still built from
   real design-system colors rather than an arbitrary borrowed gradient. */
.hero-ghost__gradient {
  position: absolute;
  z-index: 2;
  inset: 0;
  /* Two mirrored blooms (20%/80%, same size and opacity) instead of the
     earlier three-bloom layout, which had two blooms clustered on the
     right (60% and 88%) against only one on the left (22%) — genuinely
     heavier on that side, not just a perception issue. A third, smaller
     bloom stays dead-center for a touch of variation without tipping
     the balance back to either side. */
  background:
    radial-gradient(ellipse 62% 55% at 20% 100%, rgba(232, 184, 196, 0.55), transparent 60%),
    radial-gradient(ellipse 62% 55% at 80% 100%, rgba(201, 128, 79, 0.55), transparent 60%),
    radial-gradient(ellipse 50% 45% at 50% 105%, rgba(230, 172, 126, 0.45), transparent 60%),
    linear-gradient(180deg, rgba(244, 197, 168, 0) 0%, rgba(244, 197, 168, 0) 40%, rgba(180, 105, 60, 0.35) 100%);
  pointer-events: none;
}

/* Ghost name: centered horizontally and fitted to the section width in
   vw (verified against real renders at several viewport widths — not
   just assumed), positioned above true vertical center so it doesn't
   run into the headline sitting in the bottom band. */
.hero-ghost__name {
  position: absolute;
  z-index: 0;
  inset-inline: 0;
  inset-block-start: 32%;
  transform: translateY(-50%);
  width: 100%;
  text-align: center;
  font-family: var(--v3-font-display);
  font-size: 10.4vw;
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--v3-ink);
  opacity: 0.09;
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
  will-change: transform;
}

/* Subject centered horizontally, anchored to the section's bottom edge
   — one unified composition instead of a left column / right column
   split. Centering is done with flexbox (justify-content), not a
   transform: translateX(-50%) on the wrapper — that combination proved
   unreliable once JS also needed to write a transform to the same
   element for the parallax effect (the percentage got resolved once
   against a stale width and never recalculated). Flexbox has no such
   issue, and it leaves `transform` free for the parallax JS to use for
   the Y-axis only.
   Her face isn't at the exact horizontal center of the source image,
   so a small static offset nudges the *picture itself* rather than
   the wrapper JS touches. Re-measured 2026-08-10, twice the same day:
   first for the charcoal-blazer cutout (+0.9% needed), then again for
   this navy-blazer re-edit (client asked for a specific attire change)
   since the new export's face position shifted again — this asset
   needs +4.9%. Always re-measure from the alpha channel (top 35% of
   rows, not the full body bbox — shoulders/blazer width skew the
   full-body center in a way the face position doesn't share) after any
   future photo swap; never assume the previous value still applies. */
.hero-ghost__cutout-wrap {
  position: absolute;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  height: 92%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  pointer-events: none;
  will-change: transform;
}
.hero-ghost__cutout { display: block; height: 100%; margin: 0; transform: translateX(4.9%); }
.hero-ghost__cutout img { display: block; height: 100%; width: auto; max-width: none; }

/* Floating proof card — honest stand-in for the reference's client-
   avatar photo stack + unverifiable "10K+" stat. We don't have client
   headshots or that number, so this uses four real crops from Prerna's
   own session photos (see CONTENT.md's hero-avatar-1..4 row) and the
   already-approved "12+ Organisations Trained" figure instead. No card
   background — sits directly on the canvas/gradient, same as the rest
   of this composition. */
.hero-ghost__proof {
  position: absolute;
  z-index: 3;
  inset-block-start: calc(64px + 24px);
  inset-inline-end: max(24px, calc((100% - 1200px) / 2 + 32px));
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}
.hero-ghost__proof-avatars { position: relative; display: flex; }
.hero-ghost__proof-avatars picture {
  display: block;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  overflow: hidden;
  border: 2px solid rgba(255, 255, 255, 0.9);
  margin-inline-start: -10px;
}
.hero-ghost__proof-avatars picture:first-child { margin-inline-start: 0; }
.hero-ghost__proof-avatars img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* Same glossy treatment as .btn-primary-v3 (2026-07-31, docs/DESIGN-
   SYSTEM.md §6.14) — identical gradient recipe/stops, adapted to a
   circle: the gloss arc pseudo-element uses border-radius:50% instead
   of the pill's flattened-bottom shape, and the drop shadow is scaled
   down proportionally for a 32px badge instead of a 40px-tall button. */
.hero-ghost__proof-badge {
  position: relative;
  z-index: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  margin-inline-start: -10px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.9);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--v3-primary) 80%, white) 0%,
    var(--v3-primary) 45%,
    var(--v3-primary-active) 100%);
  box-shadow: 0 3px 8px -3px rgba(12, 10, 9, 0.35);
  color: var(--v3-on-dark);
  font-family: var(--v3-font-body);
  font-size: 11px;
  font-weight: 600;
}
.hero-ghost__proof-badge::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: 1px;
  border-radius: 50%;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.24), transparent 55%);
  pointer-events: none;
}
.hero-ghost__proof-text { font-family: var(--v3-font-body); font-size: 12px; line-height: 1.4; color: var(--v3-muted); }
.hero-ghost__proof-text strong { color: var(--v3-ink); font-weight: 500; }

/* Bottom content band — headline bottom-left, CTA + short line bottom-
   right, both overlapping the same lower band the subject's legs sit
   in (per the reference) rather than living in a separate text column
   next to a separate photo column. */
.hero-ghost__bottom {
  position: absolute;
  z-index: 3;
  inset-inline: max(24px, calc((100% - 1200px) / 2 + 32px));
  inset-block-end: clamp(24px, 5vh, 56px);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: clamp(16px, 4vw, 48px);
}
/* h1.hero-ghost__headline (not just .hero-ghost__headline): the global
   ".v3-section h1, h2, h3 { color: var(--v3-ink) }" rule (near the top
   of this file) has specificity (0,1,1) — a plain class selector here
   is only (0,1,0) and loses to it regardless of source order. Adding
   the type selector matches that specificity so this rule's later
   position in the file actually wins, instead of silently losing to a
   rule with a stronger selector while looking like it should apply. */
h1.hero-ghost__headline {
  position: relative;
  /* Narrow enough that "Confidence"/"Composure"/"Presence" — different
     lengths, since the opening word rotates — all wrap to the same
     5 lines instead of the shortest word letting an extra word fit on
     line 1 and shifting the whole rest of the sentence up. Verified
     programmatically across a range of widths (340–400px all gave 5
     lines for all three; 620px collapsed everything to 3 lines but
     ran wide enough to overlap the centered photo) rather than
     eyeballing one variant. */
  max-width: 380px;
  font-size: clamp(1.75rem, 4.6vw, 3.5rem);
  line-height: 1.02;
  letter-spacing: -0.03em;
  /* Plain ink, no glow/shadow trick — the mobile layout below now keeps
     the photo and this text band from overlapping at all, so there's
     nothing dark behind the text to fight for contrast against. */
  color: var(--v3-ink);
}
.hero-ghost__headline em { font-style: italic; }
.hero-ghost__cta-block { position: relative; flex: none; max-width: 280px; text-align: start; }
.hero-ghost__cta-block .btn-primary-v3 { gap: 8px; margin-block-end: 14px; }
.hero-ghost__cta-block svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.5; }
.hero-ghost__lede {
  margin: 0;
  font-family: var(--v3-font-body);
  font-size: 14px;
  line-height: 1.5;
  color: var(--v3-on-dark);
  text-shadow: 0 1px 12px rgba(12, 10, 9, 0.35);
}
/* ---- Trust strip — bordered logo grid (v3 — docs/DESIGN-SYSTEM.md §6.3,
   rebuilt 2026-07-30 from a technical-grid reference; logos/border
   reworked 2026-07-31 per client feedback that it still read dull) ----
   Border strategy: grid `gap` on a hairline-colored container background,
   not per-cell `border`. Every gap pixel between cells shows the
   container's own background — one shared line per edge, no doubled
   borders from adjacent cells each drawing their own. Grid lines use
   --v3-hairline-strong (not the plain --v3-hairline used elsewhere) so
   the grid structure itself is legible. Logos are full color at rest
   now (previously grayscale + hover-reveal) — see the img rule below. */
.trust-strip { padding: 96px 0 64px; }

.trust-grid-frame {
  position: relative;
  margin-top: 40px;
  padding: 24px 0;
}

/* Un-padded wrapper the marks position against — .trust-grid-frame's own
   24px top/bottom padding would otherwise get baked into % math for
   .trust-grid__mark (see below), which is only invisible for a
   symmetric-padding case that happens to land exactly on the midpoint
   (desktop's single top:50% mark) and visibly wrong for any other
   fraction (mobile's 25%/75% marks) — this wrapper has zero padding of
   its own so percentages inside it match the grid's real internal lines
   at every breakpoint. */
.trust-grid-wrap {
  position: relative;
  overflow: hidden;
}

/* Spotlight sweep (2026-07-31) — one-shot on first scroll-into-view
   (triggered by main.js adding .is-visible to this wrap), not a
   looping ambient animation, matching the site's established "motion
   plays once" practice (reveal-ins, count-up, flow-line). A plain
   white band wouldn't read against the near-white --v3-canvas cells,
   so this uses a slightly darker tonal band instead (rgba ink at low
   opacity) — a shadow passing over glass rather than a light glint,
   staying monochrome so it doesn't count as new "color" on this
   section. */
.trust-grid__sweep {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(115deg, transparent 42%, rgba(12, 10, 9, 0.05) 50%, transparent 58%);
  /* Rest position sits fully off the left edge — .trust-grid-wrap's
     overflow:hidden clips it, so no opacity toggling is needed to hide
     it before/after the sweep plays. */
  transform: translateX(-100%);
}
.trust-grid-wrap.is-visible .trust-grid__sweep {
  animation: trust-sweep 1.3s ease-out 0.5s forwards;
}
@keyframes trust-sweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .trust-grid__sweep { display: none; }
}

.trust-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--v3-hairline-strong);
  list-style: none;
  margin: 0;
  padding: 0;
}
.trust-grid__cell {
  position: relative;
  background: var(--v3-canvas);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px;
}
/* Hover glint (2026-07-31): logos are full-color at rest now (see the
   img rule below), so hover no longer needs to "reveal" color — instead
   a brief, non-looping peach outline flashes once around just the
   hovered cell's own edge, echoing the ambient trail's color/motion
   idea at a much smaller, one-shot scale. */
.trust-grid__cell::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid var(--v3-orb-peach);
  opacity: 0;
  pointer-events: none;
}
.trust-grid__cell:hover::after {
  animation: trust-cell-glint 0.7s ease-out;
}
@keyframes trust-cell-glint {
  0% { opacity: 0; }
  35% { opacity: 0.9; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .trust-grid__cell::after { animation: none; }
  .trust-grid__cell:hover::after { opacity: 1; }
}
@media (min-width: 881px) {
  .trust-grid__cell:nth-child(8n+2),
  .trust-grid__cell:nth-child(8n+4),
  .trust-grid__cell:nth-child(8n+5),
  .trust-grid__cell:nth-child(8n+7) {
    background: var(--v3-canvas-soft);
  }
}
.trust-grid__cell picture { display: block; }
.trust-grid__cell img {
  max-width: 100%;
  height: 32px;
  width: auto;
}

.trust-grid__mark {
  position: absolute;
  z-index: 1;
  width: 24px;
  height: 24px;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.trust-grid__mark::before,
.trust-grid__mark::after {
  content: '';
  position: absolute;
  background: var(--v3-muted-soft);
}
.trust-grid__mark::before { top: 50%; left: 0; width: 100%; height: 1px; transform: translateY(-50%); }
.trust-grid__mark::after { left: 50%; top: 0; height: 100%; width: 1px; transform: translateX(-50%); }
/* Desktop (4x2 grid): all 3 interior intersections along the single
   internal horizontal line (25%/50%/75%) get a mark. */
.trust-grid__mark--a { top: 50%; left: 25%; }
.trust-grid__mark--b { top: 50%; left: 50%; }
.trust-grid__mark--c { top: 50%; left: 75%; }

@media (max-width: 880px) {
  .trust-strip .section-head { margin-bottom: 28px; }
  .trust-grid-frame { margin-top: 12px; }
  .trust-grid { grid-template-columns: repeat(2, 1fr); }
  .trust-grid__cell { padding: 20px; }
  .trust-grid__cell:nth-child(4n+2),
  .trust-grid__cell:nth-child(4n+3) {
    background: var(--v3-canvas-soft);
  }
  /* Mobile (2x4 grid): all 3 interior intersections on the single
     internal vertical line (25%/50%/75% down) get a mark. */
  .trust-grid__mark--a { top: 25%; left: 50%; }
  .trust-grid__mark--b { top: 50%; left: 50%; }
  .trust-grid__mark--c { top: 75%; left: 50%; }
}

/* ---- Stats row (v3 — docs/DESIGN-SYSTEM.md §6.6) ----
   Replaces the ticker marquee entirely — quiet editorial stats instead
   of a moving band; v3 has no marquee/ticker component anywhere. Real
   numbers only, same 4 facts the ticker carried.
   No top padding on the section itself — the hairline border on
   .stats-row__inner lands right at the Trust Strip boundary instead of
   floating in a stray gap partway down a large empty band.
   Reworked 2026-07-31 (client feedback: read as too silent for a section
   carrying the real proof numbers) — bumped the numerals up toward
   display-mega scale and added a scroll-triggered count-up
   (js/stats-count.js) on the three numeric stats. An orb bloom was
   tried and removed (didn't look good here) — still no cards, no new
   colors, no per-item borders/shadows; the "quiet editorial" flat-row
   structure stays, motion is the only change. */
.stats-row {
  background: var(--v3-canvas);
  padding: 0 0 96px;
}
.stats-row__inner {
  border-top: 1px solid var(--v3-hairline);
  padding-top: 48px;
  text-align: center;
}
.stats-row__eyebrow { margin-bottom: 40px; }
.stats-row__grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
}
.stats-row__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 0 24px;
  border-left: 1px solid var(--v3-hairline);
}
.stats-row__item:first-child {
  border-left: none;
  padding-left: 0;
}
.stats-row__value {
  display: inline-block;
  font-family: var(--v3-font-display);
  font-weight: 400;
  font-size: 64px;
  line-height: 1.05;
  letter-spacing: -1.5px;
  color: var(--v3-ink);
  font-variant-numeric: tabular-nums;
}
/* "Landed" micro-bounce (2026-07-31) — plays once when a stat's
   count-up finishes (js/stats-count.js adds .is-counted), or at the
   same staggered moment for the non-numeric "ICF CCE" item so all 4
   stats settle with the same small sense of weight. display:inline-block
   above is required for the scale transform to apply predictably. */
.stats-row__value.is-counted {
  animation: stats-value-land 0.4s var(--ease);
}
@keyframes stats-value-land {
  0% { transform: scale(1); }
  40% { transform: scale(1.06); }
  100% { transform: scale(1); }
}
.stats-row__label {
  font-family: var(--v3-font-body);
  font-size: 14px;
  color: var(--v3-muted);
}
@media (prefers-reduced-motion: reduce) {
  .stats-row__value.is-counted { animation: none; }
}

/* ---- Industries Served (v3) — numbered hover-active list ----
   Rebuilt 2026-07-31 against a client-supplied reference: numbered rows
   (01-06), one row "active" at a time (index 0 by default, so the
   section never looks inert with nothing hovered), active row gets a
   rounded pill background and a small tilted photo thumbnail anchored
   to its own row (not a shared floating panel tracking position via JS
   — each row owns its thumb, so there's no position math and the thumb
   can never drift over the description column). An always-visible
   circular arrow sits at the row's end (decorative — not a real link
   yet, tilted per the reference rather than a plain 45deg diagonal).
   Active background + hairlines: dark (near-black, `--v3-ink`), used
   directly — replaced an earlier flat-orange (#F54927) attempt, which
   itself replaced a darkened-peach gradient attempt. Solid colors
   transition natively via `background-color` (unlike gradients, which
   are on CSS's discrete animation list and don't interpolate at all
   regardless of `transition` duration — learned the hard way earlier
   this session), so a direct `background-color` transition is enough,
   no crossfade pseudo-element needed.
   The active/hovered row hides its own hairline AND the preceding
   item's hairline (one directly above it) via a JS-managed
   `.is-active-before` class (a `:has(+ sibling)` CSS-only version
   silently failed — nesting `:has()` inside `:has()` invalidates the
   whole selector list it's part of). The very first row has no
   "preceding item," so its own missing hairline is the list's own
   `border-top` — hidden via a separate JS-managed `.is-first-active`
   class on the `<ul>` itself when row 0 is active. */
.industries {
  background: var(--v3-canvas);
  padding: 64px 0 120px;
}
.industries-list-wrap {
  position: relative;
  margin-top: 48px;
}
.industries-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid rgba(12, 10, 9, 0.3);
  transition: border-color 0.6s ease-in-out;
}
.industries-list__item {
  position: relative;
  border-bottom: 1px solid rgba(12, 10, 9, 0.3);
  transition: border-color 0.6s ease-in-out;
}

.industries-list__row {
  position: relative;
  z-index: 0;
  overflow: hidden;
  display: grid;
  grid-template-columns: 48px 240px 280px 1fr 48px;
  align-items: center;
  gap: 48px;
  width: 100%;
  margin: 0;
  padding: 22px 16px;
  border: none;
  border-radius: var(--v3-radius-xl);
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color 0.6s ease-in-out, box-shadow 0.3s ease-in-out;
}
/* Glossy "3D" active/hover treatment (2026-08-01) — same recipe as
   .btn-primary-v3: a 3-stop vertical gradient fill + a gloss-arc
   highlight, layered as pseudo-elements rather than swapped in via
   `background-image` directly. Gradients are on CSS's discrete
   animation list and don't interpolate regardless of `transition`
   duration (learned earlier on this exact row, hence the plain
   `background-color` above) — but `opacity` interpolates fine no
   matter what's underneath it, so these pseudo-elements sit at their
   final gradient permanently and only their opacity fades 0→1. The
   solid `background-color: var(--v3-ink)` transition below is kept
   as-is underneath this, so there's still a solid dark fallback the
   instant browsers paint before/without the gradient layer settling.
   `overflow: hidden` on the row (added above) contains both layers
   within the pill's rounded corners — safe against the row's own
   content (the thumb photo lives on the sibling <li>, not inside this
   button, so it's unaffected). */
.industries-list__row::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--v3-ink) 75%, white) 0%,
    var(--v3-primary) 50%,
    var(--v3-ink) 100%);
  opacity: 0;
  transition: opacity 0.6s ease-in-out;
}
.industries-list__row::after {
  content: '';
  position: absolute;
  z-index: -1;
  inset: 2px;
  border-radius: inherit;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.22), transparent 55%);
  opacity: 0;
  transition: opacity 0.6s ease-in-out;
  pointer-events: none;
}
.industries-list__row:focus-visible {
  outline: 2px solid var(--v3-ink);
  outline-offset: 2px;
}
.industries-list__number {
  font-family: var(--v3-font-display);
  font-weight: 400;
  font-size: 24px;
  color: var(--v3-ink);
  transition: color 0.6s ease-in-out;
}
.industries-list__title {
  font-family: var(--v3-font-display);
  font-weight: 400;
  font-size: 24px;
  color: var(--v3-ink);
  transition: color 0.6s ease-in-out;
}
.industries-list__desc {
  font-family: var(--v3-font-body);
  font-size: 15px;
  line-height: 1.5;
  color: var(--v3-ink);
  transition: color 0.6s ease-in-out;
  /* Narrowed deliberately so it wraps to ~3 lines instead of running
     nearly the row's full width — frees real horizontal room for a
     bigger thumbnail (the empty 1fr column right after this one). */
  max-width: 280px;
}

/* Arrow gets its own stacking context (position:relative + z-index)
   specifically so it paints above the thumb where the two overlap —
   the row/pill's own background must stay BELOW the thumb (else the
   thumb disappears behind the now-opaque pill), so this z-index lives
   only on the arrow, not the whole row. Explicit grid-column: forced
   into the row's actual last track (5) since the new empty 4th column
   (1fr, the thumb's real estate) would otherwise absorb it via normal
   auto-placement. */
.industries-list__arrow {
  grid-column: 5;
  position: relative;
  z-index: 4;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  justify-self: end;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: 1.5px solid var(--v3-hairline-strong);
  transition: background 0.6s ease-in-out, border-color 0.6s ease-in-out, box-shadow 0.6s ease-in-out;
}
.industries-list__arrow svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: var(--v3-ink);
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  transform: rotate(-8deg);
}

/* All "active row" visuals (dark pill, hairline-hiding, on-dark text
   colors, arrow/thumb reveal) are gated to hover-capable pointers —
   matching the exact media condition industries-hover.js itself checks
   before wiring up hover-driven state at all. Row 0 ships with
   `.is-active` baked into the HTML (so it's correct before JS runs on
   desktop), but nothing ever clears it on touch devices, since they run
   the separate tap-accordion path instead — without this gate, row 0
   would show as a permanently "stuck" dark pill on mobile regardless of
   what you tap, and its hairline would stay hidden forever too. Gating
   the whole visual language behind (hover: hover) means touch devices
   never render any of it, full stop, regardless of what classes happen
   to be present in the DOM. */
@media (hover: hover) and (pointer: fine) {
  .industries-list.is-first-active {
    border-top-color: transparent;
  }
  .industries-list__item.is-active,
  .industries-list__item.is-active-before,
  .industries-list__item:has(.industries-list__row:hover),
  .industries-list__item:has(.industries-list__row:focus-visible) {
    border-bottom-color: transparent;
  }
  .industries-list__item.is-active .industries-list__row,
  .industries-list__row:hover,
  .industries-list__row:focus-visible {
    background-color: var(--v3-ink);
    box-shadow: 0 14px 28px -14px rgba(12, 10, 9, 0.45);
  }
  .industries-list__item.is-active .industries-list__row::before,
  .industries-list__row:hover::before,
  .industries-list__row:focus-visible::before,
  .industries-list__item.is-active .industries-list__row::after,
  .industries-list__row:hover::after,
  .industries-list__row:focus-visible::after {
    opacity: 1;
  }
  .industries-list__item.is-active .industries-list__number,
  .industries-list__row:hover .industries-list__number,
  .industries-list__row:focus-visible .industries-list__number { color: var(--v3-on-dark); }
  .industries-list__item.is-active .industries-list__title,
  .industries-list__row:hover .industries-list__title,
  .industries-list__row:focus-visible .industries-list__title { color: var(--v3-on-dark); }
  .industries-list__item.is-active .industries-list__desc,
  .industries-list__row:hover .industries-list__desc,
  .industries-list__row:focus-visible .industries-list__desc { color: var(--v3-on-dark); }
  .industries-list__item.is-active .industries-list__arrow,
  .industries-list__row:hover .industries-list__arrow,
  .industries-list__row:focus-visible .industries-list__arrow {
    background: var(--v3-canvas);
    border-color: transparent;
    box-shadow: 0 6px 16px -6px rgba(12, 10, 9, 0.4);
  }
  .industries-list__item.is-active .industries-list__thumb {
    opacity: 1;
    transform: translateY(0) rotate(-6deg);
  }
}

/* Per-row photo thumbnail — tilted, anchored to this row only
   (position: relative lives on .industries-list__item), so it can only
   ever overlap this row's own top-right corner, never the description
   text. Positioned via `left` (a fixed offset past the description
   column's own edge: 16px row padding + 48px number + 48px gap + 240px
   title + 48px gap + 280px description + 20px breathing room = 704px)
   rather than `right` anchored near the arrow — client feedback was
   that anchoring near the arrow left too much dead space between the
   thumb and the description it's illustrating; enlarging the thumb
   itself (still client feedback: "still small") eats further into that
   same gap. z-index sits above the row's own background (so the thumb
   stays visible, not hidden behind the now-opaque pill) but below the
   arrow. Undecorated: no caption/heading inside it (the mobile
   accordion below still carries the venue caption as text,
   separately). */
.industries-list__thumb {
  position: absolute;
  top: -32px;
  left: 704px;
  width: 240px;
  height: 157px;
  border-radius: var(--v3-radius-lg);
  overflow: hidden;
  border: 1px solid var(--v3-hairline);
  box-shadow: 0 16px 32px -16px rgba(12, 10, 9, 0.35);
  opacity: 0;
  transform: translateY(10px) rotate(-6deg);
  transition: opacity 0.55s ease-in-out, transform 0.55s ease-in-out;
  pointer-events: none;
  z-index: 2;
}
.industries-list__thumb picture { display: block; width: 100%; height: 100%; }
.industries-list__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The 704px fixed `left` only clears the arrow at wide desktop widths —
   every fixed grid column (number, title, description) stays the same
   width regardless of viewport, so only the empty 1fr spacer shrinks,
   and it runs out before the thumb does. Narrower hover-capable desktop
   widths get a smaller thumb anchored near the arrow instead, avoiding
   the collision without touching the wide layout above — re-verified
   via a width sweep after enlarging the thumb (collision range shifted
   slightly wider than the previous size's 880-1010px). */
@media (min-width: 881px) and (max-width: 1119px) {
  .industries-list__thumb {
    left: auto;
    right: 82px;
    width: 150px;
    height: 98px;
    top: -22px;
  }
}

/* Mobile tap-to-expand accordion — hidden entirely on hover-capable
   devices; the per-row thumb above covers that case instead. */
.industries-list__mobile-photo {
  display: none;
}
@media (hover: none) {
  .industries-list__thumb,
  .industries-list__arrow { display: none; }
  .industries-list__mobile-photo {
    display: block;
    overflow: hidden;
    max-height: 0;
    transition: max-height 0.4s var(--ease);
    padding: 0 12px;
  }
  .industries-list__mobile-photo picture {
    display: block;
    border-radius: var(--v3-radius-lg);
    overflow: hidden;
    border: 1px solid var(--v3-hairline);
    margin: 4px 0 8px;
  }
  .industries-list__mobile-photo img { display: block; width: 100%; height: auto; }
  .industries-list__mobile-caption {
    display: block;
    font-family: var(--v3-font-body);
    font-size: 13.5px;
    color: var(--v3-muted);
    margin-bottom: 4px;
  }
  li.is-expanded .industries-list__mobile-photo { max-height: 480px; padding-bottom: 20px; }
}

/* ---- About (v3) — centered editorial statement, no photo ----
   Deliberately image-free: Hero already carries the page's photographic
   weight (the shuffling tile grid), so About leans entirely on scale,
   type and one atmospheric orb instead of competing for visual space
   with a second photo. Single centered column reads as one intentional
   statement rather than a lopsided two-column split.
   No top padding beyond the section head's own margin: Industries
   above already supplies a generous bottom padding (120px), so About
   only needs a modest top value to avoid the double-padding problem
   already fixed once on Stats Row. */
.about {
  background: var(--v3-canvas);
  padding: 64px 0 64px;
}
/* Desktop-only "fit in one screen" treatment (2026-07-31) — the 64px
   top padding stays untouched at every width (it's load-bearing: it's
   what clears the fixed 64px header when someone jumps here via the
   nav's #about link). min-height, not a hard height + overflow:hidden
   like the hero uses — this is normal flowing content, not the hero's
   rigid absolutely-positioned composition, so min-height centers it on
   typical desktop screens without risking clipped text on a short
   viewport or at high zoom. Not applied below 881px per direction —
   mobile/tablet keep natural flow. */
@media (min-width: 881px) {
  .about {
    min-height: 100dvh;
    display: flex;
    align-items: center;
  }
  .about__inner { width: 100%; }
  /* Trimmed alongside the min-height treatment above — none of these
     were earning their keep at the old sizes; mobile/tablet keep the
     original, more generous spacing (not requested there). */
  .about-lead__cta { margin-top: 24px; }
  .about__divider { margin: 36px auto; }
  .about__badges { margin-top: 28px; }
}
.about__inner {
  position: relative;
  max-width: 900px;
  margin: 0 auto;
  text-align: center;
}
/* Centered via left:50%+negative margin, not left:50%+transform: the
   .orb base class's orb-drift animation owns `transform` every frame
   and was silently overriding a translateX(-50%) here, leaving the orb
   ~260px off-center (measured via getBoundingClientRect, not assumed;
   found 2026-07-31 while building — then removing — a similar orb on
   the stats row). */
.about__orb {
  width: 520px;
  height: 520px;
  top: -180px;
  left: 50%;
  margin-left: -260px;
  z-index: 0;
}
.about__content {
  position: relative;
  z-index: 1;
}

/* ---- About lead headline — scroll-reveal statement with per-phrase
   highlights ----
   Adapted from a React/Framer Motion reference into plain CSS
   transitions hooked to the site's existing .reveal/.is-visible
   IntersectionObserver (js/main.js) — no new JS needed; that observer
   already triggers once, unobserves after, and skips straight to
   visible under prefers-reduced-motion, so this component gets all of
   that for free just by living inside .about__inner.reveal.
   Border colors: two of the three highlighted phrases use v3 orb hues
   at low opacity via color-mix() (deliberate, confirmed exception to
   "orbs never as borders" — same pattern already used for the Services
   arrow-fill) since they map directly to the design system's meaning-
   map (§1: peach=corporate, mint=interview prep). "9+ years" isn't tied
   to one program type, so it stays a plain hairline border instead of
   forcing an orb color onto a meaning-map entry that doesn't exist. */
.about-lead {
  font-family: var(--v3-font-display);
  font-weight: 400;
  /* Capped at 3rem/48px (2026-07-31, was 3.75rem/60px) — matches the
     display-xl token exactly rather than an arbitrary number. The old
     60px max exceeded even display-xl and crept toward the hero's own
     h1 scale, which read as oversized for what's meant to be a
     secondary statement, not a second hero. */
  font-size: clamp(1.75rem, 4vw, 3rem);
  line-height: 1.3;
}
.about-lead,
.about-lead__highlight {
  opacity: 0;
  filter: blur(10px);
  transition: opacity 0.7s var(--ease), filter 0.7s var(--ease), transform 0.7s var(--ease);
}
.about-lead { transform: translateY(40px); }
/* One-time 3D "flip-in" (2026-07-31) for the bordered highlight
   phrases specifically, replacing their plain translateY — a
   discrete, one-shot reveal (same stagger timing as before: 0.3s/
   0.6s/0.9s per phrase via the transition-delay rules below), not a
   continuous/looping effect, matching the site's established motion
   restraint (nothing else animates continuously except the very slow
   orb drift). transform-origin: bottom so each box reads as flipping
   up into place, like a flap lifting off the baseline it sits on. */
.about-lead__highlight {
  transform: perspective(500px) rotateX(-90deg) translateY(10px);
  transform-origin: center bottom;
}
.about__inner.is-visible .about-lead {
  opacity: 1;
  filter: blur(0);
  transform: translateY(0);
}
.about__inner.is-visible .about-lead__highlight {
  opacity: 1;
  filter: blur(0);
  transform: perspective(500px) rotateX(0deg) translateY(0);
}
.about-lead__highlight {
  display: inline-block;
  padding: 2px 12px;
  line-height: 1.6;
  border: 1.5px solid var(--v3-hairline-strong);
  border-radius: var(--v3-radius-md);
}
.about-lead__highlight--years { transition-delay: 0.3s; }
.about-lead__highlight--corporate {
  transition-delay: 0.6s;
  border-color: color-mix(in srgb, var(--v3-orb-peach) 55%, transparent);
  background: color-mix(in srgb, var(--v3-orb-peach) 16%, var(--v3-surface));
}
.about-lead__highlight--interview {
  transition-delay: 0.9s;
  border-color: color-mix(in srgb, var(--v3-orb-mint) 55%, transparent);
  background: color-mix(in srgb, var(--v3-orb-mint) 16%, var(--v3-surface));
}

.about-lead__cta {
  display: flex;
  justify-content: center;
  margin-top: 32px;
}

/* Small centered rule marking the shift from the big statement to the
   quieter reading section below — the "assembling itself" moment settles
   before the eye moves on, instead of statement and body copy running
   together with no visual pause. */
.about__divider {
  width: 56px;
  height: 1px;
  background: var(--v3-hairline-strong);
  margin: 56px auto;
}

.about__details {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 32px;
  max-width: 760px;
  margin: 0 auto;
  text-align: left;
}
.about__body {
  font-family: var(--v3-font-body);
  font-size: 16px;
  line-height: 1.6;
  color: var(--v3-body);
}

.about__badges {
  margin-top: 40px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
}

/* ---- Services (v3) — card carousel (rebuilt 2026-07-31) ----
   Rebuilt from a React/Embla/Framer reference into plain CSS scroll-snap
   + IntersectionObserver (js/services-carousel.js) — see DESIGN-SYSTEM.md
   §6.7 for the full adaptation notes (gradient/contrast, ink-not-white
   text, typography choice). */
.services__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: 48px;
}
.services__head-copy { margin-bottom: 0; }

.services-carousel__controls {
  display: flex;
  gap: 12px;
  flex-shrink: 0;
}
/* Same glossy treatment as .btn-primary-v3 (2026-07-31) — identical
   gradient recipe/stops adapted to a circle, same pattern already used
   for .hero-ghost__proof-badge. Requested directly: the carousel
   controls should match the primary button now that the cards beside
   them are dark too. */
.services-carousel__btn {
  position: relative;
  z-index: 0;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.9);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--v3-primary) 80%, white) 0%,
    var(--v3-primary) 45%,
    var(--v3-primary-active) 100%);
  box-shadow: 0 4px 12px -5px rgba(12, 10, 9, 0.35);
  transition: filter 0.2s var(--ease), box-shadow 0.2s var(--ease), transform 0.2s var(--ease);
}
.services-carousel__btn::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: 2px;
  border-radius: 50%;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.24), transparent 55%);
  opacity: 0.85;
  transition: opacity 0.2s var(--ease);
  pointer-events: none;
}
.services-carousel__btn svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: var(--v3-on-dark);
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.services-carousel__btn:hover, .services-carousel__btn:focus-visible {
  filter: brightness(1.08);
}
.services-carousel__btn:hover::before, .services-carousel__btn:focus-visible::before {
  opacity: 1;
}
.services-carousel__btn:active {
  transform: scale(0.94);
  filter: brightness(0.98);
}
@media (prefers-reduced-motion: reduce) {
  .services-carousel__btn, .services-carousel__btn::before { transition: none; }
}

.services-carousel { margin-bottom: 56px; }
.services-carousel__track {
  display: flex;
  gap: 24px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 4px;
  scrollbar-width: none;
  /* Shared vanishing point for every card's hover-spin (2026-07-31) —
     perspective has to live on an ancestor, not the rotating element
     itself, or rotateY renders as a flat orthographic squash instead of
     a real 3D turn. */
  perspective: 1400px;
}
.services-carousel__track::-webkit-scrollbar { display: none; }
.services-carousel__track:focus-visible {
  outline: 2px solid var(--v3-ink);
  outline-offset: 4px;
}

/* 3-up desktop default; overridden to 2-up / 1-up below. This component
   uses the site's actual established breakpoint (single 880px cutoff,
   matching every other v3 grid built so far — trust-grid, industries,
   about__details) plus one extra tablet step at 1023px, rather than
   §8's aspirational 640/1024/1280 table, which nothing else on the site
   follows yet. */
.service-slide {
  position: relative;
  flex: 0 0 calc((100% - 48px) / 3);
  height: 450px;
  border-radius: var(--v3-radius-xxl);
  overflow: hidden;
  scroll-snap-align: start;
  /* Resting edge + shadow (not just on hover) — the §4 "hairline +
     soft drop" card system every other card on the site already gets.
     Shadow deepened slightly (2026-07-31, was -14px/0.14) as part of the
     "raised, glossy" treatment shared with .btn-primary-v3 — a flatter
     shadow read as printed/flat once the card had a gloss highlight on
     top of it; this makes the two consistent. */
  border: 1px solid var(--v3-hairline);
  box-shadow: 0 10px 28px -14px rgba(12, 10, 9, 0.22);
  opacity: 0;
  transform: translateY(50px);
  transition: opacity 0.5s var(--ease), transform 0.3s var(--ease), box-shadow 0.2s var(--ease), filter 0.2s var(--ease);
  /* Reverted to the light "Oceanic Shimmer" version (2026-07-31) — the
     dark-card exploration (jewel-tone HSL blobs against a near-black
     base) went through three correction rounds and still read wrong
     against the client's reference once compared side by side; reverting
     to this already-verified-safe light version rather than iterating
     a fourth time on the dark direction. See §6.7 for the full history.
     3 stacked radial "glow" blobs over a solid base fill, all derived
     from --card-orb via color-mix() so all 5 cards share one
     implementation with only the orb color differing. --glow-3 caps at
     68% — the same peak already verified safe against ink body text
     (lavender, the tightest hue, ~4.71:1). */
  --glow-1: color-mix(in srgb, var(--card-orb, var(--v3-orb-peach)) 30%, var(--v3-canvas-soft));
  --glow-2: color-mix(in srgb, var(--card-orb, var(--v3-orb-peach)) 55%, var(--v3-canvas-soft));
  --glow-3: color-mix(in srgb, var(--card-orb, var(--v3-orb-peach)) 68%, var(--v3-canvas-soft));
  --bg-base: color-mix(in srgb, var(--card-orb, var(--v3-orb-peach)) 45%, var(--v3-canvas-soft));
}
.service-slide.is-visible { opacity: 1; transform: translateY(0); }
.service-slide:focus-within {
  box-shadow: 0 12px 28px -12px rgba(12, 10, 9, 0.2);
}
@media (max-width: 1023px) {
  .service-slide { flex: 0 0 calc((100% - 24px) / 2); }
}

/* Hover-only 3D tilt (2026-07-31, replaces an earlier 360° spin — the
   full rotation didn't play reliably as a hover-triggered CSS
   `animation` in testing, and a fill-mode:forwards animation has to be
   manually unwound via a separate transition on exit, which is fragile.
   A tilt is a plain `transform` change on :hover, so the existing
   `transition: transform 0.3s` above handles both directions for free.
   Scoped to real mouse pointers — touch devices keep today's behavior
   untouched (a tap-triggered tilt would fight with actually tapping the
   "Learn more" link, which nothing else on the site does). Keyboard/
   focus deliberately does NOT tilt — motion tied to tab-focus is a
   vestibular-accessibility problem, so keyboard/screen-reader users
   still get the plain shadow-lift above via :focus-within. */
@media (hover: hover) and (pointer: fine) {
  .service-slide:hover {
    transform: translateY(-6px) rotateX(4deg) rotateY(-6deg);
    box-shadow: 0 22px 44px -16px rgba(12, 10, 9, 0.3);
    filter: brightness(1.03);
  }
}
@media (prefers-reduced-motion: reduce) {
  .service-slide:hover {
    transform: translateY(-4px);
  }
}

/* Gradient / grain / scrim are separate layers (not one combined
   background) so each can be tuned independently — text stays
   ink-colored throughout (never white), so the scrim only needs to be
   a faint grounding touch, not a true dark panel (that would conflict
   with §1/§6.11's "one dark band only" rule — the pre-footer CTA). */
.service-slide__gradient {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  background-color: var(--bg-base);
  background-image:
    radial-gradient(120% 60% at 30% 12%, var(--glow-1) 0%, transparent 65%),
    radial-gradient(120% 60% at 68% 50%, var(--glow-2) 0%, transparent 65%),
    radial-gradient(120% 60% at 40% 88%, var(--glow-3) 0%, transparent 65%);
}
/* Shared grain texture — one data-URI string, byte-identical across
   all 5 cards, so the browser decodes/caches it once regardless of how
   many elements reference it. saturate(0) keeps it neutral gray so it
   reads the same regardless of the card's own hue; mix-blend-mode:
   overlay is what makes it read as tactile grain rather than a visible
   gray haze. */
.service-slide__grain {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
  opacity: 0.18;
  pointer-events: none;
}
/* Gloss-arc highlight (2026-07-31) — same "raised, glossy" language as
   .btn-primary-v3's ::before sheen, adapted to a full card: a soft
   white arc across the top third, blended with soft-light so it
   brightens the pastel gradient underneath instead of washing it flat
   white. Pseudo-element (not a new span) since nothing else needs to
   target it from PHP. */
.service-slide::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.55) 0%, rgba(255, 255, 255, 0.16) 32%, transparent 60%);
  mix-blend-mode: soft-light;
  pointer-events: none;
}
/* Faint grounding scrim — just enough to settle the bottom of the card
   for ink text, not a true dark panel. */
.service-slide__scrim {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: block;
  background: linear-gradient(to top, rgba(12, 10, 9, 0.16), transparent 55%);
  pointer-events: none;
}
.service-slide__content {
  position: relative;
  z-index: 4;
  height: 100%;
  padding: 32px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.service-slide__top {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.service-slide__index {
  font-family: var(--v3-font-body);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--v3-ink);
}
.service-slide__icon {
  display: inline-flex;
  width: 32px;
  height: 32px;
}
.service-slide__icon svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: var(--v3-ink);
  opacity: 0.85;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* h3.service-slide__title, not just .service-slide__title — the global
   ".v3-section h1, h2, h3 { color: var(--v3-ink) }" rule near the top
   of this file has specificity (0,1,1), which beats a bare single-class
   selector (0,1,0) regardless of source order — kept qualified here so
   this rule (identical color, but harmless to keep explicit) always
   wins on source order too. */
h3.service-slide__title {
  font-family: var(--v3-font-display);
  font-weight: 400;
  font-size: 24px;
  line-height: 1.2;
  color: var(--v3-ink);
  margin-bottom: 12px;
}
.service-slide__desc {
  font-family: var(--v3-font-body);
  font-size: 15px;
  line-height: 1.5;
  color: var(--v3-body);
  margin: 0 0 20px;
  max-width: 34ch;
}
.service-slide__cta { display: flex; justify-content: flex-end; }

.services__more-line {
  /* Overrides the sitewide `p { max-width: 62ch; }` base rule, which
     capped this paragraph's own box below the wrap's full width — with
     no `margin: auto`, that narrower box sat flush against the left
     edge of the content column, so `text-align: center` was only ever
     centering the text within an already off-center box. This line is
     short and single-purpose; it doesn't need a reading-width cap. */
  max-width: none;
  text-align: center;
  font-family: var(--v3-font-body);
  font-size: 16px;
  color: var(--v3-body);
}
.services__more-line .link-tertiary-v3 { margin-left: 4px; }

/* ---- Tertiary text link (design system §6.14 button-tertiary-text) ---- */
.link-tertiary-v3 {
  display: inline-block;
  font-family: var(--v3-font-body);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--v3-ink);
  border-bottom: 1px solid transparent;
  transition: border-color 0.2s var(--ease);
}
.link-tertiary-v3:hover, .link-tertiary-v3:focus-visible {
  border-color: var(--v3-ink);
}

/* ---- Expertise index ----
   A real multi-column text index (CSS `columns`, flows down each column
   like a book index), not pills/chips — the headline says "a working
   index," and this is the third bordered-rounded-box motif on the page
   (after Trusted-by's cards and Services' cards) if styled as chips, which
   starts to feel repetitive rather than intentional. */
.expertise__list {
  columns: 3;
  column-gap: 40px;
}
.expertise__list li {
  font-size: 15px;
  color: var(--ink-soft);
  padding: 14px 0;
  border-top: 1px solid var(--line-light);
  break-inside: avoid;
}

/* This section was the last one still on the old v1 palette
   (section--cream-deep, the explicitly-rejected "too dull" cream/green
   theme) even after the list inside it was rebuilt — the surrounding
   band and headline font were left behind by mistake. v3-section
   already makes h1/h2/h3 render in EB Garamond 300 automatically (see
   the rule near the top of this file), so adding that class fixes the
   headline without touching its text/markup at all; canvas-soft here
   keeps the same "alternate band" rhythm the old cream-deep was going
   for, just in the v3 palette. */
.expertise { background: var(--v3-canvas-soft); }

/* prefers-reduced-motion fallback — hidden by default, shown only when
   the marquee below is hidden (see the media query at the bottom of
   this block). Base .expertise__list rules above are untouched. */
.expertise__list--fallback { display: none; }

/* ---- Expertise marquee (rebuilt 2026-07-31, replaces the static list
   as the default presentation) ----
   Full-bleed (100vw breakout) is a deliberate, scoped exception to the
   sitewide "everything lives in .wrap" convention — see DESIGN-SYSTEM.md
   §6.7a. Works because this element is a direct sibling of .wrap (not
   nested inside it): .wrap has no explicit position, so a child of it
   would compute `left: 50%` against .wrap's own constrained width, not
   the viewport; a direct child of the unconstrained <section> computes
   correctly against the true viewport width. body already has
   overflow-x: hidden, so the 100vw width can't introduce a horizontal
   scrollbar. */
.expertise-marquee {
  position: relative;
  left: 50%;
  width: 100vw;
  margin-left: -50vw;
  margin-top: 48px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.expertise-marquee__row {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent 0, black 96px, black calc(100% - 96px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, black 96px, black calc(100% - 96px), transparent 100%);
}
.expertise-marquee__track {
  display: flex;
  align-items: center;
  width: max-content;
  gap: 16px;
  padding: 6px 0;
  animation: expertise-marquee-left 40s linear infinite;
}
.expertise-marquee__row[data-direction="right"] .expertise-marquee__track {
  animation-name: expertise-marquee-right;
}
/* Pure CSS pause on hover/focus — no JS needed for this part. Viewport-
   based pausing (js/expertise-marquee.js) works the same way: it just
   toggles .is-in-view on the outer container, gated below. */
.expertise-marquee__row:hover .expertise-marquee__track,
.expertise-marquee__row:focus-within .expertise-marquee__track {
  animation-play-state: paused;
}
.expertise-marquee:not(.is-in-view) .expertise-marquee__track {
  animation-play-state: paused;
}
@keyframes expertise-marquee-left {
  from { transform: translateX(0); }
  to { transform: translateX(-33.3333%); }
}
@keyframes expertise-marquee-right {
  from { transform: translateX(-33.3333%); }
  to { transform: translateX(0); }
}

.expertise-pill {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding: 6px 24px 6px 6px;
  background: var(--v3-surface);
  border: 1px solid var(--v3-hairline);
  border-radius: var(--v3-radius-pill);
  white-space: nowrap;
  transition: border-color 0.2s var(--ease), transform 0.2s var(--ease);
}
.expertise-pill:hover {
  border-color: var(--v3-hairline-strong);
  transform: scale(1.04);
}
.expertise-pill__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  /* Same 45% mix ratio already verified AA-safe as --bg-base on the
     Services/Testimonial cards (§6.7/§6.11) — reusing it here instead of
     a new number keeps the contrast reasoning consistent. --pill-orb is
     set per capsule via inline style in index.php, cycling through the
     same 5 orb colors/order as those cards. */
  background: color-mix(in srgb, var(--pill-orb, var(--v3-orb-peach)) 45%, var(--v3-surface-strong));
  flex-shrink: 0;
}
.expertise-pill__icon svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: var(--v3-ink);
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.expertise-pill__label {
  font-family: var(--v3-font-body);
  font-size: 16px;
  font-weight: 500;
  color: var(--v3-ink);
}

/* Stop all scrolling entirely under reduced motion, fall back to the
   original static list — content stays fully readable without relying
   on animation, and nobody who's asked to avoid motion gets it forced
   on them. */
@media (prefers-reduced-motion: reduce) {
  .expertise-marquee { display: none; }
  .expertise__list--fallback { display: block; }
}

/* ---- Methodology (v3) — feature panel (rebuilt 2026-07-31) ----
   Adapted from a React/shadcn reference into plain HTML/CSS, fully
   static (no JS — no carousel/autoplay/crossfade for this component).
   See DESIGN-SYSTEM.md §6.8 for the badge/photo/microcopy decisions.
   No longer a bordered/padded container (removed per client feedback,
   2026-07-31) — content just sits directly in the section's own .wrap,
   same as every other section on the site. */
.methodology-panel {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 48px;
  /* stretch (was center) so the media column's own height matches the
     content column's real (checklist-driven) height — align-items:
     center was leaving a single short 16:9 photo floating in a lot of
     dead space above/below it. */
  align-items: stretch;
}
.methodology-panel__content .eyebrow { margin-bottom: 16px; }
.methodology-panel__lede {
  font-family: var(--v3-font-body);
  font-size: 16px;
  line-height: 1.6;
  color: var(--v3-body);
  margin: 16px 0 0;
}
/* Sequential "rail" (2026-08-01, eased+dimmed pass same day) —
   js/methodology-checklist.js measures each icon's real Y-center (item
   heights vary with description length, so this can't be assumed from
   CSS alone) and animates a fill line growing top-to-bottom behind the
   icons, lighting each one up to the glossy dark "button style"
   treatment (same recipe as the Industries active row) as the line
   reaches it. One-shot on first scroll-into-view, matching every other
   motion module on this site — see js/methodology-checklist.js for the
   full timing writeup.
   No CSS transition on the fill itself (was `height 1.6s linear` in
   the first pass) — it's now driven per-frame via requestAnimationFrame
   so an eased (not constant-speed) curve can be used while keeping each
   icon's "light up" moment exactly synced to the line's real drawn
   position; a CSS transition would fight a per-frame manual update
   (visibly lag chasing a moving target) rather than cooperate with it. */
.methodology-panel__checklist-wrap {
  position: relative;
}
.methodology-panel__rail {
  position: absolute;
  left: 15px;
  top: 0;
  width: 2px;
  height: 0;
  background: var(--v3-hairline-strong);
  z-index: 0;
  pointer-events: none;
}
.methodology-panel__rail-fill {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 0;
  background: var(--v3-ink);
}
.methodology-panel__checklist {
  display: flex;
  flex-direction: column;
  gap: 24px;
  margin-top: 32px;
}
.methodology-panel__checklist li {
  display: flex;
  align-items: flex-start;
  gap: 14px;
}
/* Grayed-until-reached (2026-08-01) — dims icon+title+desc together for
   items the line hasn't reached yet, brightening in sync with the icon
   going glossy-dark. Dimming is added/removed by JS only (never a CSS
   default) — if JS fails or is disabled, .is-pending is simply never
   applied and the checklist stays fully readable, matching how every
   other motion module on this site degrades safely with no animation
   rather than getting stuck in a mid-animation state. */
.methodology-panel__check-item {
  transition: opacity 0.5s var(--ease);
}
.methodology-panel__check-item.is-pending {
  opacity: 0.35;
}
.methodology-panel__check {
  position: relative;
  z-index: 1;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--v3-surface-strong);
  transition: background-color 0.4s var(--ease), box-shadow 0.4s var(--ease);
}
/* Glossy fill + gloss-arc, layered as pseudo-elements rather than
   swapped in via `background-image` directly — gradients don't
   interpolate via `transition` regardless of duration, `opacity` does,
   so these sit permanently at their final gradient and only fade in.
   Same technique as .industries-list__row (see DESIGN-SYSTEM.md
   §6.3a for the full rationale). */
.methodology-panel__check::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--v3-ink) 75%, white) 0%,
    var(--v3-primary) 50%,
    var(--v3-ink) 100%);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}
.methodology-panel__check::after {
  content: '';
  position: absolute;
  inset: 1px;
  z-index: -1;
  border-radius: inherit;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.28), transparent 55%);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}
.methodology-panel__check.is-lit {
  background-color: var(--v3-ink);
  box-shadow: 0 6px 14px -6px rgba(12, 10, 9, 0.4);
}
.methodology-panel__check.is-lit::before,
.methodology-panel__check.is-lit::after {
  opacity: 1;
}
.methodology-panel__check svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: var(--v3-ink);
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke 0.4s var(--ease);
}
.methodology-panel__check.is-lit svg {
  stroke: var(--v3-on-dark);
}
@media (prefers-reduced-motion: reduce) {
  .methodology-panel__rail-fill,
  .methodology-panel__check,
  .methodology-panel__check::before,
  .methodology-panel__check::after,
  .methodology-panel__check svg,
  .methodology-panel__check-item {
    transition: none;
  }
}
.methodology-panel__check-text {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-top: 4px;
}
.methodology-panel__check-title {
  font-family: var(--v3-font-body);
  font-size: 17px;
  font-weight: 500;
  color: var(--v3-ink);
}
.methodology-panel__check-desc {
  font-family: var(--v3-font-body);
  font-size: 14px;
  line-height: 1.4;
  color: var(--v3-muted);
}
/* Single photo + credential plate (2026-08-01, was two stacked photos).
   The two-stacked-photo version paired training-corporate-team-session.jpg
   with seminar-energy.jpg — but seminar-energy also runs (smaller) in the
   Gallery masonry directly below this section, so on mobile (where both
   frames stack full-width above the gallery) the same two photos repeated
   within a short scroll, reading as filler. That's the exact "repeated
   photographs" complaint this project has reverted for twice before (see
   docs/DESIGN-SYSTEM.md history) — dropping seminar-energy here removes
   one of the two repeats and restores training-corporate-team-session as
   Methodology's sole dedicated photo, matching every other single-photo
   section (Services CTA wash, Industries Served). The credential plate
   fills the height a second photo used to (flex: 1 on the photo frame
   only, plate is fixed-content height) with real, already-approved copy
   (docs/CONTENT.md credentials block) instead of a second stock-feeling
   image. */
.methodology-panel__media {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.methodology-panel__media-frame {
  flex: 1;
  display: block;
  min-height: 0;
  border-radius: var(--v3-radius-xl);
  overflow: hidden;
  border: 1px solid var(--v3-hairline);
}
.methodology-panel__media-frame picture,
.methodology-panel__media-frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Framed like the photo above it (same radius/hairline) so the two read
   as one composed pair rather than a photo with an unrelated box glued
   underneath. flex: none — fixed to its own content height, unlike the
   photo frame's flex: 1, so the photo (not this plate) absorbs any
   change in the checklist column's height. */
.methodology-panel__credential {
  flex: none;
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 20px 24px;
  border-radius: var(--v3-radius-xl);
  border: 1px solid var(--v3-hairline);
  background: var(--v3-surface);
}
.methodology-panel__credential-label {
  font-family: var(--v3-font-display);
  font-weight: 400;
  font-size: 28px;
  color: var(--v3-ink);
}
.methodology-panel__credential-desc {
  font-family: var(--v3-font-body);
  font-size: 14px;
  color: var(--v3-muted);
}

/* ---- Photo Gallery ("In The Room") — masonry, fade-in-on-view
   (rebuilt 2026-08-01, replaces the bento grid + lightbox) ----
   CSS `columns` + `break-inside: avoid`, not `grid-template-rows:
   masonry` — the latter only ships in Firefox today, not Chrome/Safari,
   so columns is the only technique that actually works cross-browser
   right now. Breakpoints (1023px tablet / 880px mobile) reuse the
   Services carousel's existing 3-up/2-up/1-up steps rather than
   introducing a new breakpoint pair. No lightbox this time — every
   image is a plain <img>, no click behavior. */
.gallery-masonry {
  columns: 3;
  column-gap: 20px;
  margin-top: 48px;
}
.gallery-masonry__item {
  break-inside: avoid;
  margin-bottom: 20px;
  border-radius: var(--v3-radius-xl);
  overflow: hidden;
  border: 1px solid var(--v3-hairline);
  background: var(--v3-surface);
}
.gallery-masonry__img {
  display: block;
  width: 100%;
  height: auto;
  opacity: 0;
  /* Deliberate exception to var(--ease): a symmetric ease-in-out reads as
     a soft photographic fade, matching the reference behavior this
     section was rebuilt from — var(--ease)'s expo-out curve front-loads
     nearly all visible change into the first ~150ms (see the Industries
     list's identical reasoning, §6.3a), which would read as a snap
     rather than a fade for a full-second transition like this one. */
  transition: opacity 1s ease-in-out;
}
.gallery-masonry__img.is-visible {
  opacity: 1;
}

@media (max-width: 1023px) {
  .gallery-masonry { columns: 2; }
}
@media (max-width: 880px) {
  .gallery-masonry { columns: 1; margin-top: 32px; }
}

@media (prefers-reduced-motion: reduce) {
  .gallery-masonry__img {
    opacity: 1;
    transition: none;
  }
}

/* ---- Testimonials (v3) — static grid (reverted from auto-scroll
   2026-08-10, docs/DESIGN-SYSTEM.md §6.10) ----
   Gated empty-state section, same pattern as Articles below: hidden via
   PHP if $testimonials is ever emptied.
   This was a continuously auto-scrolling vertical-column marquee
   (pure CSS @keyframes loop, mirroring the Expertise marquee's
   technique) from 2026-08-01 until 2026-08-10, when the client reported
   the constant movement made the quotes hard to read. Reverted to a
   plain static grid — the exact markup/CSS that already existed as the
   prefers-reduced-motion fallback, now promoted to the only treatment.
   No JS, no animation, no duplicate DOM. */
.testimonials .section-head p { color: var(--v3-muted); }

.testimonial-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
  margin-top: 48px;
}
@media (max-width: 639px) {
  .testimonial-grid { grid-template-columns: 1fr; }
}

/* Pastel gradient-wash cards (2026-08-01, second revision) — reuses the
   Services cards' exact recipe (§6.7: solid base + 3 stacked radial glow
   blobs + shared grain texture + gloss-arc), not a new pattern. Two flat
   colors were tried first and both rejected on sight: white read as
   flat/dull (matching this project's own v1 "too dull" history), and a
   dark --v3-ink/--v3-primary gradient (see the CHANGELOG entry directly
   above this one) didn't land either — a plain color fill of any hue was
   the actual problem, not specifically light vs. dark. This leans on the
   site's real signature device (soft orb atmosphere) instead of trying a
   third flat fill. --card-orb set per card via inline style in index.php
   (cycles through the same 5 orb colors Services uses, in the same
   order), --glow-1/2/3 and --bg-base derived from it via color-mix() —
   byte-identical formula to Services, including the 68% peak already
   verified AA-safe for ink text (lavender, the tightest hue, ~4.71:1). */
.testimonial-card {
  position: relative;
  overflow: hidden;
  border-radius: var(--v3-radius-xl);
  padding: 32px;
  border: 1px solid var(--v3-hairline);
  box-shadow: 0 10px 28px -14px rgba(12, 10, 9, 0.22);
  --glow-1: color-mix(in srgb, var(--card-orb, var(--v3-orb-peach)) 30%, var(--v3-canvas-soft));
  --glow-2: color-mix(in srgb, var(--card-orb, var(--v3-orb-peach)) 55%, var(--v3-canvas-soft));
  --glow-3: color-mix(in srgb, var(--card-orb, var(--v3-orb-peach)) 68%, var(--v3-canvas-soft));
  --bg-base: color-mix(in srgb, var(--card-orb, var(--v3-orb-peach)) 45%, var(--v3-canvas-soft));
}
/* Gradient/grain layers are real DOM spans (index.php), matching how
   Services builds this same recipe, rather than pseudo-elements — keeps
   the two components' markup/CSS directly comparable. Content (blockquote,
   meta) needs an explicit z-index above these or the absolutely-positioned
   layers paint over the text — Services solves this with a dedicated
   `__content` wrapper; testimonial cards only have two content children,
   so each gets `position: relative` + z-index directly instead of adding
   a wrapper div. */
.testimonial-card__gradient {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  background-color: var(--bg-base);
  background-image:
    radial-gradient(120% 60% at 30% 12%, var(--glow-1) 0%, transparent 65%),
    radial-gradient(120% 60% at 68% 50%, var(--glow-2) 0%, transparent 65%),
    radial-gradient(120% 60% at 40% 88%, var(--glow-3) 0%, transparent 65%);
}
.testimonial-card__grain {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
  opacity: 0.18;
  pointer-events: none;
}
.testimonial-card::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.55) 0%, rgba(255, 255, 255, 0.16) 32%, transparent 60%);
  mix-blend-mode: soft-light;
  pointer-events: none;
}
.testimonial-card blockquote {
  position: relative;
  z-index: 3;
  font-family: var(--v3-font-display);
  font-style: italic;
  font-weight: 400;
  font-size: 24px;
  line-height: 1.3;
  color: var(--v3-ink);
  margin: 0 0 24px;
  max-width: none;
}
.testimonial-card__meta {
  position: relative;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 12px;
}
.testimonial-card__avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--v3-radius-pill);
  background: var(--v3-surface-strong);
  overflow: hidden;
  flex-shrink: 0;
  border: 1px solid rgba(255, 255, 255, 0.7);
}
.testimonial-card__avatar picture { display: block; width: 100%; height: 100%; }
.testimonial-card__avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }
.testimonial-card__who { display: flex; flex-direction: column; }
.testimonial-card__name {
  font-family: var(--v3-font-body);
  font-size: 16px;
  font-weight: 500;
  color: var(--v3-ink);
}
.testimonial-card__role {
  font-family: var(--v3-font-body);
  font-size: 14px;
  color: var(--v3-muted);
}


/* Articles preview card styling removed 2026-08-01 — it was the v1
   dark/green card system, only reachable through the Articles section,
   which is hidden pending Phase 2 (see index.php's $articles gating).
   Build fresh v3 styling for it then, matching the rest of the site,
   rather than reviving this. */

/* ---- Contact (v3, rebuilt 2026-08-01) ----
   Two-column split adapted from a client reference: left column is plain
   editorial content (no bordered box — matches this site's own precedent
   of removing Methodology's bordered panel after client feedback that
   boxed containers felt off), right column is a contained white card
   holding the form only. Deliberately stays on the canvas band per
   DESIGN-SYSTEM.md §6.12 rather than becoming the page's one dark band
   (§6.11/§3) — that budget is reserved for the separate, still-unbuilt
   pre-footer CTA, not spent on a functional form section. */
.contact__split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 64px;
  align-items: start;
}
.contact__lede {
  font-family: var(--v3-font-body);
  font-size: 16px;
  line-height: 1.6;
  color: var(--v3-body);
  margin-top: 16px;
}

.contact__checklist {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 32px 0;
}
.contact__checklist li {
  display: flex;
  align-items: flex-start;
  gap: 14px;
}
.contact__check-icon {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin-top: 2px;
  border-radius: 50%;
  background: var(--v3-surface-strong);
}
.contact__check-icon svg {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: var(--v3-ink);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.contact__checklist li > span:last-child {
  font-family: var(--v3-font-body);
  font-size: 15px;
  line-height: 1.5;
  color: var(--v3-body);
}

.contact__direct {
  font-family: var(--v3-font-body);
  font-size: 15px;
  color: var(--v3-muted);
}
.contact__direct .text-link-v3 { margin-left: 4px; }

.contact__form-card {
  background: var(--v3-surface);
  border: 1px solid var(--v3-hairline);
  border-radius: var(--v3-radius-xl);
  padding: 40px;
  /* Same "hairline + soft drop" tier as the testimonial cards — §4's
     single shadow tier, the section's only source of depth. */
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
}

.form-field { margin-bottom: 24px; }
fieldset.form-field {
  border: 0;
  padding: 0;
  margin: 0 0 24px;
  min-width: 0;
}
.form-field label,
.form-field__label {
  display: block;
  font-family: var(--v3-font-body);
  font-size: 13px;
  font-weight: 500;
  color: var(--v3-muted);
  margin-bottom: 8px;
}
.form-field input,
.form-field textarea {
  width: 100%;
  height: 44px;
  background: var(--v3-surface);
  border: 1px solid var(--v3-hairline-strong);
  border-radius: var(--v3-radius-md);
  padding: 0 16px;
  font-family: var(--v3-font-body);
  font-size: 16px; /* under 16px triggers an unwanted zoom-in on focus in iOS Safari */
  color: var(--v3-ink);
  transition: border-color 0.2s var(--ease);
}
.form-field input:focus-visible,
.form-field textarea:focus-visible {
  border-color: var(--v3-ink);
  outline: 2px solid var(--v3-ink);
  outline-offset: 2px;
}
.form-field textarea {
  height: auto;
  min-height: 120px;
  padding: 12px 16px;
  resize: vertical;
}
.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}
.form-honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Accessible chip/pill single-select — real radio inputs (visually
   hidden, not display:none, so they stay tabbable), styled label as the
   pill. Selected state reuses the same "active = solid ink fill" language
   already established by the Industries row and Methodology checkmarks,
   not a new color pattern. */
.contact-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.contact-chip {
  position: relative;
  cursor: pointer;
}
.contact-chip input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}
.contact-chip span {
  display: inline-flex;
  align-items: center;
  padding: 9px 18px;
  border-radius: var(--v3-radius-pill);
  border: 1px solid var(--v3-hairline-strong);
  background: var(--v3-surface);
  font-family: var(--v3-font-body);
  font-size: 14px;
  font-weight: 500;
  color: var(--v3-ink);
  transition: background-color 0.2s var(--ease), border-color 0.2s var(--ease), color 0.2s var(--ease);
}
.contact-chip:hover span { border-color: var(--v3-ink); }
.contact-chip input:checked ~ span {
  background: var(--v3-ink);
  border-color: var(--v3-ink);
  color: var(--v3-on-primary);
}
.contact-chip input:focus-visible ~ span {
  outline: 2px solid var(--v3-ink);
  outline-offset: 2px;
}

.form-notice, .form-status {
  background: var(--v3-surface-strong);
  border: 1px solid var(--v3-hairline-strong);
  border-radius: var(--v3-radius-md);
  padding: 14px 18px;
  font-family: var(--v3-font-body);
  font-size: 14px;
  color: var(--v3-ink);
  margin-bottom: 24px;
}
.form-status[hidden] { display: none; }

/* ---- Footer (v3) — rounded dark panel ----
   Confirmed exception (2026-08-01): full v3 migration of the footer
   (was still on the v1 shell, §0's migration table) using --v3-surface-dark
   as a rounded-top-corner panel, distinct from the "ONE optional dark band"
   default in §0 ("No dark gradient panels" / dark reserved for a not-yet-
   built pre-footer CTA) — this is a second, deliberate use of that token,
   requested directly and overriding that default guidance rather than an
   oversight. Layout inspired by a client-supplied React/Framer reference,
   rebuilt here with plain CSS + the site's existing IntersectionObserver
   .reveal system (js/main.js) instead of motion/react. */
.site-footer {
  position: relative;
  padding: 88px 0 32px;
  background: var(--v3-surface-dark);
  border-radius: var(--v3-radius-xxl) var(--v3-radius-xxl) 0 0;
  overflow: hidden;
}
/* Thin blurred highlight line at the panel's top edge — neutral/white
   tinted rather than an orb color, since the footer isn't an established
   color-moment zone (orbs stay "the only color moments" elsewhere). */
.site-footer__glow {
  position: absolute;
  top: 0;
  left: 50%;
  width: min(560px, 70%);
  height: 1px;
  transform: translateX(-50%);
  background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.5), transparent);
  filter: blur(2px);
  pointer-events: none;
}

.site-footer__grid {
  display: grid;
  grid-template-columns: 1.4fr 0.8fr 1fr;
  gap: 40px;
  margin-bottom: 48px;
}

/* Same script brand mark as the header (2026-07-31), now recolored for
   the dark panel via --v3-on-dark rather than --ink. */
.site-footer__wordmark {
  font-family: 'Parisienne', cursive;
  font-size: 36px;
  color: var(--v3-on-dark);
  margin-bottom: 12px;
  line-height: 1.2;
}
.site-footer__tagline { max-width: 40ch; color: var(--v3-on-dark-soft); margin-bottom: 20px; }
.site-footer__copyright { font-size: 13.5px; color: var(--v3-on-dark-soft); }

.site-footer__col { display: flex; flex-direction: column; gap: 12px; }
.site-footer__col .eyebrow { color: var(--v3-on-dark-soft); margin-bottom: 4px; }
.site-footer__col ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.site-footer__col a { color: var(--v3-on-dark-soft); transition: color 0.2s var(--ease); }
.site-footer__col a:hover, .site-footer__col a:focus-visible { color: var(--v3-on-dark); }

.site-footer__email { font-size: 15px; }

.site-footer__bottom {
  padding-top: 24px;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 16px;
  font-size: 13.5px;
  color: var(--v3-on-dark-soft);
}
.site-footer__credit a { color: inherit; transition: color 0.2s var(--ease); }
.site-footer__credit a:hover, .site-footer__credit a:focus-visible { color: var(--v3-on-dark); }
.site-footer__top-link { color: var(--v3-on-dark-soft); transition: color 0.2s var(--ease); }
.site-footer__top-link:hover, .site-footer__top-link:focus-visible { color: var(--v3-on-dark); }

/* Reveal variant: drop-in + blur, layered on top of the sitewide
   .reveal/.is-visible system (js/main.js already drives the observer,
   stagger via each column's data-reveal-delay, and the no-JS/reduced-
   motion fallbacks — this only overrides the resting/visible values). */
.site-footer .reveal {
  transform: translateY(-8px);
  filter: blur(4px);
  transition: opacity 0.8s var(--ease), transform 0.8s var(--ease), filter 0.8s var(--ease);
}
.site-footer .reveal.is-visible { transform: translateY(0); filter: blur(0); }

@media (max-width: 880px) {
  .site-footer__grid { grid-template-columns: 1fr 1fr; }
  .site-footer__brand { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  .site-footer__grid { grid-template-columns: 1fr; gap: 32px; }
}

/* ---- Scroll reveal ---- */
.reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
}
.reveal.is-visible { opacity: 1; transform: translateY(0); }

/* ---- Reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  .site-footer .reveal { filter: none; }
  .about-lead, .about-lead__highlight { opacity: 1; filter: none; transform: none; transition: none; }
  .flow-line path { animation: none; stroke-dashoffset: 0; }
  .hero-in, .hero-in--scale, .hero__scroll-cue {
    opacity: 1;
    transform: none;
    filter: none;
    animation: none;
  }
  .hero__scroll-line::after { animation: none; }
}

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 880px) {
  .wrap { padding: 0 24px; }
  h1 { font-size: 40px; }
  h2 { font-size: 30px; }
  .section-pad { padding: 72px 0; }
  /* Trust Strip, Stats Row and Industries carry their own hand-tuned
     desktop padding (96px/96px/120px) instead of .section-pad's scale,
     and never got a matching mobile reduction — full desktop-scale
     padding stacked with the neighboring section's own padding produced
     dead-space gaps up to 216px between sections, well past the ~144px
     rhythm every other section boundary keeps at this breakpoint
     (measured via getBoundingClientRect, not assumed). Bringing each
     down to 64px here — the mobile value the design system already
     specifies for this rhythm (DESIGN-SYSTEM.md's spacing scale line:
     "96px padding (64px mobile)") — re-aligns them with every other
     section boundary on the page. */
  .trust-strip { padding: 64px 0; }
  .stats-row { padding-bottom: 64px; }
  .industries { padding-bottom: 64px; }
  .hero__scroll-cue { display: none; }
  /* At the true 320px minimum, 30px left only ~9px clearance to the
     hamburger button — not overlapping, but tight enough to feel
     precarious rather than designed. Smaller size here gives real
     breathing room (confirmed via getBoundingClientRect at 320px). */
  .wordmark { font-size: 24px; }
  .expertise__list { columns: 2; }
  .expertise-marquee { margin-top: 32px; gap: 14px; }
  .expertise-marquee__row {
    -webkit-mask-image: linear-gradient(to right, transparent 0, black 32px, black calc(100% - 32px), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, black 32px, black calc(100% - 32px), transparent 100%);
  }
  .expertise-pill { padding: 5px 18px 5px 5px; gap: 10px; }
  .expertise-pill__icon { width: 44px; height: 44px; }
  .expertise-pill__icon svg { width: 19px; height: 19px; }
  .expertise-pill__label { font-size: 14.5px; }

  .site-nav {
    position: absolute;
    top: 100%; left: 0; right: 0;
    background: var(--v3-canvas);
    border-bottom: 1px solid var(--v3-hairline);
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding: 8px 24px 24px;
    display: none;
  }
  .site-nav.is-open { display: flex; }
  .site-nav a:not(.nav-cta) {
    width: 100%; padding: 14px 0;
    border-bottom: 1px solid var(--v3-hairline);
    color: var(--v3-ink); opacity: 1;
  }
  .site-nav .nav-cta { margin-top: 16px; }
  .nav-toggle { display: flex; }

  .hero { padding-top: calc(64px + 56px); }
  .hero__grid { grid-template-columns: 1fr; gap: 32px; }
  .hero__text { max-width: none; }
  .hero__lede { max-width: none; }
  .hero__orb { width: 320px; height: 320px; top: -110px; left: -100px; }
  .hero__tiles-wrap { max-width: 340px; margin-inline: auto; }
  .hero__tiles { gap: 8px; }
  .hero__tiles-caption { text-align: start; }
  /* Back to the large, bottom-anchored absolute photo (the normal-flow
     small-photo rebuild that briefly replaced this made her look tiny
     and lost the composition's presence) — contrast where the headline
     overlaps her photo is now handled by a mobile-specific heading
     color + a taller, stronger gradient instead of a layout change. */
  .hero-ghost__cutout-wrap { height: 82%; }
  .hero-ghost__proof { padding: 10px 14px; gap: 8px; }
  .hero-ghost__proof-avatars picture,
  .hero-ghost__proof-badge { width: 26px; height: 26px; }
  .hero-ghost__bottom { flex-direction: column; align-items: flex-start; gap: 16px; inset-block-end: 20px; }
  .hero-ghost__cta-block { max-width: none; }
  /* Taller / stronger than the desktop version: the transparent zone
     shrinks so color reaches further up the section, giving the
     overlapping headline a more consistently mid-toned backdrop to sit
     on rather than crossing from near-white canvas into dark photo. */
  .hero-ghost__gradient {
    background:
      radial-gradient(ellipse 70% 65% at 20% 100%, rgba(232, 184, 196, 0.6), transparent 65%),
      radial-gradient(ellipse 70% 65% at 80% 100%, rgba(201, 128, 79, 0.6), transparent 65%),
      radial-gradient(ellipse 55% 55% at 50% 105%, rgba(230, 172, 126, 0.5), transparent 65%),
      linear-gradient(180deg, rgba(244, 197, 168, 0) 0%, rgba(244, 197, 168, 0) 18%, rgba(180, 105, 60, 0.45) 100%);
  }
  h1.hero-ghost__headline { color: var(--v3-canvas); }

  .stats-row__value { font-size: 48px; }

  /* 2-column layout: a left-column/right-column divider reads correctly
     as a border-left, but a stray border-left would also land on
     whichever item starts each new row (no column to its left there) —
     swap those rows' dividers to a border-top instead. With 5 items in
     2 columns that's item 3 (start of row 2) and item 5 (start of row 3,
     alone). */
  .stats-row__grid { grid-template-columns: repeat(2, 1fr); row-gap: 32px; }
  .stats-row__item:nth-child(3),
  .stats-row__item:nth-child(5) {
    border-left: none;
    padding-left: 0;
    border-top: 1px solid var(--v3-hairline);
    padding-top: 24px;
  }

  /* Stale-block fix: this hadn't been touched since the desktop grid grew
     to 5 columns and .industries-list__desc picked up a 280px max-width
     (both meant for the wide layout only) — carrying that width limit
     into a ~300-340px mobile content area left the description reading
     narrow/misaligned rather than using the row's real available width,
     and 8px of horizontal row padding was tighter than this site's usual
     mobile card/row padding (20px, matching .trust-grid__cell etc). */
  .industries-list__row {
    grid-template-columns: 32px 1fr;
    grid-template-areas: "number title" "desc desc";
    row-gap: 8px;
    column-gap: 12px;
    padding: 20px 16px;
  }
  .industries-list__number { grid-area: number; font-size: 20px; }
  .industries-list__title { grid-area: title; font-size: 20px; }
  .industries-list__desc { grid-area: desc; font-size: 14px; max-width: none; }
  .industries-list__arrow { display: none; }

  /* Every other section rides .section-pad, which drops to 72px vertical
     padding at this breakpoint — .about has its own padding rule (it
     needed a lighter top/heavier bottom split on desktop) and had never
     picked up the matching mobile reduction, leaving its 120px bottom
     padding stacked on top of Services' own 72px top padding for ~48px
     of dead space not seen at any other section boundary on the site
     (confirmed via getBoundingClientRect, not assumed). */
  .about { padding: 64px 0 72px; }
  .about__details { grid-template-columns: 1fr; gap: 20px; }
  .about__orb { width: 320px; height: 320px; top: -100px; margin-left: -160px; }

  /* Media stacks BELOW content on mobile (spec'd, opposite of the old
     figure-first layout this replaced) — natural DOM order already
     puts content first, so no `order` override is needed here at all. */
  .methodology-panel { grid-template-columns: 1fr; gap: 32px; }
  /* flex:1/object-fit:cover on the photo frame only earns its keep when
     it's dividing the checklist column's real height beside it — at this
     single-column mobile layout the photo stacks below the content
     instead, so it's dropped in favor of the photo's own natural aspect
     ratio, matching every other stacked photo on the site at mobile
     width. The credential plate is already flex:none/content-height, so
     it needs no mobile override. */
  .methodology-panel__media-frame { flex: none; }
  .methodology-panel__media-frame img { height: auto; object-fit: initial; }

  .services__head { flex-direction: column; align-items: flex-start; gap: 20px; }
  .service-slide { flex: 0 0 100%; }
  .articles__grid { grid-template-columns: 1fr; }
  /* Natural DOM order already puts the reassurance content above the
     form card — no `order` override needed, same reasoning as
     .methodology-panel above. */
  .contact__split { grid-template-columns: 1fr; gap: 40px; }
  .contact__form-card { padding: 28px; }
}

@media (max-width: 560px) {
  .wrap { padding: 0 20px; }
  h1 { font-size: 34px; }
  .hero__orb { width: 260px; height: 260px; }
  .hero-ghost__name { display: none; }
  h1.hero-ghost__headline { font-size: clamp(1.625rem, 9vw, 2.25rem); max-width: 320px; }
  .hero-ghost__cutout-wrap { height: 66%; }
  .hero-ghost__proof { inset-inline-end: 16px; }
  .hero-ghost__bottom { inset-block-end: 36px; gap: 12px; }
  .stats-row__value { font-size: 40px; }
  .stats-row__grid { grid-template-columns: 1fr; row-gap: 24px; }
  .stats-row__item,
  .stats-row__item:nth-child(3) { border-left: none; padding-left: 0; border-top: none; padding-top: 0; }
  .stats-row__item:not(:first-child) { border-top: 1px solid var(--v3-hairline); padding-top: 20px; }
  .trust-strip__row { gap: 28px; }
  .form-row { grid-template-columns: 1fr; }
  .expertise__list { columns: 1; }
}
