/*
  Jopera Consult — site styles
  Every color and font value below resolves to a variable generated in
  tokens.css from color-tokens.json (see /design-tokens-rule.md).
  Non-token constants (container width, breakpoints, z-index scale,
  aspect ratios) are layout math, not brand values, and are defined here.
*/

/* ---------------------------------------------------------------------
   Layout constants (not design tokens — see note above)
------------------------------------------------------------------------ */
:root {
  --container-max: 75rem; /* 1200px content rail */
  --header-height: 4.5rem;
  --z-header: 100;
  --z-nav-panel: 90;
  --z-hero-content: 2;
  --z-hero-scrim: 1;
  --z-cursor-dot: 999;
  --motion-fast: 150ms;
  --motion-base: 250ms;
  --motion-slow: 400ms;
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
}

/* Mobile-only header trim (per request). Scoped to max-width: 1023px,
   not the usual 767px — that's the actual mobile/desktop split for
   the header itself: the hamburger .nav-toggle only gives way to the
   full inline .nav-links row at min-width: 1024px (see further down),
   so this covers the entire range that still shows the compact
   hamburger header, not just phones. Redefining the custom property
   here (not just .site-header's own height below) means every other
   rule keyed off --header-height automatically stays in sync at this
   range too: .nav-panel's inset (so the mobile menu still starts
   exactly at the header's bottom edge), the html scroll-margin-top
   (anchor-scroll offset under the sticky header), and even the
   768-1023px slice of the desktop hero's own viewport-relative
   min-height calc further down (correctly subtracts the smaller
   header at those widths, since it's still on-screen there too). */
@media (max-width: 1023px) {
  :root {
    --header-height: 3.75rem;
  }
}

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

html {
  /* Explicit root font-size so every rem value in this file computes
     predictably (1rem = 16px by default in every mainstream browser).
     100%, not a hardcoded 16px — that keeps rem tied to the user's own
     browser/OS font-size preference (e.g. someone who bumps their
     browser default to 20px still gets everything scaled up), which a
     fixed px value would silently override and defeat the point of
     using rem for accessibility in the first place. */
  font-size: 100%;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
}

img,
svg {
  display: block;
  max-width: 100%;
}

button,
input,
textarea {
  font: inherit;
  color: inherit;
}

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

a {
  color: inherit;
  text-decoration: none;
}

[id] {
  scroll-margin-top: var(--header-height);
}

/* ---------------------------------------------------------------------
   Base
------------------------------------------------------------------------ */
body {
  font-family: var(--font-sans), sans-serif;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-regular);
  line-height: var(--line-height-normal);
  color: var(--color-on-background-color);
  background-color: var(--color-background-color);
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display), serif;
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-tight);
  margin: 0;
  color: inherit;
}

p {
  margin: 0;
}

.eyebrow {
  display: inline-block;
  font-family: var(--font-mono), monospace;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  letter-spacing: var(--letter-spacing-widest);
  text-transform: uppercase;
  color: var(--color-primary-color);
  margin-bottom: var(--spacing-3);
}

/* Reusable inline accent — e.g. the second line of a two-line heading */
.text-accent {
  color: var(--color-primary-color);
}

/* Skip link */
.skip-link {
  position: absolute;
  top: -3rem;
  left: var(--spacing-4);
  z-index: 1000;
  background: var(--color-primary-color);
  color: var(--color-on-primary-color);
  padding: var(--spacing-2) var(--spacing-4);
  border-radius: var(--radius-sm);
  font-weight: var(--font-weight-medium);
  transition: top var(--motion-fast) var(--ease-standard);
}

.skip-link:focus {
  top: var(--spacing-4);
}

/* Focus visibility */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--color-outline-color);
  outline-offset: 3px;
  border-radius: var(--radius-xs);
}

/* In-page anchor nav (header/footer links, "Get in touch") moves focus
   to the target section after scrolling — tabindex="-1" is the marker
   main.js sets right before calling .focus() there (see js/main.js).
   That focus is always script-driven, never a direct click on the
   section itself, so :focus-visible's heuristics can't be relied on
   to show anything — and without this rule the browser's own default
   focus ring (soft/blurred in some browsers) shows instead, reading
   like a rendering glitch rather than an intentional indicator.
   Deliberately subtler than the outline rule above (1px + the lighter
   --color-outline-variant-color, already used elsewhere as a faint
   hairline) rather than the same crisp/bold treatment — this is a
   barely-there presence for accessibility, not something meant to
   draw the eye the way a clicked/focused control's outline should. */
[tabindex="-1"]:focus {
  outline: 1px solid var(--color-outline-variant-color);
  outline-offset: 3px;
  border-radius: var(--radius-xs);
}

/* ---------------------------------------------------------------------
   Layout helpers
------------------------------------------------------------------------ */
.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--spacing-5);
}

@media (min-width: 768px) {
  .container {
    padding-inline: var(--spacing-8);
  }
}

.section {
  padding-block: var(--spacing-20);
}

.section-header {
  max-width: 40rem;
  margin-bottom: var(--spacing-12);
}

.section-header--center {
  margin-inline: auto;
  text-align: center;
}

.section-title {
  /* Fluid clamp() default (was flat 3xl below 768px, flat 4xl from
     768px up). In practice every current .section-title usage is
     inside a section that overrides this with its own larger 5xl/6xl
     clamp() below — see Who We Help, Services, Approach, Why Us,
     Story, FAQ, CTA — so this is the fallback for a bare/unscoped
     .section-title, kept in step with those for consistency. */
  font-size: clamp(var(--font-size-3xl), 1.5vw + 1.52rem, var(--font-size-4xl));
}

/* Site-wide per-letter "ripple" entrance, replacing the earlier
   spiral — every section heading (Clients, Services, Proof, Our
   Approach, Our Story, Contact) has its text split into individual
   .letter-ripple spans by main.js (wrapHeadingLetters), each rising
   into place with a wave passing left-to-right across the whole
   heading. Deliberately just the heading: every wrapper this sits
   inside (.section-header, .services-header, .story-copy) still has
   its own plain .reveal fade for its OTHER children (eyebrow, lede,
   description), untouched by this.
   No new observer — reuses the same .reveal/.is-visible toggle every
   wrapper already gets from one of the two existing
   IntersectionObserver instances; this is purely additive CSS keyed
   off that same class. --letter-index is set per span by JS and
   drives the stagger via transition-delay; spaces are left as plain
   text (not wrapped), so they don't animate and word spacing/wrapping
   behaves normally. No opacity:0 base on .section-title itself this
   time — if JS never runs (e.g. disabled), the heading just renders
   normally instead of staying invisible forever. */
.letter-ripple-word {
  display: inline-block;
}

.letter-ripple {
  display: inline-block;
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 450ms var(--ease-standard),
    transform 450ms var(--ease-standard);
  transition-delay: calc(var(--letter-index, 0) * 70ms);
}

.reveal.is-visible .letter-ripple {
  opacity: 1;
  transform: none;
}

/* Shared line-by-line staggered description reveal — used by the Our
   Approach phase descriptions, the Who We Help intro + 5 card
   descriptions, and (via main.js's WAAPI variant, same visual values)
   the Services category description. main.js's wrapWordsForLineReveal
   splits a paragraph into .line-reveal-word spans up front (word-level,
   not letters, so wrapping still only breaks at real word boundaries —
   same reasoning as .letter-ripple-word above); revealWordsByLine
   groups them into visual lines by shared offsetTop right when
   triggered, so it stays correct at whatever width is actually
   rendering, then stamps a per-line transition-delay before flipping
   .is-line-revealed on an ancestor. */
.line-reveal-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.75rem);
  transition: opacity 400ms var(--ease-standard),
    transform 400ms var(--ease-standard);
}

.is-line-revealed .line-reveal-word {
  opacity: 1;
  transform: none;
}

.section-lede {
  margin-top: var(--spacing-4);
  font-size: var(--font-size-lg);
  color: var(--color-on-surface-variant-color);
}

@media (min-width: 768px) {
  .section {
    /* 112px -> 56px: each section now contributes half of a uniform
       112px gap (56+56) between any two neighbors, instead of 112+112
       compounding to 224px. See chat: audited gaps were inconsistent
       (224px normally, 112px after Hero, 152px before Footer) — this
       plus the .hero margin-bottom and .site-footer padding changes
       below make every section-to-section gap 112px at this
       breakpoint and up. Mobile (<768px) is unchanged/out of scope. */
    padding-block: var(--spacing-14);
  }
}

@media (max-width: 767px) {
  .section {
    /* 160px -> 96px: same "half-gap" idea as the >=768px override
       above, sized down for mobile. Every section sharing the plain
       .section rule (Who We Help, Services, Approach, Why Us, Story,
       FAQ, CTA) contributes 48px top + 48px bottom, so any two
       neighbors land on a uniform 96px gap instead of the base rule's
       160px (80+80) — that was the "too large" gap noticed between
       Services and Our Approach, and it was equally too large between
       every other pair, just less noticed. .trusted-brands keeps its
       own deliberately-thinner override (16px, unchanged) since it
       wins the cascade by appearing later in the file at equal
       specificity. */
    padding-block: var(--spacing-12);
  }
}

/* ---------------------------------------------------------------------
   Buttons
------------------------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-2);
  padding: var(--spacing-4) var(--spacing-6);
  min-height: 3rem;
  border-radius: var(--radius-full);
  border: none;
  cursor: pointer;
  font-family: var(--font-sans), sans-serif;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
  transition: background-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard),
    box-shadow var(--motion-fast) var(--ease-standard);
}

.btn-primary {
  background-color: var(--color-primary-color);
  color: var(--color-on-primary-color);
  box-shadow: var(--elevation-1);
  /* Redeclares .btn's transition list plus transform (scale only — no
     vertical movement), so both hover-in and mouse-leave ease through
     this same single transition; no keyframe animation involved. */
  transition: background-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard),
    box-shadow var(--motion-fast) var(--ease-standard),
    transform var(--motion-base) var(--ease-standard);
}

.btn-primary:hover {
  background-color: color-mix(in srgb, var(--color-primary-color) 88%, var(--color-on-primary-color) 12%);
  box-shadow: var(--elevation-2);
  /* Gentle grow, no overshoot — scale() only, so it never affects
     layout/reflow. var(--ease-standard) is this project's one shared
     deceleration curve (already used by the other properties above),
     kept here instead of the literal `ease-out` keyword so all four
     transitioning properties stay in the same rhythm. */
  transform: scale(1.06);
}

.btn-secondary {
  background: transparent;
  color: var(--color-on-background-color);
  border: 1px solid var(--color-outline-variant-color);
}

.btn-secondary:hover {
  background-color: var(--color-surface-container-low-color);
  border-color: var(--color-outline-color);
}

.btn-text {
  background: transparent;
  padding: var(--spacing-2) 0;
  color: var(--color-primary-color);
  font-weight: var(--font-weight-medium);
  border-radius: var(--radius-xs);
}

.btn-text:hover {
  color: var(--color-tertiary-color);
}

.btn-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--spacing-4);
}

/* ---------------------------------------------------------------------
   Header / Nav
------------------------------------------------------------------------ */
.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  height: var(--header-height);
  display: flex;
  align-items: center;
  border-bottom: 1px solid var(--color-outline-variant-color);
}

/* The translucent fill + blur live on a ::before layer, not directly
   on .site-header — critical bug found live-testing the mobile menu:
   .nav-panel (position: fixed, a descendant of .site-header) is meant
   to cover the full viewport below the header, but backdrop-filter on
   an ancestor makes that ancestor the CONTAINING BLOCK for fixed-
   position descendants (same family as transform/filter/will-change).
   With backdrop-filter on .site-header itself, .nav-panel's fixed
   positioning was being resolved against the header's own ~60px-tall
   box instead of the viewport — its `inset` math broke, and the
   panel only ever covered a sliver near the top, with the hero
   section showing through everywhere below that (duplicated "Book
   Consultation" button included, since .nav-panel's own copy was
   still rendering, just floating over unobstructed page content
   instead of an opaque background). Moving the filter to a
   non-ancestor ::before layer keeps the exact same visual blur/tint
   on the header bar while leaving .site-header itself filter-free, so
   .nav-panel's fixed positioning resolves against the real viewport
   again. inset: 0 sizes it to .site-header's own box (sticky
   establishes the positioning context); z-index: -1 keeps it behind
   the logo/toggle within the header's own stacking context. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: color-mix(in srgb, var(--color-surface-color) 92%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

/* While the mobile drawer is open (.is-nav-open, set by main.js
   alongside .nav-panel's own .is-open), the header switches to the
   drawer's dark surface too — same --color-inverse-surface-color —
   so the header bar and the panel below it read as one continuous
   full-screen takeover instead of a light bar sitting on top of a
   dark panel. Desktop never sets this class (.nav-toggle, the only
   thing that can, is hidden there), so no width guard is needed. */
.site-header.is-nav-open::before {
  background-color: var(--color-inverse-surface-color);
  /* Solid opaque fill now (not the translucent-over-scrolled-content
     mix above) — blur has nothing left to do and was leaving a faint
     seam right at the header/panel boundary from sampling across it. */
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.site-header.is-nav-open {
  /* border-bottom-width: 0, not just a transparent color — the ::before
     dark fill is inset:0 within the padding-box (excludes the border
     area), so a merely-transparent 1px border still left a 1-device-
     pixel gap at the header/panel join with the page's own (light)
     body background showing through it. Zeroing the width removes
     that dead strip entirely instead of just hiding its color. */
  border-bottom: none;
}

.site-header .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-4);
}

.brand {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

/* The only other direct child of .site-header .container — everything
   nav-related (toggle button + panel) lives inside this one wrapper,
   so the header is unambiguously a 2-child flex row: logo, cluster. */
.nav-cluster {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

/* The logo asset is a full lockup (mark + wordmark baked in, 445:153
   intrinsic ratio) — no separate text label needed next to it. */
.brand img {
  width: auto;
}

.site-header .brand img {
  height: 2.25rem;
}

/* Hidden rather than recolored while the drawer is open — no light/
   on-dark logo asset exists (jopera-logo.svg is a single flattened
   raster lockup, not an editable vector), and a CSS filter stand-in
   (brightness(0) invert(1)) flattened the orange mark to plain white,
   losing brand color. The close (X) icon is enough wayfinding on its
   own for this state. */
.site-header.is-nav-open .brand {
  visibility: hidden;
}

/* Matches the header-height trim above (same max-width: 1023px range)
   so the logo scales down with the shorter header instead of nearly
   filling it edge to edge. .nav-toggle (44px) is deliberately left
   untouched at any width — that's the minimum recommended touch
   target size, and there's still enough slack in the reduced 60px
   header to center it without crowding. */
@media (max-width: 1023px) {
  .site-header .brand img {
    height: 1.875rem;
  }
}

.site-footer .brand img {
  height: 1.875rem;
}

.nav-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--color-on-background-color);
  transition: color var(--motion-fast) var(--ease-standard);
}

/* Close (X) icon needs to flip to light the moment the header goes
   dark (.is-nav-open, above) — otherwise it's the same dark-on-dark
   problem the nav links would have without their own color override. */
.site-header.is-nav-open .nav-toggle {
  color: var(--color-inverse-on-surface-color);
}

/* (hover: hover) so this only applies on devices with a true pointing
   device (mouse/trackpad) — without it, touchscreens apply :hover on
   tap and (having no real mouse to "leave") never clear it again
   until something else is tapped, so the background reads as a
   persistent box stuck around the icon specifically after
   tapping/opening the menu, even though the default/closed state
   never showed one. Desktop mouse users keep the exact same hover
   feedback as before. */
@media (hover: hover) {
  .nav-toggle:hover {
    background-color: var(--color-surface-container-low-color);
  }

  .site-header.is-nav-open .nav-toggle:hover {
    background-color: color-mix(in srgb, var(--color-inverse-on-surface-color) 12%, transparent);
  }
}

/* Both icons are stacked directly on top of each other (absolute,
   centered in the button via the -50% translate below) and cross-
   fade + rotate between states, rather than the old display:none/
   block swap — display changes can't be transitioned, so the old
   version snapped instantly between hamburger and X with no
   animation in either direction. Same --motion-base duration as
   .nav-panel's own open/close transition so the icon spin reads as
   part of the same motion, not a separate, unsynced animation. */
.nav-toggle svg {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1.5rem;
  height: 1.5rem;
  transition: opacity var(--motion-base) var(--ease-standard),
    transform var(--motion-base) var(--ease-standard);
}

.nav-toggle .icon-open {
  opacity: 1;
  transform: translate(-50%, -50%) rotate(0deg);
}

.nav-toggle .icon-close {
  opacity: 0;
  transform: translate(-50%, -50%) rotate(-180deg);
}

.nav-toggle[aria-expanded="true"] .icon-open {
  opacity: 0;
  transform: translate(-50%, -50%) rotate(180deg);
}

.nav-toggle[aria-expanded="true"] .icon-close {
  opacity: 1;
  transform: translate(-50%, -50%) rotate(0deg);
}

.nav-panel {
  position: fixed;
  /* Full inset (including bottom: 0) — the COVERING box spans the
     entire viewport below the header so there is no gap anywhere for
     page content to show through, regardless of scroll position. This
     is separate from how much of that box the CONTENT actually fills:
     .nav-panel has no flex/grid stretch rules on mobile (that's
     desktop-only, in the >=1024px block below), so .nav-links/the CTA
     button/.nav-social simply stack in normal block flow at the top
     and stop — the remaining height below them is just more of this
     same opaque background, not stretched content. (Previously this
     used top/left/right only, no bottom, so height was intrinsic to
     content — that left the actual viewport uncovered below the
     content on short drawers, which is the bug this fixes: the
     container now always fully covers the screen; only the content
     block within it stays compact.) */
  inset: var(--header-height) 0 0 0;
  z-index: var(--z-nav-panel);
  /* Same dark surface as .contact ("Let's Talk.") — reusing that
     token (rather than a new dark value) keeps this in step with the
     one other place the site already commits to a dark surface. */
  background-color: var(--color-inverse-surface-color);
  transform: translateY(-8px);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--motion-base) var(--ease-standard),
    transform var(--motion-base) var(--ease-standard),
    visibility var(--motion-base);
  /* Forces this onto its own stable GPU compositing layer independent
     of the transform's actual animated value — without it, iOS Safari
     was rendering a thin seam partway down the drawer (worst near
     .nav-social) while this element was both position:fixed AND its
     own scrolling container with an animated transform. That specific
     combination is a documented WebKit tiling/compositing bug; the fix
     is two-part: this permanent 3D-layer hint, plus moving the actual
     overflow-y:auto scrolling off onto .nav-panel-inner below so this
     element's own box is never simultaneously "the animated layer" and
     "the scroll container." */
  -webkit-transform: translateY(-8px) translateZ(0);
  -webkit-backface-visibility: hidden;
}

.nav-panel.is-open {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  -webkit-transform: translateY(0) translateZ(0);
}

.nav-panel-inner {
  height: 100%;
  padding: var(--spacing-8) var(--spacing-5) var(--spacing-10);
  /* iOS home-indicator safe area — without this the drawer's own
     background still reaches the true bottom edge (that's .nav-panel's
     job via inset:0 above), but CONTENT could sit flush against/under
     the home indicator. max() keeps the existing spacing-10 as the
     floor on devices with no inset (env() resolves to 0 there). */
  padding-bottom: max(var(--spacing-10), env(safe-area-inset-bottom));
  /* Safety net, not the primary fix — content is confirmed to fit at
     812/844/932px without this ever engaging. If a future content
     addition or a genuinely short viewport ever does exceed the
     available height, this lets the drawer's own content scroll
     internally (still fully opaque throughout, since .nav-panel itself
     still covers the whole screen) rather than clipping content or
     pushing the panel itself past the viewport. */
  overflow-y: auto;
}

.nav-links {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-1);
  /* Gap to .nav-tagline right below — back up to a generous,
     breathing value (this went through a tight --spacing-4 phase
     when links+button were meant to read as one cramped block; the
     structure has since changed to a 4-part links -> tagline ->
     button -> socials flow with even spacing throughout, so this
     matches .nav-tagline's and .nav-social's own margin-top below).
     Desktop still resets this to 0 in its own override further down,
     unaffected either way. */
  margin-bottom: var(--spacing-6);
}

.nav-links a {
  display: block;
  padding: var(--spacing-2) var(--spacing-4);
  /* Drawer nav links ARE the primary content of this full-screen
     takeover, not body-text-scale list items — sized to read as the
     dominant heading-level element (display-scale, tight line-height
     like h1-h4 above) rather than a secondary index, while still
     staying well under --font-size-5xl (48px, the section-title
     mobile floor) so hierarchy against real headings stays intact. */
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-medium);
  line-height: var(--line-height-tight);
  color: var(--color-inverse-on-surface-color);
  transition: color var(--motion-fast) var(--ease-standard);
}

/* Current/active section indicator — text color only, no fill/pill.
   Unchanged from the light-drawer version: --color-primary-color
   (orange) holds 5.24:1 against the new dark surface too (WCAG AA). */
.nav-links a.is-active {
  color: var(--color-primary-color);
  font-weight: var(--font-weight-semibold);
}

/* New second element in the links -> tagline -> button -> socials
   flow: reuses the exact line from .contact-cta-copy p (contact.html)
   minus its trailing "Book a call..." clause (the button right below
   already covers that). Same color-mix technique as .contact-hero-sub/
   .contact .section-lede use for muted copy on THIS same dark surface
   (--color-inverse-surface-color) — 75% of the on-surface color mixed
   toward transparent, ~7.3:1 against the drawer background (WCAG AA).
   Hidden at desktop like .nav-social — .nav-panel there is the plain
   inline nav row, not this drawer layout. */
.nav-tagline {
  margin: 0 0 var(--spacing-8);
  max-width: 22rem;
  color: color-mix(in srgb, var(--color-inverse-on-surface-color) 75%, transparent);
  font-size: var(--font-size-lg);
}

@media (min-width: 1024px) {
  .nav-tagline {
    display: none;
  }
}

.nav-panel .btn-primary {
  width: 100%;
}

/* Mirrors .footer-social's icon-button treatment (same circle size,
   border, hover fill) but as a horizontal row instead of a vertical
   column. Fixed margin-top (not margin-top: auto pushing it to the
   drawer's bottom edge, tried in an earlier pass — on the full-
   viewport-height drawer that consumed all the leftover space in one
   giant void). Matches .nav-links' and .nav-tagline's own margin-
   bottom so all three transitions in the links -> tagline -> button
   -> socials flow share one even rhythm instead of an uneven mix of
   tight and huge gaps; whatever height the tall drawer has left over
   simply trails AFTER the icons as bottom padding. */
.nav-social {
  display: flex;
  gap: var(--spacing-3);
  margin-top: var(--spacing-8);
}

.nav-social a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-full);
  border: 1px solid color-mix(in srgb, var(--color-inverse-on-surface-color) 30%, transparent);
  color: var(--color-inverse-on-surface-color);
  transition: background-color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard),
    transform var(--motion-fast) var(--ease-standard),
    opacity var(--motion-fast) var(--ease-standard);
}

@media (hover: hover) {
  .nav-social a:hover {
    background-color: var(--color-primary-color);
    border-color: var(--color-primary-color);
    color: var(--color-on-primary-color);
  }
}

.nav-social a:focus-visible {
  background-color: var(--color-primary-color);
  border-color: var(--color-primary-color);
  color: var(--color-on-primary-color);
}

/* Tap/press feedback for touch devices (hover:none) — scale down +
   dim slightly while pressed, springing back immediately on release
   via the transform/opacity transition above (same --motion-fast
   150ms as the rest of this element's own transitions). Scoped to
   hover:none so it doesn't run alongside/interfere with the desktop
   mouse hover-fill above on hybrid devices. Relies on the touchstart
   listener in main.js — iOS Safari otherwise never applies :active to
   links at all. */
@media (hover: none) {
  .nav-social a:active {
    transform: scale(0.92);
    opacity: 0.75;
  }
}

.nav-social svg {
  width: 1.125rem;
  height: 1.125rem;
}

@media (min-width: 1024px) {
  .nav-toggle {
    display: none;
  }

  /* Desktop reuses .nav-panel as the plain inline nav row (links +
     CTA button) — the social row is a mobile-drawer-only addition,
     not part of that design. */
  .nav-social {
    display: none;
  }

  .nav-panel {
    position: static;
    inset: auto;
    background: transparent;
    padding: 0;
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: none;
  }

  .nav-panel {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    gap: var(--spacing-5);
  }

  /* Makes the mobile-only scroll wrapper (see .nav-panel-inner above)
     disappear from the layout tree here, so .nav-links/.btn-primary
     go back to being direct flex children of .nav-panel — i.e. this
     desktop inline nav row lays out exactly as it did before that
     wrapper existed. */
  .nav-panel-inner {
    display: contents;
  }

  .nav-links {
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--spacing-3);
    margin-bottom: 0;
  }

  .nav-links a {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    padding: var(--spacing-2) var(--spacing-4);
    font-size: var(--font-size-base);
    white-space: nowrap;
    /* .nav-panel is a plain transparent inline row here, not the dark
       mobile drawer — reset back to the light header's normal text
       color instead of inheriting the drawer-only light/cream value
       the base (mobile) rule sets above. */
    color: var(--color-on-background-color);
  }

  .nav-links a:hover:not(.is-active) {
    color: var(--color-primary-color);
  }
}

/* ---------------------------------------------------------------------
   Hero
------------------------------------------------------------------------ */
.hero {
  position: relative;
  /* This value only ever renders below 768px — the min-width:768px
     override further down fully replaces min-height there regardless
     of the number here, so there's no separate mobile media query
     needed to scope this. 38rem/608px was ~72% of a 390x844 mobile
     viewport with the actual title+button content only needing
     ~360-390px inside it (confirmed via computed measurements) —
     align-items: center was just centering that content in a lot of
     extra empty space above/below. Cut to 28rem/448px: still taller
     than the content so it stays centered with real breathing room
     (not cramped/content-hugging), just without the excess void. */
  min-height: 28rem;
  display: flex;
  align-items: center;
  overflow: hidden;
  color: var(--color-inverse-on-surface-color);
  /* Paints immediately, before the slideshow's first photo has loaded —
     without this the hero was plain white/transparent until that image
     arrived, which is what actually read as a "blank/gray flash" (worst
     on mobile networks). Same dark surface token as .contact and the
     mobile nav drawer, so it already reads as on-brand rather than a
     generic gray placeholder, and it's close in tone to .hero-scrim's
     own darkened center, so the transition into the loaded photo is
     subtle rather than a hard swap. */
  background-color: var(--color-inverse-surface-color);
}

.hero-slideshow {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.hero-slide-track {
  display: flex;
  width: 500%;
  height: 100%;
  /* Transform is driven by js/main.js — advancing on the same tick that
     rotates the headline ending, so both stay perfectly paired. This
     transition animates the slide js sets; JS disables it (transition:
     none) for the seamless-loop reset and for prefers-reduced-motion. */
  transition: transform 500ms var(--ease-standard);
  will-change: transform;
}

.hero-slide {
  /* One slide-width = the track's own width (500%) divided by its 5
     slides — NOT 100%, which would mean 100% of the track (i.e. the
     full 500%), blowing each slide up to 5x the hero's actual width. */
  flex: 0 0 calc(100% / 5);
  width: calc(100% / 5);
  height: 100%;
}

.hero-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Default favors the upper portion of each portrait (more headroom
     above the face) over the default 50% center, which was cropping
     too tight against foreheads/eyes right under the header. Per-image
     overrides below where this default doesn't fit a specific photo. */
  object-position: 50% 30%;
  display: block;
}

/* Single scrim over the whole slideshow, darkest behind the copy, fading to the edges */
.hero-scrim {
  position: absolute;
  inset: 0;
  z-index: var(--z-hero-scrim);
  /* Peak 88% -> 92% (moderately darker/moodier at the text zone, still
     short of reading as solid black). Ellipse 65% 60% -> 78% 72% so the
     same proportional falloff shape (unchanged: 45%/75%/100% stops)
     projects across more of the frame, not just a bigger dark core.
     Mid/outer stops (60%, 22%, transparent) intentionally untouched —
     that gap versus the now-darker peak keeps edges clearly lighter/
     more transparent, so the photography stays visible there. */
  background-image:
    radial-gradient(
      ellipse 78% 72% at 50% 45%,
      color-mix(in srgb, var(--color-on-background-color) 92%, transparent) 0%,
      color-mix(in srgb, var(--color-on-background-color) 60%, transparent) 45%,
      color-mix(in srgb, var(--color-on-background-color) 22%, transparent) 75%,
      transparent 100%
    );
}

.hero-content {
  position: relative;
  z-index: var(--z-hero-content);
  width: 100%;
  text-align: center;
  padding-block: var(--spacing-16);
}

/* Trimmed for mobile only — desktop's 64px stays as-is (see .hero's
   own min-height comment above: at >=768px that height comes from
   the viewport-relative min-height override below, not this padding,
   so this change doesn't touch it either way). */
@media (max-width: 767px) {
  .hero-content {
    padding-block: var(--spacing-10);
  }
}

.hero-content .eyebrow {
  /* Hidden per request — markup/shared .eyebrow class kept intact so
     other sections' eyebrows are unaffected; this is hero-scoped only. */
  display: none;
  color: var(--color-primary-container-color);
}

.hero-title {
  /* Floor is --font-size-5xl (48px), matching every internal section's
     own mobile heading size (Services/Approach/Why Us) — this is the
     page's first, most prominent element over a full-bleed photo with
     prominent CTAs below it, so it shouldn't render smaller than
     secondary sections' own headings. At 48px + this size, the fixed
     prefix "We help growing businesses" alone already fills 3 full
     lines at mobile widths (down to "businesses" nearly spanning the
     whole line by itself) — leaving no room for any real ending text
     on that 3rd line, so the 4 rotating endings (below, in main.js)
     were rewritten short enough to complete the headline in exactly 1
     more line (4 total), not 2-3 more (5-6 total, what the original
     longer endings needed at this size). Verified uniform across
     320-559px (the range where this floor is actually in effect —
     the vw term overtakes it above ~559px). Keeps the same vw growth
     into --font-size-7xl at wider widths, so this floor has no effect
     at typical desktop widths, no separate media query needed. */
  font-size: clamp(var(--font-size-5xl), 5vw + 1rem, var(--font-size-7xl));
  max-width: 48rem;
  margin-inline: auto;
  /* Reserves space for the tallest of the 4 rotating endings (measured:
     all 4 render at 3 lines at this max font-size) so the headline
     doesn't reflow/jump as the ending text changes length. em-based so
     it scales with the fluid clamp() font-size above; a hair of margin
     (3.05 vs. an exact 3) absorbs sub-pixel rounding across variants. */
  min-height: calc(3.05 * var(--line-height-tight) * 1em);
}

/* The 3.05-line reservation above was tuned for wider/desktop widths;
   at mobile widths the prefix+ending combination needs a uniform 4
   lines with the shortened endings above (measured across 320-412px —
   see the font-size comment above), so 3.05 lines under-reserves
   there. Scoped to mobile only so desktop's existing reservation, and
   desktop's own (much shorter, 1-2 line) wrap behavior, is untouched. */
@media (max-width: 767px) {
  .hero-title {
    min-height: calc(4 * var(--line-height-tight) * 1em);
  }
}

.hero-title-ending {
  transition: opacity var(--motion-base) var(--ease-standard);
}

.hero-title-ending.is-fading {
  opacity: 0;
}

.hero-subhead {
  /* Hidden per request — markup/transition rules kept intact (see the
     .hero-subhead.reveal rule below) in case it's needed again. */
  display: none;
  max-width: 38rem;
  margin: var(--spacing-6) auto 0;
  font-size: var(--font-size-lg);
  color: var(--color-inverse-on-surface-color);
}

/* Compound selector (not just .hero-subhead) so this reliably outranks
   .reveal's own `transition` shorthand — that shorthand resets delay to
   its implicit 0s for both listed properties, and .reveal is declared
   later in the file, so at equal specificity it would otherwise win
   regardless of this rule's own position. */
.hero-subhead.reveal {
  /* Entrance stagger — reuses .reveal (see main.js's existing
     IntersectionObserver); starts ~120ms after the headline's own
     .reveal transition, per request. */
  transition-delay: 120ms;
}

.hero-content .btn-group {
  justify-content: center;
  /* Bumped from --spacing-8: with the subhead hidden again, the CTAs
     sit directly under the headline — 32px alone reads as cramped
     under a headline this large, so this restores the breathing room
     used the first time the subhead was hidden. */
  margin-top: var(--spacing-12);
}

/* Mobile-only tightening: the 48px reasoning above was tuned against
   desktop's much larger (up to 72px) headline — at mobile's smaller
   48px size the same 48px gap reads as loose/disconnected rather than
   "breathing room." Scoped so desktop's own pairing (large headline,
   48px gap) is untouched. First cut was --spacing-6 (24px) — still
   read as loose against the shorter mobile headline, cut further. */
@media (max-width: 767px) {
  .hero-content .btn-group {
    margin-top: var(--spacing-4);
  }
}

.hero-content .btn-secondary {
  color: var(--color-inverse-on-surface-color);
  border-color: color-mix(in srgb, var(--color-inverse-on-surface-color) 40%, transparent);
}

.hero-content .btn-secondary:hover {
  background-color: color-mix(in srgb, var(--color-inverse-on-surface-color) 12%, transparent);
  border-color: var(--color-inverse-on-surface-color);
}

/* Pause/resume control for the auto-rotating hero headline + slideshow
   (WCAG 2.2.2). A quiet, circular icon button pinned to the hero's
   bottom-right corner, above the scrim. Pause icon (two bars) shows
   while playing; play icon (triangle) shows while paused via the
   .is-paused class toggle (main.js).
   Hidden by default (opacity: 0) and faded in on hero hover. Only the
   visual state is hidden: opacity (not display/visibility) keeps the
   button in the tab order and accessibility tree, and it re-shows on
   :focus so keyboard and screen-reader users can still reach and
   operate it without a pointer. */
.hero-pause {
  position: absolute;
  right: var(--spacing-4);
  bottom: var(--spacing-4);
  z-index: var(--z-hero-content);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-full);
  border: 1px solid color-mix(in srgb, var(--color-inverse-on-surface-color) 40%, transparent);
  background-color: color-mix(in srgb, var(--color-inverse-surface-color) 35%, transparent);
  color: var(--color-inverse-on-surface-color);
  opacity: 0;
  cursor: pointer;
  transition: opacity var(--motion-fast) var(--ease-standard),
    background-color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}

.hero:hover .hero-pause,
.hero-pause:focus {
  opacity: 1;
}

/* No persistent hover (touch devices) — the hover-to-reveal trigger
   above is unreachable, so keep the control always visible instead of
   permanently hidden. */
@media (hover: none) {
  .hero-pause {
    opacity: 1;
  }
}

.hero-pause:hover,
.hero-pause:focus-visible {
  background-color: var(--color-primary-color);
  border-color: var(--color-primary-color);
  color: var(--color-on-primary-color);
}

.hero-pause-icon {
  width: 1.125rem;
  height: 1.125rem;
}

.hero-pause-icon--play {
  display: none;
}

.hero-pause.is-paused .hero-pause-icon--pause {
  display: none;
}

.hero-pause.is-paused .hero-pause-icon--play {
  display: block;
}

@media (min-width: 768px) {
  .hero {
    /* Full viewport height below the header — .site-header is
       `position: sticky`, not fixed, so it already reserves its own
       space in normal flow (no padding-top compensation needed); the
       header's height still needs subtracting here so header + hero
       together equal exactly one screen, not one screen plus 72px.
       max() keeps the 70rem floor from the shoulders/clothing crop fix
       so shorter viewports don't lose that framing again. vh first as
       the fallback, dvh second to override it where supported (mobile-
       Safari-style address-bar resizing on tablets). */
    min-height: max(70rem, calc(100vh - var(--header-height)));
    min-height: max(70rem, calc(100dvh - var(--header-height)));
    /* Hero has no padding/background system of its own, so it
       contributes nothing to the gap before the section that follows
       it — margin (not padding) so it doesn't eat into the vertical
       centering above, which relies on equal top/bottom space inside
       this min-height box. 56px matches the half-gap every .section
       now uses, so Hero -> next section lands on the same uniform
       112px as every other transition. */
    margin-bottom: var(--spacing-14);
  }
}

/* ---------------------------------------------------------------------
   Who We Help
------------------------------------------------------------------------ */
/* Scoped to this section only — the shared .section-title class stays
   its normal size everywhere else (Services, Approach, etc). Sized
   against this section's own --font-size-lg lede: --font-size-6xl is
   ~3.3x that, matching the proportionally large, statement-style
   headline weight of the Spotlite Africa reference rather than a
   typical section title. Fluid clamp() (was a flat 5xl below 768px,
   flat 6xl from 768px up) so it ramps smoothly across that range
   instead of jumping at the breakpoint; same 5xl/6xl floor and
   ceiling either way. */
#who-we-help .section-title {
  font-size: clamp(var(--font-size-5xl), 3vw + 2.25rem, var(--font-size-6xl));
}

.section-divider {
  margin: var(--spacing-10) 0 0;
  border: none;
  /* Faint hairline: --color-outline-variant-color is a warm, fairly
     saturated tan and read as too visible for "barely there" — mixed
     down to ~12% against transparent instead, using the neutral
     on-background ink token as the "black" base (still token-driven,
     no hardcoded hex, per design-tokens-rule.md). */
  border-top: 1px solid color-mix(in srgb, var(--color-on-background-color) 12%, transparent);
}

.audience-grid {
  display: flex;
  flex-wrap: wrap;
  /* Centers each wrapped row as its own group — including an
     incomplete last row — rather than left-aligning it. Combined with
     a fixed flex-basis (no grow) on .audience-card below, this is what
     keeps every card the same width while still centering the 2-card
     second row instead of stretching or left-aligning it. */
  justify-content: center;
  gap: var(--spacing-6);
  margin-top: var(--spacing-12);
}

.audience-card {
  /* No background/border by design — items sit flush on the section's
     own background (Spotlite Africa reference), not boxed as cards.
     Padding is kept for spacing between items, not as card interior. */
  flex: 0 0 100%;
  padding: var(--spacing-8);
}

.audience-card-number {
  display: block;
  font-family: var(--font-mono), monospace;
  font-size: var(--font-size-5xl);
  font-weight: var(--font-weight-medium);
  color: var(--color-outline-variant-color);
  line-height: 1;
}

.audience-card-rule {
  display: block;
  width: 2.5rem;
  /* 3px -> 2px, within the requested 1-2px range. Width/color/position
     left exactly as-is (not part of this change — see chat: current
     width is 40px, outside the 24-32px mentioned, flagged separately
     rather than resized here). */
  height: 2px;
  margin-top: var(--spacing-3);
  background-color: var(--color-primary-color);
  border-radius: var(--radius-full);
}

.audience-card h3 {
  margin-top: var(--spacing-5);
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
}

.audience-card p {
  margin-top: var(--spacing-3);
  color: var(--color-on-surface-variant-color);
}

@media (min-width: 768px) {
  .audience-card {
    /* 2 per row at tablet — same fixed-basis + centered-row technique */
    flex: 0 0 calc((100% - var(--spacing-6)) / 2);
  }
}

@media (min-width: 1024px) {
  .audience-card {
    /* 3 per row at desktop, so 5 cards fall into a 3-row + centered
       2-row split, per the reference layout. */
    flex: 0 0 calc((100% - 2 * var(--spacing-6)) / 3);
  }
}

/* Entrance — driven entirely by the existing IntersectionObserver in
   main.js: .audience-grid already carries .reveal, so the observer
   toggles .is-visible on the GRID exactly as it does for every other
   .reveal target on the page (no new JS/observer added). Everything
   below is CSS reacting to that one class toggle: each card's own
   fade+slide, the per-card 90ms stagger, and the number -> underline
   -> title/description sequence within each card, all computed from a
   --card-index custom property so nothing is written out 5 times over. */
.audience-card:nth-child(1) { --card-index: 0; }
.audience-card:nth-child(2) { --card-index: 1; }
.audience-card:nth-child(3) { --card-index: 2; }
.audience-card:nth-child(4) { --card-index: 3; }
.audience-card:nth-child(5) { --card-index: 4; }

.audience-card {
  opacity: 0;
  transform: translateY(1.75rem);
  transition: opacity var(--motion-slow) var(--ease-standard),
    transform var(--motion-slow) var(--ease-standard);
}

.audience-grid.is-visible .audience-card {
  opacity: 1;
  transform: none;
  transition-delay: calc(var(--card-index) * 90ms);
}

.audience-card-number {
  opacity: 0;
  transform: translateY(0.75rem);
  transition: opacity var(--motion-base) var(--ease-standard),
    transform var(--motion-base) var(--ease-standard);
}

.audience-grid.is-visible .audience-card-number {
  opacity: 1;
  transform: none;
  transition-delay: calc(var(--card-index) * 90ms);
}

.audience-card-rule {
  /* Draws left-to-right via scaleX (transform-only, no width/layout
     thrash) rather than an actual width transition. */
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--motion-slow) var(--ease-standard);
}

.audience-grid.is-visible .audience-card-rule {
  transform: scaleX(1);
  transition-delay: calc(var(--card-index) * 90ms + 130ms);
}

.audience-card h3 {
  opacity: 0;
  transition: opacity var(--motion-slow) var(--ease-standard);
}

.audience-grid.is-visible .audience-card h3 {
  opacity: 1;
  transition-delay: calc(var(--card-index) * 90ms + 450ms);
}

/* .audience-card p's own visibility is now owned entirely by the
   shared .line-reveal-word mechanism (main.js) — no separate
   paragraph-level opacity rule here, so the two don't fight over the
   same property. */

/* Hover-only: underline widen + number tilt/scale, firing together as
   one combined moment. Deliberately the ONLY visual change beyond that
   (no cursor/shadow/lift): these cards are informational, not
   interactive controls, so there's no focus/keyboard state to style.
   Declared after the entrance rules so it wins at equal selector
   specificity regardless of .is-visible state. */
.audience-card:hover .audience-card-rule {
  transform: scaleX(4.5);
  transition-delay: 0ms;
}

.audience-card:hover .audience-card-number {
  /* Gentle tilt + scale, no bounce/overshoot — reuses the number's own
     existing transition (--motion-base/--ease-standard, the same pair
     the CTA button's gentle-scale hover uses) rather than a new one. */
  transform: rotate(5deg) scale(1.06);
  transition-delay: 0ms;
}

/* ---------------------------------------------------------------------
   Trusted Brands
------------------------------------------------------------------------ */
.trusted-brands {
  /* Faint tint instead of the body's plain background — same token
     used elsewhere on the site (e.g. Our Approach, Contact) for a
     section that wants to read as a distinct band without a hard
     color break. */
  background-color: var(--color-surface-container-low-color);
  /* .trusted-brands-marquee's track is deliberately much wider than
     the viewport (2 duplicated wordmark sets) — this keeps that
     overflow from ever creating a page-level horizontal scrollbar,
     as a backstop alongside the marquee's own overflow: hidden. */
  overflow: hidden;
  /* Overrides .section's normal 80px/56px padding-block (below): that
     amount was sized for a full heading+lede+content section, but
     this one is just an eyebrow label and one marquee row — at the
     full section padding it read as a hero-sized band with a lot of
     empty space above/below its actual content. Same value at every
     width (no separate >=768px override) since there's no larger
     content here that would need more room on bigger screens either.
     Equal specificity to .section, but defined later in the file, so
     this wins the cascade at every breakpoint including the >=768px
     .section override further up. Cut again from --spacing-8 (32px)
     on request — tight enough now that the marquee row's own line-
     height is doing most of the section's remaining height. */
  padding-block: var(--spacing-4);
}

.trusted-brands-header {
  text-align: center;
}

/* The eyebrow here is an h2 (was a span — promoted so screen readers
   get a real heading for the section). Base h1-h4 sets
   --line-height-tight; restore the normal line-height the span
   inherited so the upgrade is visually byte-identical. */
.trusted-brands-header .eyebrow {
  line-height: var(--line-height-normal);
}

.trusted-brands-marquee {
  /* 0 here — the shared .eyebrow's own 12px margin-bottom alone
     already gives the requested 12px total between the label and the
     wordmark row (they don't collapse into one margin since
     .trusted-brands-header sits between them). */
  margin-top: 0;
  overflow: hidden;
  /* Centers .trusted-brands-track within this wrapper's own box on
     the cross axis, so the scrolling row can't end up flush to one
     edge with leftover space on the other — robust to any height
     mismatch between the two (different wordmark line-heights across
     the 4 fonts, font-loading timing, etc.) rather than relying on
     the row's natural content height to exactly match this wrapper's
     height by coincidence. */
  display: flex;
  align-items: center;
}

.trusted-brands-track {
  display: flex;
  width: max-content;
  /* 20s for one full loop of the whole track (2 sets) — increased
     further from 28s on request; the visible half-loop (one set's
     worth) a viewer actually watches pass by is ~10s. Paused by
     default; .is-visible (added by the same .reveal/
     IntersectionObserver system every other section's entrance
     uses — see main.js, no new observer needed) starts it once the
     section is actually scrolled into view. */
  animation: trusted-brands-scroll 20s linear infinite;
  animation-play-state: paused;
}

.trusted-brands-marquee.is-visible .trusted-brands-track {
  animation-play-state: running;
}

/* Hover-to-pause: comes after the .is-visible rule above so it wins
   the tie (equal specificity, later source order) while both a
   playing marquee and an active hover are true at once. */
.trusted-brands-marquee:hover .trusted-brands-track {
  animation-play-state: paused;
}

.trusted-brands-set {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  gap: var(--spacing-20);
  padding-inline: var(--spacing-10);
}

/* Mobile-only uniform treatment: below 768px every wordmark shares one
   font-family/size/weight/style (Oiza's own 3xl read visibly larger
   than the other three's 2xl at narrow widths, and 4 different
   typefaces read cramped/inconsistent in a single mobile row). 768px+
   restores the original 4 distinct per-brand treatments below — the
   variety there was deliberate (see .trusted-brand--* further down),
   this override is mobile-only per request, not a full replacement. */
.trusted-brand {
  white-space: nowrap;
  color: var(--color-on-surface-color);
  font-family: var(--font-sans), sans-serif;
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-semibold);
  font-style: normal;
  letter-spacing: normal;
  text-transform: none;
}

@media (min-width: 768px) {
  /* Bold, tracked-out geometric sans, all caps. */
  .trusted-brand--tcl {
    font-family: var(--font-geometric), sans-serif;
    font-weight: 700;
    font-size: var(--font-size-2xl);
    letter-spacing: var(--letter-spacing-widest);
    text-transform: uppercase;
  }

  /* Italic, mixed-case, the site's own display serif (Fraunces) — the
     one wordmark that deliberately reuses an existing site font
     rather than one of the 3 added just for this row. */
  .trusted-brand--oiza {
    font-family: var(--font-display), serif;
    font-style: italic;
    font-weight: var(--font-weight-medium);
    font-size: var(--font-size-3xl);
  }

  /* High-contrast italic serif, lowercase, editorial. text-transform
     (not literal lowercase in the markup) so the real proper-noun
     text stays in the DOM/accessible name — only the visual
     presentation is lowercased. */
  .trusted-brand--maison {
    font-family: var(--font-editorial), serif;
    font-style: italic;
    font-weight: 700;
    font-size: var(--font-size-2xl);
    text-transform: lowercase;
  }

  /* Rounded, soft sans, semi-bold, lowercase — same text-transform
     reasoning as .trusted-brand--maison above. */
  .trusted-brand--hermano {
    font-family: var(--font-rounded), sans-serif;
    font-weight: 600;
    font-size: var(--font-size-2xl);
    text-transform: lowercase;
  }
}

/* Exactly -50%: with 2 identical duplicated sets making up the track,
   that lands the animation precisely at the start of the second set —
   pixel-identical to the track's own starting position — so the loop
   restart is invisible. transform only (no width/left animated), for
   a compositor-friendly, smooth continuous scroll. */
@keyframes trusted-brands-scroll {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .trusted-brands-track {
    animation: none;
  }
}

/* ---------------------------------------------------------------------
   Services — filterable catalog: header (static intro beside the
   headline) + 4 tabs + active category's description + that category's
   tile grid. Only one .services-grid panel is unhidden at a time (see
   main.js) — inactive categories are removed via [hidden], not just
   scrolled past.
------------------------------------------------------------------------ */
.services-header {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  /* flex-start (not flex-end) so the description's own margin-top
     below can nudge it down to align with the headline specifically,
     rather than the eyebrow+headline block as a whole. */
  align-items: flex-start;
  gap: var(--spacing-6);
  /* Same value as the gap above (between headline and description) —
     tabs sit snug under the header row rather than floating with
     extra whitespace. */
  margin-bottom: var(--spacing-6);
}

/* Entrance sequence, step 1: heading + description fade up together —
   they're both inside .services-header, so the shared .reveal system
   already fades them as one unit on first scroll into view, no new
   JS needed here. Only the slide distance is scoped narrower than the
   site-wide default (1.5rem/24px) to land in the requested 15-20px
   range; duration is left at .reveal's own --motion-slow (400ms),
   already inside the requested 300-400ms window. Steps 2 (tabs) and 3
   (cards) are sequenced in JS off this same reveal, see main.js. */
.services-header.reveal {
  transform: translateY(1.125rem);
}

.services-header-heading {
  max-width: 28rem;
}

/* Matches #who-we-help .section-title exactly, for the same
   statement-style headline weight across both sections. */
#services .section-title {
  font-size: clamp(var(--font-size-5xl), 3vw + 2.25rem, var(--font-size-6xl));
}

.text-regular {
  font-weight: var(--font-weight-regular);
}

.services-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-3);
  /* Entrance sequence, step 2: opacity-only (no slide — the brief
     only asked the tab pills to fade in, unlike the heading/cards'
     fade+slide), triggered by JS adding .is-visible once the heading
     has finished (see main.js). One-time only — this class is never
     removed, so switching tabs later doesn't replay it. */
  opacity: 0;
  transition: opacity var(--motion-base) var(--ease-standard);
}

.services-tabs.is-visible {
  opacity: 1;
}

.services-tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem;
  padding: var(--spacing-3) var(--spacing-5);
  border-radius: var(--radius-full);
  border: 1px solid var(--color-outline-variant-color);
  background-color: transparent;
  color: var(--color-on-surface-variant-color);
  font-family: var(--font-sans), sans-serif;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  cursor: pointer;
  transition: background-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard);
}

.services-tab:hover {
  border-color: var(--color-primary-color);
  color: var(--color-on-surface-color);
}

.services-tab.is-active {
  background-color: var(--color-primary-color);
  border-color: var(--color-primary-color);
  color: var(--color-on-primary-color);
}

.services-category-description {
  /* Now the right-hand column of .services-header (flush beside the
     headline), not a standalone line below the tabs — same treatment
     the removed static intro paragraph had (24rem, flex handles the
     gap via .services-header's own `gap`). margin-top roughly matches
     the eyebrow's own height + its margin-bottom, so this aligns with
     the headline specifically instead of the eyebrow+headline block. */
  max-width: 24rem;
  margin-top: var(--spacing-8);
  font-size: var(--font-size-lg);
  color: var(--color-on-surface-variant-color);
}

/* Below 768px .services-header wraps to a stacked column, so the
   heading and description no longer sit side by side — the row's own
   `gap` (1.5rem) plus this paragraph's desktop margin-top (2rem,
   tuned to align with the headline in the two-column layout) stack
   into ~3.5rem of dead space between them. Mobile-only: tighten both
   down so the two sit close together, without touching the desktop
   two-column spacing above. */
@media (max-width: 767px) {
  .services-header {
    gap: var(--spacing-2);
  }

  .services-category-description {
    margin-top: 0;
  }
}

.services-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--spacing-4);
  margin-top: var(--spacing-6);
  opacity: 1;
  transition: opacity var(--motion-base) var(--ease-standard);
}

.services-grid[hidden] {
  display: none;
}

@media (min-width: 768px) {
  .services-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1024px) {
  .services-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.service-tile {
  position: relative;
  /* z-index:0 (with position:relative) forces this to establish its
     own stacking context, so the traveling-stroke ::before below —
     which needs a negative z-index to paint behind this card's own
     text — stays contained to this card instead of potentially
     slipping behind sibling tiles or other page content. */
  z-index: 0;
  background-color: var(--color-surface-container-low-color);
  border: 1px solid var(--color-outline-variant-color);
  border-radius: var(--radius-xl);
  padding: var(--spacing-6);
  /* Entrance sequence, step 3: opacity+transform starting state.
     .service-tile deliberately does NOT carry .reveal in the markup —
     unlike every other .reveal element on the page, this one has to
     REPLAY every time the active tab switches (see main.js), which
     the generic one-shot .reveal/.is-visible observer can't do.
     main.js drives the reveal/replay via element.animate() (Web
     Animations API), not a CSS transition — see animateServiceCards
     there for why. Only transform/box-shadow (the hover lift) are in
     this transition list now; opacity is animated independently by
     that JS animation instead, so it's deliberately left out here to
     avoid the two fighting over the same property. */
  opacity: 0;
  transform: translateY(1.125rem);
  transition: transform var(--motion-base) var(--ease-standard),
    box-shadow var(--motion-base) var(--ease-standard);
}

/* transform and transition-delay are !important here because the
   entrance sequence above sets both directly as inline styles, and an
   inline style always wins over a stylesheet rule regardless of
   specificity — without this, the hover lift would apply fine before
   the entrance ever ran, then stop working the instant it did. */
.service-tile:hover {
  transform: translateY(-4px) !important;
  box-shadow: var(--shadow-lg);
  transition-delay: 0ms !important;
}

/* Traveling stroke — cards 02-05 only (:not(--featured), card 01 is
   already a full color fill per the brief, so just the lift keeps it
   consistent without extra visual noise). A pseudo-element sized
   slightly larger than the card, ringed down to just the border-width
   via the padding-box/border-box double-background trick (an opaque
   layer clipped to padding-box sits on top and covers the interior,
   leaving the conic-gradient — clipped to border-box, painted below —
   visible only in the border ring).

   The angle is animated via the --stroke-angle custom property fed
   into conic-gradient(from var(--stroke-angle) ...), NOT via
   transform: rotate() on the element itself. Two earlier attempts
   both used transform: rotate() and both were visibly broken —
   rotating the whole rectangular pseudo-element swings its corners
   outside the card's bounds (a rotated rectangle's bounding box is
   larger than the unrotated one), which is exactly what produced the
   stray diagonal/square artifact poking out above the card. Animating
   the gradient's own angle instead keeps the element itself perfectly
   axis-aligned the entire time — only the color sweep inside it
   moves. @property registers the custom property as a real <angle>
   so the browser interpolates it smoothly frame-to-frame rather than
   jumping straight from 0deg to 360deg. */
@property --stroke-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.service-tile:not(.service-tile--featured)::before {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  border: 2px solid transparent;
  background:
    linear-gradient(
        var(--color-surface-container-low-color),
        var(--color-surface-container-low-color)
      )
      padding-box,
    conic-gradient(
        from var(--stroke-angle),
        transparent 0%,
        transparent 78%,
        var(--color-primary-color) 92%,
        transparent 100%
      )
      border-box;
  /* Behind the card's own text (see .service-tile's z-index:0 above) —
     without this, the opaque padding-box fill layer paints over the
     index number and heading, since position:absolute content stacks
     above normal in-flow content regardless of DOM order. */
  z-index: -1;
  opacity: 0;
  transition: opacity var(--motion-base) var(--ease-standard);
  animation: service-tile-stroke-spin 2.5s linear infinite;
  animation-play-state: paused;
  pointer-events: none;
}

.service-tile:not(.service-tile--featured):hover::before {
  opacity: 1;
  animation-play-state: running;
}

@keyframes service-tile-stroke-spin {
  to {
    --stroke-angle: 360deg;
  }
}

.service-tile-index {
  display: block;
  font-family: var(--font-mono), monospace;
  font-size: var(--font-size-sm);
  color: var(--color-outline-color);
}

.service-tile h3 {
  margin-top: var(--spacing-3);
  font-size: var(--font-size-xl);
}

/* Featured tile is always the first item in whichever panel is active —
   a structural :first-child selector, not tied to specific content, so
   it re-applies correctly per category automatically. */
.service-tile--featured {
  background-color: var(--color-primary-color);
  border-color: var(--color-primary-color);
  color: var(--color-on-primary-color);
  padding: var(--spacing-8);
}

.service-tile--featured .service-tile-index {
  color: color-mix(in srgb, var(--color-on-primary-color) 70%, transparent);
}

.service-tile--featured h3 {
  font-size: var(--font-size-2xl);
}

@media (min-width: 768px) {
  .service-tile--featured {
    grid-column: span 2;
  }
}

/* ---------------------------------------------------------------------
   Approach — staggered Gantt-style timeline. .approach-chart holds the
   4 decorative bars on an 11-unit grid (their spans overlap by design,
   producing the staircase cascade) plus the numbered axis below, which
   shows exactly 4 markers (01-04) — one per phase, aligned to the 4
   equal-width .approach-columns text items rather than the bars' own
   staggered start lines. Each bar sits on its own grid row since
   overlapping ranges can't share a row without visually colliding.
   Suggested bar spans from the brief: Discovery 1-3, Strategy 3-6,
   Design & Build 5-9, Launch & Beyond 8-11.
------------------------------------------------------------------------ */
/* Matches #who-we-help/#services .section-title exactly, for the same
   statement-style headline size across all three sections. Font
   family, weight, color, and style are all untouched — this only
   overrides the size. */
#approach .section-title {
  font-size: clamp(var(--font-size-5xl), 3vw + 2.25rem, var(--font-size-6xl));
}
.approach-timeline {
  margin-top: var(--spacing-12);
}

.approach-chart {
  display: grid;
  grid-template-columns: repeat(11, 1fr);
  column-gap: var(--spacing-2);
  row-gap: var(--spacing-3);
  /* Faint vertical gridlines, one per unit boundary — a repeating
     background instead of 11 separate DOM elements. */
  background-image: repeating-linear-gradient(
    to right,
    color-mix(in srgb, var(--color-on-background-color) 10%, transparent) 0,
    color-mix(in srgb, var(--color-on-background-color) 10%, transparent) 1px,
    transparent 1px,
    transparent calc(100% / 11)
  );
}

/* Loading-fill entrance, triggered by the same .reveal/.is-visible
   toggle as the rest of this section (see main.js — no new observer
   needed). .approach-bar is now just a positioning shell: the actual
   orange box is .approach-bar-fill, a separate absolutely-positioned
   layer growing via transform: scaleX() from transform-origin: left,
   so it visually grows outward from the bar's own start position —
   not the timeline's left edge — without scaling/distorting the
   label text on top of it. Sharp corners (border-radius: 0) per the
   brief, no rounding on any of the 4 bars. */
.approach-bar {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  min-height: 3rem;
  border-radius: 0;
  color: var(--color-on-primary-color);
  font-family: var(--font-sans), sans-serif;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
}

.approach-bar-fill {
  position: absolute;
  inset: 0;
  background-color: var(--color-primary-color);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 700ms var(--ease-standard);
}

.reveal.is-visible .approach-bar-fill {
  transform: scaleX(1);
}

.approach-bar-label {
  position: relative;
  z-index: 1;
  padding-inline: var(--spacing-4);
  /* Bumped from the bar's own --font-size-sm (14px) — family, weight,
     and color are untouched (still inherited from .approach-bar).
     Checked against all 4 bars' actual widths before choosing this:
     even the longest label ("Launch & Beyond", 112px natural width at
     14px) has 376px of available room in its bar, so scaling up to
     20px (~160px) stays comfortably clear of any overflow. */
  font-size: var(--font-size-xl);
}

.approach-bar--1 { grid-column: 1 / 4; grid-row: 1; }
.approach-bar--2 { grid-column: 3 / 7; grid-row: 2; }
.approach-bar--3 { grid-column: 5 / 10; grid-row: 3; }
.approach-bar--4 { grid-column: 8 / 12; grid-row: 4; }

/* Same duration-per-unit-width rate for all 4 (~233ms/unit), so the
   fill reads as one steady rate rather than each bar picking its own
   speed — bar widths in grid tracks are 3/4/5/4 respectively (e.g.
   bar 1 spans column-line 1 to 4, a width of 3), giving durations
   proportional to length: shortest (Discovery, 3 units) lands at
   700ms, inside the requested 600-900ms range; the rest scale up from
   there rather than syncing to a shared end time. */
.approach-bar--1 .approach-bar-fill { transition-duration: 700ms; }
.approach-bar--2 .approach-bar-fill { transition-duration: 930ms; }
.approach-bar--3 .approach-bar-fill { transition-duration: 1160ms; }
.approach-bar--4 .approach-bar-fill { transition-duration: 930ms; }

.approach-axis {
  grid-column: 1 / 12;
  grid-row: 5;
  display: grid;
  /* Exactly 4 markers, one per phase (01-04), aligned to the 4
     equal-width .approach-columns text items below — not to the bars'
     own staggered 11-unit start lines. Same column count + gap as
     .approach-columns so each marker sits flush under its phase card. */
  grid-template-columns: repeat(4, 1fr);
  column-gap: var(--spacing-24);
  margin-top: var(--spacing-2);
  padding-top: var(--spacing-2);
  border-top: 1px solid color-mix(in srgb, var(--color-on-background-color) 18%, transparent);
}

.approach-axis-tick {
  font-family: var(--font-mono), monospace;
  font-size: var(--font-size-xs);
  color: var(--color-outline-color);
}

/* Switched from an 11-track grid mirroring each bar's start position
   to a plain equal-width 4-column grid, by request — the bar-tied
   widths (2/2/3/4 of 11 tracks) were structurally why the gaps could
   never read as even: three of the four bar-start anchors aren't
   evenly spaced to begin with. The 4 items still fall in the same
   left-to-right order as their bars above (auto-placed, one per
   column, no explicit grid-column needed anymore), which keeps them
   in each bar's general vicinity without the column WIDTH being
   tied to it. */
.approach-columns {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  column-gap: var(--spacing-24);
  margin-top: var(--spacing-8);
}

.approach-text {
  /* Grid items default to min-width: auto, which floors their size at
     their content's max-content width — normally invisible, but the
     h3's white-space: nowrap below gives two of the four items a much
     wider max-content than the other two, which was pulling their
     1fr tracks wider than their fair equal share (measured: columns
     went from an even 212px each to 187/187/218/257 once nowrap was
     added) and shrinking the other columns to compensate. This forces
     every column back to a genuinely equal 1fr regardless of content. */
  min-width: 0;
}

.approach-text h3 {
  font-size: var(--font-size-3xl);
  /* Keeps all 4 titles on one line at the equal 212px column width.
     "Design & Build" and "Launch & Beyond" are wider than the column
     at this font-size (218px/257px measured vs. 212px available), so
     without this they wrap; nowrap lets them overflow into the
     generous column-gap space instead, which is empty at this height
     for every column and has no overflow:hidden anywhere in the
     ancestor chain to clip it. Font-size is left at --font-size-3xl
     (unchanged, all 4 still match) rather than shrunk uniformly to
     force a fit — that would have undone the recent title size
     increase for something that already renders cleanly this way.
     That headroom only holds while the container is near its full
     1200px max-width, though: .approach-columns's 4 tracks stay
     locked at an even ~212px as long as the container itself is
     capped by --container-max, but below a ~1200px viewport the
     container starts shrinking 1:1 with it, so the columns (and the
     gap's ability to absorb the overflow) shrink too while the
     titles' overflow amount does not — confirmed to actually collide
     with the next column by 768px. The media query below steps the
     size back down across that narrower band; full-width desktop is
     unaffected. */
  white-space: nowrap;
}

/* Both steps below are floored at min-width: 768px — that's the exact
   mobile-stack breakpoint (see @media (max-width: 767px) further
   down), where .approach-columns stops being a 4-column grid at all
   and each item gets the full container width instead. Without the
   floor, these rules also matched there and were shrinking the
   titles to 14px on phones for no reason — there's no overflow risk
   once every item has ~350px to itself. */
@media (min-width: 768px) and (max-width: 1200px) {
  .approach-text h3 {
    /* See the comment above — keeps "Design & Build" and "Launch &
       Beyond" clear of the next column once the container drops
       below its max-width and the 4 columns start shrinking. */
    font-size: var(--font-size-2xl);
  }
}

@media (min-width: 768px) and (max-width: 1023px) {
  .approach-text h3 {
    /* Second step: "Design & Build" and "Strategy" stay clear of
       their neighbor at --font-size-2xl all the way down to the
       767px mobile-stack breakpoint, but "Launch & Beyond" (the
       last column, with no neighbor to check against — only the
       container's own trailing margin) was overflowing PAST THE
       CONTAINER at this width, forcing the entire page into
       horizontal scroll below ~1024px. Confirmed via
       document.documentElement.scrollWidth vs window.innerWidth,
       not just the inter-column collision check. */
    font-size: var(--font-size-sm);
  }
}

.approach-text p {
  margin-top: var(--spacing-3);
  font-size: var(--font-size-xl);
  color: var(--color-on-surface-variant-color);
}

@media (max-width: 767px) {
  /* Below the breakpoint the cascading axis has no room to work, so
     the whole thing collapses to a plain stacked flex column instead:
     display:contents on the two grid wrappers flattens their children
     (the 4 bars + axis, the 4 text items) into .approach-timeline
     itself, and `order` below interleaves them back into
     bar-then-text-per-phase reading order. grid-column/grid-row on
     the bars and text items are simply inert once their layout
     context is flex instead of grid, so nothing needs to be reset. */
  .approach-timeline {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-3);
  }

  .approach-chart,
  .approach-columns {
    display: contents;
  }

  .approach-axis {
    display: none;
  }

  .approach-bar--1 { order: 1; }
  .approach-text--1 { order: 2; }
  .approach-bar--2 { order: 3; }
  .approach-text--2 { order: 4; }
  .approach-bar--3 { order: 5; }
  .approach-text--3 { order: 6; }
  .approach-bar--4 { order: 7; }
  .approach-text--4 { order: 8; }

  .approach-text {
    margin-bottom: var(--spacing-3);
  }

  /* The h3 restates the exact text already on this phase's orange
     .approach-bar-label above it — redundant once both are visible in
     the same stacked reading order on mobile. Visually hidden rather
     than display:none: .approach-bar is aria-hidden="true" (it's
     purely decorative, a Gantt-chart bar, on every breakpoint), so
     this h3 is the ONLY accessible source of each phase's name —
     removing it from the accessibility tree here would silently drop
     "Discovery"/"Strategy"/etc for screen reader users on mobile.
     Desktop (>=768px, the 4-column grid) is untouched. */
  .approach-text h3 {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
}

/* ---------------------------------------------------------------------
   Why Us
------------------------------------------------------------------------ */
.why-us {
  background-color: var(--color-surface-container-lowest-color);
}

@media (min-width: 768px) {
  .why-us .container {
    /* Breaks past the site's standard 75rem/1200px container rail
       (used by every other section) specifically so "15" and "20" can
       grow well beyond what that width allows. Only matters on
       viewports wider than ~1200px — below that this section's
       container was already viewport-bound, not rail-bound, so it's
       unaffected. */
    max-width: 115rem;
  }
}

.why-us .section-header {
  /* Scoped down from the shared 48px so "Why Us" sits closer to its
     stat blocks than other sections' headers do — collapses together
     with .stat-blocks' own margin-top below into a single ~32px gap
     (this rule's 8px + .stat-block's 24px padding-top). */
  margin-bottom: var(--spacing-2);
}

/* Matches #who-we-help/#services/#approach .section-title exactly —
   those three already share this larger statement-style size, Why
   Us was left at the plain shared .section-title default (3xl/4xl)
   and read noticeably smaller next to them. Font family, weight,
   color, and style are all untouched — this only overrides the size. */
#why-us .section-title {
  font-size: clamp(var(--font-size-5xl), 3vw + 2.25rem, var(--font-size-6xl));
}

.stat-blocks {
  margin-top: var(--spacing-2);
}

.stat-block {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-8);
  min-height: 96vh;
  padding-block: var(--spacing-6);
}

.stat-copy {
  flex: 0 0 auto;
  /* Narrowed repeatedly now (30% -> 22% -> 18% -> 15% -> 12%) — each
     numeral was still hitting its own font-size clamp ceiling before
     it reached the edge of its available column, so the column
     keeps growing alongside the ceiling. */
  width: 12%;
  min-width: 9rem;
}

.stat-copy h3 {
  font-family: var(--font-display), serif;
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
}

.stat-copy p {
  margin-top: var(--spacing-3);
  font-size: var(--font-size-base);
  color: var(--color-on-surface-variant-color);
}

/* The numeral's image-fill effect: the digit is real text (not an SVG
   or a flat-colored glyph), clipped so only the glyph's own shape
   paints — background-clip: text restricts painting to the text, then
   the photo is set as that element's background-image, so the photo
   is only visible through the numeral's counters and curves, exactly
   like a stencil. background-color is a token-based fallback fill
   (shows through if the background-image 404s, e.g. before the real
   photos below are supplied) so the numeral never renders fully
   invisible. font-size uses clamp() with a large vw component so it
   scales aggressively with viewport width — deliberately sized to
   nearly fill .stat-block's own height, not just "large heading"
   territory. line-height: 0.8 pulls the glyph's own internal leading
   in tight, since the goal is maximum numeral height within the
   block, not comfortable text spacing. */
.stat-numeral {
  flex: 1 1 auto;
  /* Flex items default to min-width: auto, which floors them at their
     own content's width — with text-align: right and no wrap
     possible on a 1-2 character numeral, that was letting the two
     2-digit numerals ("15", "20") push past their column and the
     page's own right edge instead of respecting the space actually
     available next to .stat-copy. */
  min-width: 0;
  /* Switched from IBM Plex Sans to the site's own display serif
     (Fraunces, already used for every heading — "Our Services", "No
     phase works alone.", etc.) at its heaviest loaded weight, so the
     numerals read as the same editorial type family rather than a
     generic sans stuck in next to serif headings everywhere else.
     Fraunces is a variable font with an optical-size axis tuned for
     exactly this — dramatic, high-contrast forms at large display
     sizes — and weight 900 (added to the Google Fonts request
     alongside the existing 400-700) thickens its naturally thin
     hairline strokes enough to still read cleanly once part of each
     glyph is revealing a photo instead of solid color. */
  font-family: var(--font-display), serif;
  font-weight: var(--font-weight-black);
  line-height: 0.8;
  /* Sized for a 1-digit numeral ("5") — see .stat-numeral--wide below
     for the 2-digit blocks, which need their own clamp to stay clear
     of the column at the same relative width. Fraunces' digit
     proportions differ from the old IBM Plex Sans (re-measured after
     the font swap: single-digit ink is ~0.57x font-size, was 0.6x;
     double-digit is ~0.98x font-size, was 1.2x — Fraunces runs
     narrower, especially for 2-digit numbers, which is why
     .stat-numeral--wide's ceiling jumps by more than this one below).
     Driven by vw so the glyph's own WIDTH grows with the available
     column space, not just its height. Checked against the column's
     actual current width each time this is raised so it doesn't push
     "5" into horizontal overflow. */
  font-size: clamp(24rem, 93vw, 96rem);
  text-align: right;
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
  background-color: var(--color-primary-color);
  -webkit-background-clip: text;
  background-clip: text;
  color: var(--color-primary-color);
  -webkit-text-fill-color: transparent;
}

.stat-numeral--wide {
  font-size: clamp(24rem, 56vw, 57rem);
}

.stat-block--1 .stat-numeral {
  background-image: url("../assets/images/stat-1.svg");
}

.stat-block--2 .stat-numeral {
  background-image: url("../assets/images/stat-2.svg");
  /* Pulls "1" and "5" closer together — Fraunces' default spacing
     between these two digits reads a bit loose at this scale.
     -0.04em left the "1"/"5" boxes genuinely touching (confirmed via
     isolated zoomed screenshots, crowding "5"'s flag-tip/stem against
     "1"). -0.03em is the tightest value that still keeps a real,
     visible gap — checked incrementally against -0.035em, which was
     visibly approaching the same touching point. */
  letter-spacing: -0.03em;
}

.stat-block--3 .stat-numeral {
  /* Swapped from the temporary product-bottle placeholder (stat-3.svg)
     to a supplied real asset. Same SVG structure as stat-1/2.svg (a
     <rect> filled via a <pattern>/embedded raster image, not a plain
     vector graphic) — background-size: cover + background-position:
     center below already handle that identically to a raster photo,
     so no mask-specific changes were needed for the swap itself. */
  background-image: url("../assets/images/number-1.svg");
  /* Pulls "2" and "0" closer together, matching "15"'s treatment.
     Capped lower than "15"'s -0.04em: "2"/"0" have different natural
     side-bearings than "1"/"5", so the same value left their advance
     boxes touching (measured ~0px gap), crowding "0" against "2" and
     flattening its left curve — same root cause as the "15" issue,
     just triggered at a different threshold for this glyph pair. */
  letter-spacing: -0.02em;
}

/* min-width: 768px on everything in this block — without it, these
   rules' higher specificity (2 classes: .stat-block--N .stat-numeral
   or .stat-copy) than the mobile @media (max-width: 767px) overrides
   for the bare .stat-numeral / .stat-copy (1 class each) would win
   there too regardless of source order, since specificity beats
   cascade order across media query boundaries. Confirmed via two
   real mobile screenshots showing "15" and "20" both cut off at the
   viewport edge before this floor was added — this isn't
   theoretical, it broke both of them. */
@media (min-width: 768px) {
  .stat-block--1 .stat-copy {
    /* Widened well past the shared 12%/9rem baseline so the
       description paragraph can wrap to 4 lines instead of 6 at a
       readable measure. Narrows "5"'s available numeral column, so
       its font-size clamp below is pulled back to compensate. */
    width: 22%;
    min-width: 19rem;
    /* Pinned to a fixed 304px past this point — without a ceiling,
       the 22% share would keep growing along with the widened
       .why-us container below, eating into the room meant for the
       numeral instead of giving it to "15"/"20". */
    max-width: 19rem;
  }

  .stat-block--1 .stat-numeral {
    /* Same calc()-based approach as "15"/"20": mirrors the box-width
       formula (viewport minus the container's fixed offset) instead
       of a flat vw ratio, so it can safely use the much bigger box
       "5" now has at wide viewports (after .why-us .container was
       widened) without overflowing at narrow ones — a flat 95vw
       overflowed real box width at 900px/768px when tried directly. */
    font-size: clamp(20rem, calc(157vw - 653px), 140rem);
  }

  .stat-block--2 .stat-copy {
    /* Same treatment as block 1: widened past the shared 12%/9rem
       baseline so this description can wrap to 4 lines too. */
    width: 22%;
    min-width: 19rem;
    /* Pinned to a fixed 304px past this point — without a ceiling,
       the 22% share would keep growing along with the widened
       .why-us container below, eating into the room meant for the
       numeral instead of giving it to "15"/"20". */
    max-width: 19rem;
  }

  .stat-block--2 .stat-numeral {
    /* Pulled back to match the narrower column left over now that
       .stat-copy above is wider. Sized against "15"'s actual measured
       glyph-width-to-font-size ratio (0.9805, confirmed via a Range
       bounding-rect measurement) against the box width at each tested
       breakpoint (353px-800px), so the glyph itself stays inside its
       box rather than just the div. */
    /* Now that .why-us .container can grow well past the site's
       standard rail, the numeral's available box width no longer
       tracks viewport width as a flat percentage — it's (viewport -
       ~415px) up to the container's new cap. A pure Nvw clamp can't
       fit that offset-based curve without being badly conservative at
       one end, so this mirrors the box-width formula directly via
       calc(), scaled down against "15"'s real glyph ratio (0.9805)
       with a safety margin, then capped by min/max like normal. */
    font-size: clamp(19rem, calc(95vw - 394px), 90rem);
  }

  .stat-block--3 .stat-copy {
    /* Narrowed back below the other two blocks' shared 304px column
       (15.5rem/248px, a small margin above the measured 240px point
       where its 3-line description starts wrapping to 4) to hand the
       freed-up space to "20"'s numeral — its margins were already
       tight (down to 8px at 768px) with the shared column width, so
       further growth needed real extra room, not just a smaller
       safety margin. */
    width: 15.5rem;
    min-width: 15.5rem;
    max-width: 15.5rem;
  }

  .stat-block--3 .stat-numeral {
    /* "20"'s actual measured glyph-width-to-font-size ratio is 1.2675
       (Range bounding-rect measurement) — noticeably wider than "15"'s
       0.9805. Recomputed against the bigger box freed up by narrowing
       .stat-copy above (240px width headroom found by testing where
       its 3-line description actually breaks to 4). ~5% safety
       margin at the tightest breakpoints (768px/1920px). */
    font-size: clamp(18rem, calc(71vw - 237px), 71rem);
  }
}

@media (max-width: 767px) {
  .stat-block {
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: var(--spacing-4);
    min-height: 0;
    padding-block: var(--spacing-8);
  }

  .stat-copy {
    width: 100%;
  }

  .stat-numeral {
    width: 100%;
    text-align: left;
    font-size: clamp(6rem, 45vw, 14rem);
  }
}

.trust-note {
  margin-top: var(--spacing-12);
  padding-top: var(--spacing-8);
  border-top: 1px solid var(--color-outline-variant-color);
  color: var(--color-on-surface-variant-color);
  font-size: var(--font-size-sm);
}

/* ---------------------------------------------------------------------
   Our Story
------------------------------------------------------------------------ */
.story-layout {
  display: grid;
  grid-template-columns: 1fr;
  /* Below 1024px .story-heading-col and .story-copy stack into one
     vertical read (heading directly above the paragraph) instead of
     sitting side by side — this gap was tuned as if it separated two
     independent blocks (32px) when it's actually the headline-to-
     paragraph spacing for that whole range, reading as a gap rather
     than one connected block. Halved so the two feel like a single
     unit; .story-cta's own 32px margin-top (untouched, below) still
     keeps the button clearly separated from the paragraph above it. */
  gap: var(--spacing-4);
  align-items: start;
}

@media (min-width: 1024px) {
  .story-layout {
    /* Same 0.8fr/1.2fr split as before the photo column was removed —
       still reads correctly now that the left column is a big heading
       instead of a heading+eyebrow stacked beside an image. At this
       width the two columns sit side by side, not stacked, so this
       gap is horizontal column spacing, not the headline-to-paragraph
       relationship the rule above controls — left at its own tuned
       value, not tied to the stacked gap above. */
    grid-template-columns: 0.8fr 1.2fr;
    gap: var(--spacing-16);
  }
}

/* Matches #approach/#who-we-help/#services/#why-us .section-title
   exactly — same statement-size override (5xl/6xl) AND, unlike the
   initial build, no distinct typeface/case/tracking override either:
   this heading now uses the site's normal Fraunces display serif via
   the shared h1-h4 rule, same as every other section's heading. */
#story .section-title {
  font-size: clamp(var(--font-size-5xl), 3vw + 2.25rem, var(--font-size-6xl));
}

/* Matches every other section's statement-size heading (5xl/6xl) for
   the same page-wide consistency — not explicitly requested for FAQ,
   but leaving it at the plain smaller .section-title default would
   read as inconsistent next to Approach/Why Us/Story right above it. */
#faq .section-title {
  font-size: clamp(var(--font-size-5xl), 3vw + 2.25rem, var(--font-size-6xl));
}

/* Mobile-only step-down for all 6 secondary-section headings sharing
   the 5xl/6xl clamp above (Who We Help, Services, Approach, Why Us,
   Story, FAQ) — the clamp resolves to its 48px floor at mobile
   widths, tying these for LARGEST text on the page with the hero
   headline itself. Same fix already applied to the CTA teaser and
   footer hero headings (both also 2.75rem/44px on mobile) for the
   same reason: those are one-off headline elements, not tied to a
   shared class, hence separate rules there instead of one more
   selector added here. Grouped into one rule (rather than 6 scattered
   media queries matching each section's individual override above)
   since all 6 get the identical treatment — one source of truth
   instead of the same declaration repeated 6 times. Desktop
   (>=768px) is untouched; the hero's own clamp has a fluid vw term
   that's already resolving above 48px well before 768px, so it stays
   the largest text on mobile too. */
@media (max-width: 767px) {
  #who-we-help .section-title,
  #services .section-title,
  #approach .section-title,
  #why-us .section-title,
  #story .section-title,
  #faq .section-title {
    font-size: 2.75rem;
  }
}

/* #who-we-help-title carries a hard <br> in the markup (splits "For
   businesses ready" from the accent-colored "for their next
   chapter." as two independently-wrapping chunks) — at 44px/mobile
   widths each side then wraps again on its own, landing on 4 lines
   total ("For businesses" / "ready" / "for their next" / "chapter.").
   display:none on just the <br>, mobile-only, removes that forced
   split and lets the whole sentence reflow as one continuous run —
   confirmed this lands on exactly 3 lines ("For businesses" / "ready
   for their" / "next chapter.") at 360-430px. The color split is
   unaffected either way: .text-accent is a semantic span around "for
   their next chapter.", not tied to the <br> position, so words still
   render orange/black correctly regardless of where the new line
   breaks fall. Desktop (>=768px) keeps the <br> exactly as authored —
   this is scoped media-query-only, no HTML change. */
@media (max-width: 767px) {
  #who-we-help-title br {
    display: none;
  }
}

.story-copy p {
  color: var(--color-on-surface-variant-color);
  font-size: var(--font-size-lg);
}

.story-cta {
  margin-top: var(--spacing-8);
}

.btn-arrow-icon {
  width: 1.125rem;
  height: 1.125rem;
}

/* ---------------------------------------------------------------------
   FAQ
------------------------------------------------------------------------ */
.faq-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--spacing-8);
  align-items: start;
}

@media (min-width: 1024px) {
  .faq-layout {
    /* Same 0.8fr/1.2fr split as .story-layout, for the same reason:
       narrower heading column, wider content column. */
    grid-template-columns: 0.8fr 1.2fr;
    gap: var(--spacing-16);
  }
}

.faq-cta {
  margin-top: var(--spacing-8);
}

.faq-list {
  border-top: 1px solid var(--color-outline-variant-color);
  /* Drives .faq-index below — see the comment there for why this is a
     counter instead of the hardcoded "01" text the markup used to
     carry. */
  counter-reset: faq-index;
}

.faq-item {
  border-bottom: 1px solid var(--color-outline-variant-color);
  counter-increment: faq-index;
}

/* Mobile-only trim: 7 questions is too many to browse in a mobile
   accordion, so these two (process detail already covered by the
   Approach section above, and an audience question redundant with
   "Who do you typically work with?") drop out below 768px, leaving
   the 5 highest-value questions. Desktop keeps all 7. Elements with
   display:none don't generate a box, so .faq-item's counter-increment
   above skips them automatically — the visible mobile items renumber
   01-05 with no gaps, no JS required. */
@media (max-width: 767px) {
  .faq-item--secondary {
    display: none;
  }
}

.faq-question-heading {
  /* h3 wrapping the button, for AT heading-navigation of the question
     list — margin/font reset to 0 so it's invisible as a "heading"
     and reads as plain button styling instead. */
  margin: 0;
  font: inherit;
  font-weight: inherit;
}

.faq-question {
  display: flex;
  align-items: center;
  gap: var(--spacing-5);
  width: 100%;
  padding-block: var(--spacing-6);
  background: none;
  border: none;
  cursor: pointer;
  text-align: left;
  color: inherit;
}

.faq-index {
  /* Same visual language as .audience-card-number (mono, medium
     weight, muted outline color) — the site's one existing
     numbered-index precedent — scaled down for a compact list row
     instead of a large card. */
  flex: 0 0 auto;
  font-family: var(--font-mono), monospace;
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-medium);
  color: var(--color-outline-variant-color);
}

/* Generated from .faq-list's counter (empty span in the markup, now
   aria-hidden) instead of hardcoded "01"-"07" text — with 2 items
   dropping out on mobile (see .faq-item--secondary above), a hardcoded
   number would leave gaps like 01, 02, 04 in the visible mobile list.
   decimal-leading-zero matches the original two-digit "01" styling. */
.faq-index::before {
  content: counter(faq-index, decimal-leading-zero);
}

.faq-question-text {
  flex: 1 1 auto;
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
}

.faq-icon {
  position: relative;
  flex: 0 0 auto;
  width: 1.125rem;
  height: 1.125rem;
  color: var(--color-primary-color);
}

.faq-icon-line {
  position: absolute;
  top: 50%;
  left: 50%;
  background-color: currentColor;
  border-radius: var(--radius-full);
  transform: translate(-50%, -50%);
  transition: transform var(--motion-fast) var(--ease-standard);
}

.faq-icon-line--h {
  width: 100%;
  height: 2px;
}

.faq-icon-line--v {
  width: 2px;
  height: 100%;
}

/* Collapsing the vertical stroke to 0 turns the + into a − — no
   separate icon asset/state needed. */
.faq-item.is-open .faq-icon-line--v {
  transform: translate(-50%, -50%) scaleY(0);
}

/* grid-template-rows: 0fr -> 1fr is the only way to transition to/
   from an intrinsic (auto) height without JS measuring scrollHeight —
   max-height would need an arbitrary large value (jerky/inaccurate
   timing) and the [hidden] attribute can't transition at all (it's a
   display:none flip). The inner wrapper's overflow: hidden is what
   actually clips the content during the animation; this outer
   element only owns the grid track itself. */
.faq-answer {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--motion-slow) var(--ease-standard);
}

.faq-answer-inner {
  overflow: hidden;
}

.faq-item.is-open .faq-answer {
  grid-template-rows: 1fr;
}

.faq-answer p {
  padding-bottom: var(--spacing-6);
  color: var(--color-on-surface-variant-color);
}

/* ---------------------------------------------------------------------
   CTA Teaser (index.html only — the actual Contact section/form now
   lives on its own page, contact.html; the two share the same dark
   background/text-color pair by design, see the shared values below.)
------------------------------------------------------------------------ */
.cta-teaser {
  background-color: var(--color-inverse-surface-color);
  color: var(--color-inverse-on-surface-color);
  /* The source image (cta-svg1) is a vivid, saturated orange swirl —
     brighter/more saturated than this section's near-black background,
     which would both fight the heading's legibility and rival the
     "Book Consultation" button (also orange) for visual dominance if
     shown at full strength. Layering a flat dark wash over it — the
     same hsl(32, 37%, 6%) this section's own background-color
     resolves to (--color-inverse-surface-color, not used directly
     here since it carries no alpha channel) at 80% opacity, roughly
     the middle of the requested 75-85% range — keeps the image
     reading as warm ambient texture behind the content rather than
     competing with it. background-color above stays as the fallback
     paint if the image fails to load. */
  background-image: linear-gradient(hsla(32, 37%, 6%, 0.8), hsla(32, 37%, 6%, 0.8)), url("../assets/images/cta-svg1.svg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* Single column (eyebrow+heading, then button below) below 768px —
   there isn't room for a side-by-side split at narrow widths. */
.cta-teaser-content {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--spacing-8);
}

@media (min-width: 768px) {
  .cta-teaser-content {
    /* ~68%/32% split (left column takes the "roughly 65-70%" the
       heading needs to wrap across 1-2 lines; right column is just
       wide enough for the button, not stretched to fill it — see
       justify-self below). align-items: center is what actually
       centers the button against the heading block's full height,
       not a fixed pixel offset — it stays correct regardless of
       whether the heading wraps to 1 or 2 lines. */
    grid-template-columns: 2fr 1fr;
    align-items: center;
    gap: var(--spacing-12);
  }
}

.cta-teaser-heading {
  max-width: 40rem;
}

#cta .section-title {
  /* Matches every other section's statement-size heading (5xl/6xl),
     same as #approach/#why-us/#story/#faq. */
  font-size: clamp(var(--font-size-5xl), 3vw + 2.25rem, var(--font-size-6xl));
}

/* At 390px this clamp resolves to its 48px floor (--font-size-5xl) —
   confirmed via a live screenshot: the headline wraps to 4 lines and
   runs edge-to-edge across the section's mobile width, on the larger
   end for a two-line-ish statement headline squeezed into the CTA
   teaser's tightened mobile padding. Same issue as the footer hero
   heading, scaled back slightly here rather than a full tier down —
   this is the section's actual headline (not a secondary element), so
   the cut is smaller: just enough to feel proportionate to the
   section's mobile width without losing its punch as the page's
   closing statement. Desktop (>=768px, where the clamp's vw component
   already pushes it well past the 48px floor) is unaffected. */
@media (max-width: 767px) {
  #cta .section-title {
    font-size: 2.75rem;
  }
}

.cta-teaser-button {
  margin-top: var(--spacing-8);
}

@media (min-width: 768px) {
  .cta-teaser-button {
    /* No longer stacked directly below the heading at this width —
       positioning now comes entirely from the grid (column placement
       + align-items: center above). */
    margin-top: 0;
    /* Without this, a grid item defaults to justify-self: stretch,
       which would stretch the button to fill the whole right column
       width instead of sitting at its own natural pill size. */
    justify-self: start;
  }
}

/* Mobile-only height trim, spacing first (per request — the heading's
   size/punch is intentionally untouched here). At 390px this section
   was measuring ~492px tall: 48px section padding each edge, 12px
   eyebrow-to-heading gap, 32px heading-to-button gap, then straight
   into the footer with no further spacer (the section's own
   padding-bottom IS the space before the footer, so trimming it
   handles both "reduce bottom padding" and "less excess space before
   the footer" at once). Knocks off ~48px total: 16+16 off the section
   edges, 4 off the eyebrow gap, 12 off the heading-to-button gap.
   #cta (ID) and this media query both outrank .section's own
   max-width:767 rule and .cta-teaser-content's unscoped base gap
   regardless of source order, so this is safe wherever it sits in the
   file. Desktop (>=768px, side-by-side grid with its own gap/margin
   rules above) is untouched. */
@media (max-width: 767px) {
  #cta {
    padding-block: var(--spacing-8);
  }

  #cta .eyebrow {
    margin-bottom: var(--spacing-2);
  }

  .cta-teaser-content {
    gap: var(--spacing-5);
  }
}

/* ---------------------------------------------------------------------
   Contact
------------------------------------------------------------------------ */
.contact {
  background-color: var(--color-inverse-surface-color);
  color: var(--color-inverse-on-surface-color);
}

.contact .section-lede {
  color: color-mix(in srgb, var(--color-inverse-on-surface-color) 75%, transparent);
}

.contact-hero-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--spacing-16);
}

@media (min-width: 1024px) {
  .contact-hero-grid {
    grid-template-columns: 1fr 1fr;
    gap: var(--spacing-20);
    /* start (not stretch/center) so each column sizes to its own
       natural content height — the headline is free to grow/wrap
       without being stretched to match the form column's height, and
       the two columns are allowed to end at different heights
       (asymmetric, by design: whatever follows starts below the
       taller of the two automatically, since neither is
       artificially inflated to match the other). */
    align-items: start;
  }

  /* Right column's offset is relative to the LEFT column's own
     rendered height (--contact-left-height, kept in sync by a
     ResizeObserver in main.js), not a fixed pixel value — as the
     headline grows/wraps and the left column gets taller, this scales
     with it instead of staying pinned. 0.5 lands the form's top
     roughly at the left column's own vertical midpoint; the form is
     the taller of the two (measured ~1.8-2x), so it naturally runs on
     past the left column's bottom — expected asymmetry, not a bug.
     The 3rem fallback covers the instant before JS first runs and
     browsers without ResizeObserver. */
  .contact-hero-right {
    margin-top: calc(var(--contact-left-height, 3rem) * 0.5);
  }
}

.contact-hero-left {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* Hidden per request — markup/shared .contact-badge class kept intact
   in case it's needed again (same pattern as .hero-content .eyebrow
   elsewhere in this file). Was muted/neutral (deliberately not the
   brand orange, which stays reserved for CTAs) with no live-status
   dot. */
.contact-badge {
  display: none;
  align-items: center;
  padding: var(--spacing-2) var(--spacing-4);
  border-radius: var(--radius-full);
  border: 1px solid color-mix(in srgb, var(--color-inverse-on-surface-color) 25%, transparent);
  background-color: color-mix(in srgb, var(--color-inverse-on-surface-color) 10%, transparent);
  color: color-mix(in srgb, var(--color-inverse-on-surface-color) 85%, transparent);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  margin-bottom: var(--spacing-6);
}

/* Oversized display headline, well past .section-title's normal 3xl/4xl
   scale (Anakle-style "Let's Talk!" reference) — a vw-driven clamp
   rather than a fixed token size: at this scale a flat rem value
   can't stay right-sized across the full viewport range. Bold weight
   (not the h1-h4 default 600) to read as heavier/oversized. Bumped further
   again (was 3.5rem/7vw+2rem/7.5rem) per request to grow bigger still.
   Mobile/tablet keep normal wrapping — at those widths the column is
   too narrow for this size to fit on one line without either
   overflowing the viewport or shrinking back down, and wrapping there
   was already the established look (see chat). */
.contact-hero-title {
  font-family: var(--font-display), serif;
  font-weight: var(--font-weight-bold);
  line-height: var(--line-height-none);
  font-size: clamp(3.5rem, 7vw + 2rem, 9rem);
  margin: 0;
}

/* Desktop only (matches the two-column breakpoint): let the headline
   grow horizontally instead of wrapping to a second line — it's a
   flex child of .contact-hero-left with align-items:flex-start, so it
   already sizes to its own natural (shrink-to-fit) width rather than
   being stretched to the grid column's width, meaning nowrap lets it
   spill past the left column's own edge without pushing the grid
   track wider or displacing the form column. Safe against the form
   visually: the title's single-line height is well under the vertical
   offset .contact-hero-right's margin-top already puts before the
   form begins (see that rule), so there's no overlap at any width
   this was verified at. */
@media (min-width: 1024px) {
  .contact-hero-title {
    white-space: nowrap;
  }
}

.contact-hero-sub {
  margin-top: var(--spacing-5);
  max-width: 28rem;
  font-size: var(--font-size-xl);
  color: color-mix(in srgb, var(--color-inverse-on-surface-color) 75%, transparent);
}

/* Icons beside the copy+button on desktop; stacks vertically on mobile
   (max-width: 767px) since that pairing needs more horizontal room
   than a phone screen allows, per request. Same breakpoint the rest
   of the site treats as its primary mobile/desktop split. */
.contact-cta-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-6);
  margin-top: var(--spacing-10);
}

@media (max-width: 767px) {
  .contact-cta-row {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--spacing-5);
  }
}

/* Same circular-outline treatment as .footer-social (40px circle,
   1.125rem icon, spacing-3 gap, identical stroke weights baked into
   each icon's own SVG) — just re-pointed at the inverse-on-surface
   tokens since this section's background is dark, unlike the light
   footer. Horizontal row (was a vertical column) — a single 40px-tall
   row reads more compact and balanced beside/above the tagline+button
   than a ~144px-tall stack of 3 circles did, on both desktop (beside
   .contact-cta-copy) and mobile (.contact-cta-row switches to column
   below, stacking this row above the copy). */
.contact-social {
  display: flex;
  flex-direction: row;
  gap: var(--spacing-3);
  flex: 0 0 auto;
}

.contact-social a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-full);
  border: 1px solid color-mix(in srgb, var(--color-inverse-on-surface-color) 30%, transparent);
  color: var(--color-inverse-on-surface-color);
  transition: background-color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}

.contact-social a:hover,
.contact-social a:focus-visible {
  background-color: var(--color-primary-color);
  border-color: var(--color-primary-color);
  color: var(--color-on-primary-color);
}

.contact-social svg {
  width: 1.125rem;
  height: 1.125rem;
}

.contact-cta-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-3);
}

.contact-cta-copy p {
  color: color-mix(in srgb, var(--color-inverse-on-surface-color) 85%, transparent);
  font-weight: var(--font-weight-medium);
  max-width: 16rem;
}

.contact-form-header {
  font-family: var(--font-sans), sans-serif;
  /* Fluid clamp() (was flat 2xl below 768px, flat 3xl from 768px up). */
  font-size: clamp(var(--font-size-2xl), 1.5vw + 1.15rem, var(--font-size-3xl));
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  max-width: 32rem;
  margin: 0 0 var(--spacing-8);
}

.contact-form-grid {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-5);
}

/* .contact-form-grid's own `display: flex` is an author-stylesheet rule,
   so it beats the UA stylesheet's `[hidden] { display: none }` in the
   cascade regardless of selector specificity — without this override,
   setting the `hidden` attribute (main.js, on successful submit) would
   have no visual effect. */
.contact-form-grid[hidden] {
  display: none;
}

/* Flex column defaults every child to align-items:stretch (full-width) —
   override just the submit button back to its natural content-sized
   width, matching every other .btn-primary on the site. */
.contact-form-grid > .btn-primary {
  align-self: flex-start;
  margin-top: var(--spacing-2);
}

/* "Two-column grid where space allows" — auto-fit/minmax so this
   collapses to one column on its own once the column gets too narrow
   (mobile, or the right column at in-between viewport widths), rather
   than a fixed viewport breakpoint that doesn't know how wide this
   column actually is next to its sibling. */
.form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--spacing-5);
}

.form-field {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-2);
}

.form-field label {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
}

.form-field .required {
  color: var(--color-primary-color);
}

/* Built from --color-inverse-on-surface-color, not the site's usual
   on-background token: .contact sets that as its own text color since
   this whole section (form included) sits on a dark surface, and
   labels inherit it — the generic light-mode token would render this
   near-invisible here. */
.form-field .optional {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-regular);
  color: color-mix(in srgb, var(--color-inverse-on-surface-color) 60%, transparent);
}

.form-field input,
.form-field textarea,
.form-field select {
  width: 100%;
  padding: var(--spacing-3) var(--spacing-4);
  border-radius: var(--radius-md);
  border: 1px solid color-mix(in srgb, var(--color-inverse-on-surface-color) 30%, transparent);
  background-color: color-mix(in srgb, var(--color-inverse-on-surface-color) 8%, transparent);
  color: var(--color-inverse-on-surface-color);
  font-family: inherit;
  font-size: var(--font-size-base);
  min-height: 2.75rem;
  transition: background-color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard);
}

/* Real-time validation states — Full Name/Email/Company Name/message
   only (wired up in main.js). main.js never applies both classes at
   once, but .has-error is declared after .has-success here anyway so
   it wins on equal specificity if that ever changes.
   Success is a neutral brightness lift (a lighter shade of this same
   field's own dark background, plus a soft white-alpha border) rather
   than a color-coded state — deliberately no green/blue introduced, per
   request. */
.form-field.has-success input,
.form-field.has-success textarea {
  background-color: var(--color-field-success-bg);
  border-color: var(--color-field-success-border);
}

.form-field.has-error input,
.form-field.has-error textarea {
  border-color: var(--color-error-container-color);
}

.form-field select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: var(--spacing-10);
  cursor: pointer;
}

/* Select options render via the OS's own popup, outside our color
   tokens' reach — force dark-on-light here since most platforms
   default an unstyled <option> list to light regardless of the
   select's own (dark, inverse-surface) styling, which otherwise makes
   the closed control's text unreadable against its own open list. */
.form-field select option {
  color: var(--color-on-background-color);
  background-color: var(--color-surface-color);
}

.select-field {
  position: relative;
}

.select-chevron {
  position: absolute;
  top: 50%;
  right: var(--spacing-4);
  transform: translateY(-50%);
  width: 1rem;
  height: 1rem;
  color: color-mix(in srgb, var(--color-inverse-on-surface-color) 60%, transparent);
  pointer-events: none;
}

.form-field textarea {
  min-height: 8rem;
  resize: vertical;
}

.form-field input::placeholder,
.form-field textarea::placeholder {
  color: color-mix(in srgb, var(--color-inverse-on-surface-color) 55%, transparent);
}

.form-field small {
  font-size: var(--font-size-xs);
  color: var(--color-error-container-color);
}

.form-status {
  margin-top: var(--spacing-4);
  font-size: var(--font-size-sm);
  min-height: 1.25rem;
}

.form-status[data-state="success"] {
  color: var(--color-tertiary-container-color);
}

.form-status[data-state="error"] {
  color: var(--color-error-container-color);
}

/* Replaces the form in place after a successful Formspree submission
   (see main.js) — same section background (inherited from .contact)
   and the same heading/body typography treatment as the form it
   replaces (.contact-form-header / .contact-cta-copy p) so the
   confirmation doesn't read as a different surface. */
.contact-form-success {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-3);
  padding: var(--spacing-8) 0;
}

/* Same cascade trap as .contact-form-grid[hidden] above: this rule's own
   `display: flex` is author-origin and beats the UA stylesheet's
   `[hidden] { display: none }` regardless of specificity, so without
   this override the panel would render on page load despite starting
   with the `hidden` attribute in contact.html. */
.contact-form-success[hidden] {
  display: none;
}

.contact-form-success-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius-full);
  background-color: color-mix(in srgb, var(--color-tertiary-container-color) 20%, transparent);
  color: var(--color-tertiary-container-color);
  margin-bottom: var(--spacing-2);
}

.contact-form-success-icon svg {
  width: 1.5rem;
  height: 1.5rem;
}

.contact-form-success-title {
  font-family: var(--font-sans), sans-serif;
  font-size: clamp(var(--font-size-2xl), 1.5vw + 1.15rem, var(--font-size-3xl));
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  margin: 0;
}

.contact-form-success-sub {
  color: color-mix(in srgb, var(--color-inverse-on-surface-color) 85%, transparent);
  font-size: var(--font-size-base);
  max-width: 26rem;
  margin: 0;
}

/* ---------------------------------------------------------------------
   Footer
------------------------------------------------------------------------ */
.site-footer {
  /* Matches #approach's own background — that section has no
     background-color rule of its own, so it renders as the plain
     site background (--color-background-color); reused directly
     here rather than via #approach itself, since that's the actual
     value being matched, not a container-specific token. (Briefly
     swapped for --color-surface-container-color, a deeper token in
     the same family, when this read as too pastel — reverted back
     to this on request.) */
  background-color: var(--color-background-color);
  color: var(--color-on-background-color);
  padding-block: var(--spacing-10);
  /* Base for .footer-social's own left offset — how far it sits from
     the true viewport edge. Linear vw formula solved to hit exactly
     12px at a 320px viewport and 40px at 1920px, scaling continuously
     between and beyond. */
  --footer-edge-gap: calc(1.75vw + 0.4rem);
  /* Where .footer-social's own right edge lands (its 20px left offset
     + 40px icon-circle width) plus 16px of breathing room — the
     baseline .footer-brand-stack's margin-left is solved against
     below, so the two stay in sync off one shared value instead of
     independently-guessed numbers drifting apart. */
  --footer-icon-clearance: calc(var(--footer-edge-gap) + 76px);
}

@media (min-width: 768px) {
  .site-footer {
    /* Was a standalone 40px with no responsive tier at all — now uses
       the same 56px half-gap every .section does at this breakpoint,
       so Contact -> Footer lands on the uniform 112px too, instead of
       152px from a one-off value. Mobile (<768px) is unchanged. */
    padding-block: var(--spacing-14);
  }
}

/* Upper footer row: icons + brand/contact/book-a-call on the left,
   heading + supporting line on the right. */
.footer-hero-row {
  position: relative;
}

.footer-social {
  position: absolute;
  top: 0;
  left: calc(var(--footer-edge-gap) + 20px);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-3);
}

.footer-hero-content {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-10);
}

@media (min-width: 768px) {
  .footer-hero-content {
    flex-direction: row;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--spacing-12);
  }
}

.footer-left-col {
  flex: 0 0 auto;
}

/* margin-left clears .footer-social's own width + a visible gap —
   needed because that element no longer participates in this
   column's normal flow (it's absolutely positioned against the row
   above, not this column), so nothing here reserves space for it
   automatically. Solved from --footer-icon-clearance (an absolute,
   row-relative offset) minus this breakpoint's own .container
   padding-inline, since margin-left is itself relative to where the
   container's content-box already starts — a flat value here (the
   previous approach) only happened to clear at viewport widths wide
   enough for .container's margin-inline: auto centering to add bonus
   slack beyond its padding; below that width (most of mobile/tablet,
   where .container sits flush with only its padding as inset) it
   under-cleared and the icon column visibly overlapped the logo/
   contact text. */
.footer-brand-stack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-6);
  margin-left: calc(var(--footer-icon-clearance) - var(--spacing-5));
}

@media (min-width: 768px) {
  .footer-brand-stack {
    margin-left: calc(var(--footer-icon-clearance) - var(--spacing-8));
  }
}

/* Both bumped up from a shared --font-size-sm (14px, read as too
   quiet) — the label stays smaller/lighter than the email, just at a
   larger base size than before. font-size is smaller on mobile than
   the 2xl used from 768px up: the email has no natural break point,
   so at 2xl (24px) it doesn't fit this column's available width below
   768px (narrowest down to a 320px viewport) without overflowing past
   the edge of the page. */
.footer-contact-block {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-1);
  font-size: var(--font-size-xl);
}

@media (min-width: 768px) {
  .footer-contact-block {
    font-size: var(--font-size-2xl);
  }
}

.footer-contact-label {
  font-size: var(--font-size-lg);
  color: color-mix(in srgb, var(--color-on-background-color) 60%, transparent);
}

.footer-contact-block a {
  font-weight: var(--font-weight-medium);
  /* Backstop for the email itself: it has no spaces to wrap at, so
     without this it overflows its column (and the page) rather than
     wrapping, at any width narrow enough that the column can't fit it
     on one line — allow a mid-string break there instead. */
  overflow-wrap: anywhere;
}

.footer-contact-block a:hover {
  color: var(--color-primary-color);
}

/* Shared soft CTA text link (arrow included) — the footer's
   "Get in touch →" and the mid-page "Explore Our Services →" links.
   Deliberately NOT a .btn pill so these read as quiet text affordances
   instead of competing with the page's primary orange "Book
   Consultation" button. Muted variant color, primary on hover. */
.text-link {
  display: inline-block;
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-medium);
  color: color-mix(in srgb, var(--color-on-background-color) 60%, transparent);
  transition: color var(--motion-fast) var(--ease-standard);
}

.text-link:hover {
  color: var(--color-primary-color);
}

.footer-right-col {
  flex: 1 1 auto;
  max-width: 42rem;
}

/* Bumped up to match every other section's statement-size heading
   (5xl/6xl) exactly — was 3xl/5xl, a tier lower, and read as too
   small/not prominent enough per a later request. Sans-serif (the
   site's body font) and a muted variant color, not the bold black
   used for real section headings — "light-colored," per the original
   request, and deliberately softer than the giant wordmark below it
   so the two don't compete despite now being closer in size. */
.footer-hero-heading {
  margin: 0;
  font-family: var(--font-sans), sans-serif;
  font-weight: var(--font-weight-semibold);
  color: var(--color-on-surface-variant-color);
  line-height: var(--line-height-tight);
  font-size: clamp(var(--font-size-5xl), 3vw + 2.25rem, var(--font-size-6xl));
}

/* The 5xl/6xl clamp above was tuned for desktop, where this sits in
   its own wide column next to the logo/contact/button stack and reads
   as a balanced hero-style pairing (confirmed in a live screenshot).
   Below 768px .footer-hero-content stacks to one column and this text
   wraps to 4 lines at the same 48px used by full section headlines —
   next to the footer's much smaller nav/contact/copyright text it
   reads as oversized and out of proportion for what's a secondary
   footer element, not a primary section headline. Scaled back down
   for mobile only; still clearly the largest, boldest text in the
   footer, just no longer competing with actual section headings above
   it in the page. 2.75rem/44px rather than a token (--font-size-4xl
   is 36px, --font-size-5xl jumps to 48px, no step in between) — a
   touch bigger/more confident than the initial 36px pass while
   staying clearly under the 48px section headlines. */
@media (max-width: 767px) {
  .footer-hero-heading {
    font-size: 2.75rem;
  }
}

.footer-hero-sub {
  margin: 0;
  margin-top: var(--spacing-4);
  color: color-mix(in srgb, var(--color-on-surface-variant-color) 75%, transparent);
  font-size: var(--font-size-2xl);
}

.footer-social a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-full);
  border: 1px solid color-mix(in srgb, var(--color-on-background-color) 30%, transparent);
  color: var(--color-on-background-color);
  transition: background-color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}

.footer-social a:hover,
.footer-social a:focus-visible {
  background-color: var(--color-primary-color);
  border-color: var(--color-primary-color);
  color: var(--color-on-primary-color);
}

.footer-social svg {
  width: 1.125rem;
  height: 1.125rem;
}

/* Copyright/nav row, facing nav on the opposite side of the same
   line. The hairline top border gives this row its own visual
   grounding now that there's no wordmark above it acting as an
   implicit divider; margin-top separates it from the hero row's
   content, padding-top from the border itself. */
.footer-bottom-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-4);
  margin-top: var(--spacing-12);
  padding-top: var(--spacing-8);
  border-top: 1px solid color-mix(in srgb, var(--color-on-background-color) 12%, transparent);
}

@media (min-width: 640px) {
  .footer-bottom-row {
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
  }
}

/* Below 640px .footer-bottom-row stacks to a plain column, and DOM
   order (copyright, then nav) put the legal boilerplate above the
   actual navigation — backwards from the usual footer convention of
   primary links first, fine print last. `order` (not a markup swap)
   so desktop's row layout above is untouched: at >=640px .footer-meta
   still renders first in the DOM and lands on the left / .footer-nav
   on the right via justify-content: space-between, the standard
   copyright-left/nav-right split, which already read correctly there. */
@media (max-width: 639px) {
  .footer-nav {
    order: -1;
  }
}

.footer-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-5);
  font-size: var(--font-size-lg);
}

/* Below 640px, keep the (now 4-link) nav on one row rather than
   stacking: --font-size-base (down from --font-size-lg) is the
   largest size that still holds one row at a 320px viewport, and
   justify-content distributes the 4 links evenly across the full
   width instead of clustering them at a fixed gap — verified
   against real renders at 320px and 375px so nothing wraps. */
@media (max-width: 639px) {
  .footer-nav {
    width: 100%;
    flex-wrap: nowrap;
    justify-content: space-between;
    gap: var(--spacing-2);
    font-size: var(--font-size-base);
  }
}

.footer-nav a:hover {
  color: var(--color-primary-color);
}

.footer-meta {
  font-size: var(--font-size-base);
  color: color-mix(in srgb, var(--color-on-background-color) 70%, transparent);
}

/* ---------------------------------------------------------------------
   Scroll reveal
------------------------------------------------------------------------ */
.reveal {
  opacity: 0;
  transform: translateY(1.5rem);
  transition: opacity var(--motion-slow) var(--ease-standard),
    transform var(--motion-slow) var(--ease-standard);
}

.reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* Who We Help's intro block (eyebrow/headline/subtitle) only — slides
   in from the left instead of up, via the same .reveal/is-visible
   toggle used everywhere else (trigger point is separate — see
   .reveal-delayed in main.js). Scoped to #who-we-help so the 5
   numbered cards (a separate .reveal target, .audience-grid) and every
   other section's .section-header are unaffected. Distance bumped
   48px -> 80px and duration 400ms (--motion-slow) -> 600ms: the
   animation wasn't reading as visible (see chat), so both are pushed
   past the site's normal reveal values on purpose, not left at the
   originally-requested -40/-60px range. 600ms doesn't match an
   existing --motion-* token (fastest/base/slow top out at 400ms) — a
   literal value here rather than stretching an existing token to a
   duration it isn't otherwise used for. */
#who-we-help .section-header.reveal {
  transform: translateX(-5rem);
  transition-duration: 600ms;
}

/* .reveal.is-visible's own transform: none (above) does NOT win here
   despite .is-visible being added after .reveal on the same element —
   #who-we-help .section-header.reveal (an ID selector) outranks
   .reveal.is-visible (two classes, no ID) regardless of source order,
   so without this the element was stuck at translateX(-5rem) forever
   once revealed: opacity resolved to 1, but the -80px offset never
   cleared, clipping the first ~80px of the eyebrow/heading/lede off
   the left edge — severe on mobile, where 80px is a large fraction of
   the viewport (this is what the previous distance/duration bump
   above was actually chasing, without fixing the real cause). Needs
   to match/exceed that selector's specificity, hence repeating the ID
   + all three classes rather than just chaining .is-visible. */
#who-we-help .section-header.reveal.is-visible {
  transform: none;
}

/* ---------------------------------------------------------------------
   Cursor-follower dot
   Position is updated every frame in main.js via requestAnimationFrame
   (a JS lerp, not a CSS transition) so it can trail the real cursor
   with visible lag rather than snapping. A single element (previously
   split into an outer positioned element + an inner element carrying
   the visuals, from when the inner also had its own hover-scale
   transform) — collapsed back into one because mix-blend-mode only
   works here on the actual position: fixed + z-index element itself:
   Chromium composites a fixed/z-indexed element on its own layer, and
   a blend-mode on a *child* of that layer doesn't correctly composite
   against the page behind it (renders as flat background-color
   instead, confirmed empirically) — verified fixed via a minimal
   isolated test page before landing this, not assumed from spec
   reading alone, since the two disagree here.
------------------------------------------------------------------------ */
.cursor-dot {
  position: fixed;
  top: 0;
  left: 0;
  width: 1.25rem;
  height: 1.25rem;
  margin: -0.625rem 0 0 -0.625rem;
  border-radius: 50%;
  pointer-events: none;
  z-index: var(--z-cursor-dot);
  opacity: 0;
  /* white + difference (not a themed color) so this inverts against
     whatever it's over — reads dark on light sections, light on dark
     ones, automatically, with no per-section overrides needed.
     Requires an unbroken view straight through to the page's actual
     content behind it: appended directly to <body> in main.js (not
     nested in any clipped/scrolling wrapper), and neither <html> nor
     <body> set overflow: hidden, so nothing here clips it. */
  background-color: #fff;
  mix-blend-mode: difference;
}

.cursor-dot.is-active {
  opacity: 1;
}

/* No persistent pointer (touch/stylus-only devices) — never show the
   dot, so it can't get stuck on-screen after the last tap. */
@media (hover: none), (pointer: coarse) {
  .cursor-dot {
    display: none;
  }
}

/* ---------------------------------------------------------------------
   Reduced motion
------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  .reveal {
    opacity: 1;
    transform: none;
  }

  /* No bounce/positional motion — the existing background-color and
     box-shadow hover shift still applies (those aren't disabled here),
     plus a subtle brightness cue as the explicit non-motion signal. */
  .btn-primary:hover {
    transform: none;
    filter: brightness(1.05);
  }

  /* Who We Help cards aren't .reveal-classed themselves (only the grid
     is, for the existing observer to toggle) — the blanket rule above
     already collapses their transition *durations*, but not the
     per-card/per-element stagger *delays*, which would otherwise still
     read as a slow-motion sequence of instant pops rather than
     everything appearing at once. Forced to final state + zero delay
     here; !important because the entrance rules this overrides are
     more specific (.audience-grid.is-visible ...). */
  .audience-card,
  .audience-card-number,
  .audience-card h3,
  .audience-card p {
    opacity: 1;
    transform: none;
    transition-delay: 0ms !important;
  }

  .audience-card-rule {
    transform: scaleX(1);
    transition-delay: 0ms !important;
  }

  /* Hover widen still happens, just instantly (duration is already
     collapsed above) rather than animating the draw. */
  .audience-card:hover .audience-card-rule {
    transform: scaleX(4.5);
  }

  /* Same treatment for the number tilt/scale — still lands on hover,
     just instantly rather than animating. */
  .audience-card:hover .audience-card-number {
    transform: rotate(5deg) scale(1.06);
  }

  /* Services entrance sequence — same reasoning as the Who We Help
     cards above: the blanket rule already collapses durations, but
     not .services-tabs's own delay-free opacity transition needs no
     extra help here since it has none. The card stagger is inline-
     style-driven (see main.js animateServiceCards), which checks
     prefersReducedMotion directly and skips the delay/stagger
     entirely in that branch — no CSS backstop needed for it. */
  .services-tabs.is-visible {
    transition-delay: 0ms !important;
  }

  /* Letter-ripple heading entrance — same reasoning as everything
     else in this block: the blanket rule above already collapses
     each letter's own transition duration, but not the per-letter
     stagger delay, which would otherwise still read as a slow
     letter-by-letter crawl instead of the heading simply being
     present. !important because .reveal.is-visible .letter-ripple is
     more specific than this. */
  .letter-ripple {
    transition-delay: 0ms !important;
  }
}
