/* ==========================================================================
   Carolina C. Cordeiro — carolinacordeiro.com
   Single stylesheet. No build step, no framework, no preprocessor.

   Order: tokens → fonts → reset/base → layout → components → utilities → print
   Every value traces to a token in Brand & Visual Standards/Visual Standards.md.
   Need a new value? Add it to that note FIRST, then use it here.
   ========================================================================== */


/* ==========================================================================
   1. TOKENS   — copied verbatim from Visual Standards §11
   ========================================================================== */

:root {
  /* ---- Colour ---- */
  --color-ink:            #000000;
  --color-paper:          #FFFFFF;
  --color-mist:           #E7E7E7;
  --color-muted:          #4A4A4A;
  --color-muted-inverse:  #D0D0D0;
  --color-rule:           rgba(0, 0, 0, 0.12);
  --color-rule-strong:    rgba(0, 0, 0, 0.34);
  --color-rule-inverse:   rgba(255, 255, 255, 0.18);

  /* FIXED tokens — deliberately do NOT change under .on-dark. For components
     that must read as a light "spotlit" surface regardless of what section
     they sit in (currently: endorsement cards on the dark "What people say"
     section) — the card stays a light-grey island against black rather than
     inheriting the section's own inversion. Same literal values as the
     regular tokens, so nothing changes for existing light-section usage;
     only the behaviour inside .on-dark differs. See Decisions, 2026-09-16. */
  --color-mist-fixed:  #E7E7E7;
  --color-paper-fixed: #FFFFFF;
  --color-ink-fixed:   #000000;
  --color-muted-fixed: #4A4A4A;
  --color-rule-fixed:  rgba(0, 0, 0, 0.34);

  /* Near-opaque white wash over a card's background photo — the photo reads
     as texture, not as a competing image. Same value five times over would
     otherwise be five literal colours; one token, one place to tune it. */
  --card-photo-wash: rgba(255, 255, 255, 0.7);
  /* Go Clean's source photo is busier than the others at this wash level —
     lighter just for this one card. */
  --card-photo-wash-goclean: rgba(255, 255, 255, 0.85);

  /* Darkens the hero photograph so white text clears 4.5:1.
     Verify against the LIGHTEST region of the image, not the average. */
  --scrim: linear-gradient(to bottom, rgba(0,0,0,0.45), rgba(0,0,0,0.68));

  /* ---- Type ---- */
  /* Agrandir pending licence — Manrope is the interim family in use. */
  --font-display: 'Manrope', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --font-lead:    'Playfair Display', Georgia, 'Times New Roman', serif;

  --text-hero:       clamp(2.25rem, 5vw, 3.25rem);        /* 36 → 52 */
  --text-h1:         clamp(2.125rem, 4.5vw, 3rem);        /* 34 → 48 */
  --text-wordmark:   clamp(1.125rem, 3.5vw, 2rem);        /* 18 → 32 */
  --text-h3:         clamp(1.375rem, 2vw, 1.5rem);        /* 22 → 24 */
  --text-lead:       clamp(1.0625rem, 1.5vw, 1.1875rem);  /* 17 → 19 */
  --text-card-title: clamp(1.125rem, 1.5vw, 1.25rem);     /* 18 → 20 */
  --text-body:       1rem;                                /* 16 */
  --text-small:      0.9375rem;                           /* 15 */
  --text-stat:       clamp(2.5rem, 4vw, 3rem);            /* 40 → 48 */

  --leading-tight: 1.15;
  --leading-snug:  1.3;
  /* 1.5, not 1.45 — WCAG 2.2 SC 1.4.12 requires line-height >= 1.5x for
     text at or below 18px, and .lead's clamp bottoms out at 17px, as do
     the endorsement quotes that share this token. Found by a11y-audit;
     the visual difference at this size is negligible. */
  --leading-lead:  1.5;
  --leading-body:  1.6;

  /* ---- Space ---- */
  --space-1: 0.25rem;  --space-2: 0.5rem;   --space-3: 0.75rem;
  --space-4: 1rem;     --space-5: 1.25rem;  --space-6: 1.5rem;
  --space-8: 2rem;     --space-10: 2.5rem;  --space-16: 4rem;
  --space-20: 5rem;    --space-25: 6.25rem;

  --gutter:           var(--space-4);
  --grid-gap:         var(--space-8);
  --section-pad:      var(--space-10);
  --section-pad-hero: var(--space-25);
  --container-max:    1140px;
  --measure:          68ch;

  /* ---- Header ---- */
  --header-height: 5rem;

  /* ---- Forms ---- */
  --field-border:       1.5px solid var(--color-rule-strong);
  --field-border-hover: 1.5px solid var(--color-ink);
  --field-border-error: 2.5px solid var(--color-ink);
  --field-bg:           var(--color-paper);
  --field-pad:          var(--space-3) var(--space-4);
  --field-radius:       var(--radius-sm);
  --field-gap:          var(--space-5);

  /* ---- Shape ---- */
  --radius:      10px;
  --radius-sm:   4px;
  --radius-pill: 999px;
  --border:      1px solid var(--color-ink);

  /* ---- Focus ---- */
  /* Dual-tone ring: readable on white, on black, and over the hero photograph.
     A single-colour ring disappears on one of the three. */
  --focus-inner: var(--color-paper);
  --focus-outer: var(--color-ink);

  /* ---- Motion ---- */
  --ease:     cubic-bezier(0.22, 1, 0.36, 1);
  --duration: 400ms;
}

@media (min-width: 48rem) {                 /* 768px */
  :root { --gutter: var(--space-6); }
}

@media (min-width: 64rem) {                 /* 1024px */
  :root { --section-pad: var(--space-20); }
}

@media (prefers-reduced-motion: reduce) {
  :root { --duration: 0.01ms; }
}

/* Dark sections invert the token set. Components never name a colour directly —
   they read these tokens, so .on-dark works with no per-component overrides. */
.on-dark {
  --color-ink:         #FFFFFF;
  --color-paper:       #000000;
  --color-mist:        #1A1A1A;
  --color-muted:       #D0D0D0;
  --color-rule:        rgba(255, 255, 255, 0.18);
  --color-rule-strong: rgba(255, 255, 255, 0.38);
  --focus-inner:       #000000;
  --focus-outer:       #FFFFFF;
}


/* ==========================================================================
   2. FONTS

   INTERIM SUBSTITUTE IN USE — 2026-09-16. See Decisions/_index.md and
   Visual Standards §3.

   Agrandir's webfont licence is still unconfirmed, so it is not self-hosted.
   --font-display currently resolves to HANKEN GROTESK, an open-licence (OFL)
   geometric sans chosen for its resemblance to Agrandir's character. It is a
   placeholder: when a licence is confirmed, swap only the @font-face blocks
   below for Agrandir's real files — the --font-display token, and every rule
   that reads it, does not change.

   Playfair Display is genuinely final — OFL, no substitution needed.

   Both are self-hosted WOFF2 subsets (latin + latin-ext), fetched once from
   Google Fonts and served from Site/assets/fonts/. NOT linked from Google's
   CDN at runtime — that would cost a third-party connection and break the
   CSP. Masters are in Brand & Visual Standards/Fonts/.
   ========================================================================== */

/* Manrope ships as a single variable-font file from Google; both the
   400 and 700 @font-face blocks below deliberately point at the SAME file —
   the browser reads the requested weight from the font's own variation axis.
   Two differently-weighted downloads would just be the same bytes twice. */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../assets/fonts/manrope.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../assets/fonts/manrope-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
    U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../assets/fonts/manrope.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../assets/fonts/manrope-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
    U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../assets/fonts/playfair-display-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../assets/fonts/playfair-display-400-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
    U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ==========================================================================
   3. RESET / BASE
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* WCAG 2.4.11 Focus Not Obscured. The header is sticky, so an anchor jump
     would otherwise land the target underneath it. */
  scroll-padding-top: calc(var(--header-height) + var(--space-4));
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--color-paper);
  color: var(--color-ink);
  font-family: var(--font-display);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  margin: 0 0 var(--space-4);
  font-weight: 700;
  line-height: var(--leading-tight);
  text-wrap: balance;
}
h1 { font-size: var(--text-h1); }
h2 { font-size: var(--text-h1); }
h3 { font-size: var(--text-h3); font-weight: 400; line-height: var(--leading-snug); }
h4 { font-size: var(--text-card-title); line-height: var(--leading-snug); }

p { margin: 0 0 var(--space-4); }
p:last-child { margin-bottom: 0; }

ul, ol { margin: 0 0 var(--space-4); padding-left: var(--space-5); }
li { margin-bottom: var(--space-2); }

a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.15em;
  text-decoration-thickness: 1px;
}
a:hover { text-decoration-thickness: 2px; }

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

strong, b { font-weight: 700; }

/* Focus — dual-tone ring, see token block. :focus-visible only, so mouse
   users don't get rings on click. */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--focus-inner);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--focus-outer);
  border-radius: var(--radius-sm);
}

/* Skip link — first focusable element on every page. */
.skip-link {
  position: absolute;
  left: var(--space-4);
  top: calc(-1 * var(--space-16));
  z-index: 100;
  padding: var(--space-3) var(--space-4);
  background: var(--color-ink);
  color: var(--color-paper);
  text-decoration: none;
  border-radius: var(--radius-sm);
  transition: top var(--duration) var(--ease);
}
.skip-link:focus-visible { top: var(--space-4); }

::selection { background: var(--color-ink); color: var(--color-paper); }


/* ==========================================================================
   4. LAYOUT
   ========================================================================== */

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

.section {
  padding-block: var(--section-pad);
  background: var(--color-paper);
  color: var(--color-ink);
}

/* One dark anchor per page — NOT strict alternation. See Decisions 2026-09-16.
   The inversion is done ENTIRELY by .on-dark swapping the token values. This
   class must not restate background/color: .on-dark has already flipped
   --color-paper to black, and the .section rule above reads it. Setting
   background:var(--color-ink) here produced a white "dark" section. */
.section--dark { /* semantic marker only — .on-dark carries the colours */ }

/* Zeroes only the TOP padding — merges with the section directly above (its
   own intro text) so the two read as one continuous block. Bottom padding is
   kept so there's still breathing room before whatever comes next, which is
   often a different background colour now that sections alternate — a flush
   section with zero padding on BOTH sides left its content touching the
   colour change with no margin at all. */
.section--flush { padding-top: 0; }

/* Body copy stays at a comfortable measure regardless of container width.
   NOTE: .prose must be a CHILD of .container, never combined on one element —
   combined, .prose's max-width wins and .container's margin-inline:auto centres
   the measure, pushing copy out of alignment with the headings. */
.prose { max-width: var(--measure); }
.container.prose { max-width: var(--container-max); }
.container.prose > * { max-width: var(--measure); }
.prose > * + * { margin-top: var(--space-4); }

/* Two-column: stacks below 768px. */
.split {
  display: grid;
  gap: var(--grid-gap);
  align-items: start;
}
@media (min-width: 48rem) {
  .split { grid-template-columns: 1fr 1fr; }
  .split--media-first > :first-child { order: -1; }
  .split--record { grid-template-columns: 3fr 2fr; align-items: start; }
}



/* ==========================================================================
   5. COMPONENTS
   ========================================================================== */

/* ---- 5.1 Header ------------------------------------------------------- */
/* ⚠ Never put backdrop-filter / filter / transform on .site-header. Any of
   them makes it the containing block for its position:fixed descendants, and
   the mobile panel stops being fixed to the viewport. This broke the nav on a
   sibling site. Put such effects on a ::before instead.
   See Visual Standards §7. */

.site-header {
  position: sticky;
  top: env(safe-area-inset-top, 0px);
  z-index: 50;
  background: var(--color-paper);
  border-bottom: 1px solid var(--color-rule);
}

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

.wordmark {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-wordmark);
  line-height: 1.1;
  text-decoration: none;
  /* Deliberately NOT nowrap. At 320px the mark + wordmark + menu button is
     arithmetically tight; letting the name wrap is better than overflowing.
     min-width:0 lets it shrink inside the flex header. */
  min-width: 0;
}
.wordmark__mark { width: 2.25rem; height: auto; flex: none; }

@media (max-width: 25rem) {            /* 400px */
  .wordmark__mark { width: 1.75rem; }
  .site-header__inner { gap: var(--space-2); }
}

/* ---- 5.2 Navigation --------------------------------------------------- */

.nav__list {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
}
.nav__list li { margin: 0; }

.nav__link {
  display: inline-block;
  padding: var(--space-2) 0;
  font-size: var(--text-small);
  text-decoration: none;
  min-height: 44px;
  line-height: 28px;
}
.nav__link:hover { text-decoration: underline; text-underline-offset: 0.3em; }
.nav__link[aria-current="page"] {
  text-decoration: underline;
  text-underline-offset: 0.3em;
  text-decoration-thickness: 2px;
}

.nav__toggle { display: none; }

/* Mobile: panel + toggle. Content is reachable without JS — see the
   :not([hidden]) fallback below. */
@media (max-width: 47.99rem) {
  .nav__toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    background: none;
    border: 0;
    color: inherit;
    cursor: pointer;
  }
  .nav__panel {
    position: fixed;
    inset: var(--header-height) 0 auto 0;
    padding: var(--space-4) var(--gutter) var(--space-8);
    background: var(--color-paper);
    border-bottom: 1px solid var(--color-rule);
  }
  .nav__panel[hidden] { display: none; }
  .nav__list { flex-direction: column; align-items: flex-start; gap: var(--space-2); }
  .nav__link { font-size: var(--text-body); }
}

/* ---- 5.3 Hero --------------------------------------------------------- */

.hero {
  position: relative;
  display: grid;
  align-items: end;
  padding-block: var(--section-pad-hero);
  background: var(--color-ink);
  color: var(--color-paper);
  overflow: hidden;
}
.hero__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Scrim guarantees contrast rather than hoping for it. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--scrim);
}
.hero__inner { position: relative; z-index: 1; }
.hero__title { font-size: var(--text-hero); font-weight: 400; margin-bottom: var(--space-3); }

/* Entrance on load — deliberately NOT gated behind .js or an observer. This is
   the first thing anyone sees; a static hero read as "no animation at all" in
   review. Pure CSS keyframes fire immediately on paint regardless of JS, and
   the base rule above already sets the resting opacity:1, so a browser with
   no animation support (or the property stripped) simply shows the end
   state — never a stuck-invisible one. */
@media (prefers-reduced-motion: no-preference) {
  .hero__title { animation: hero-in 850ms var(--ease) both; }
  .hero .lead  { animation: hero-in 850ms var(--ease) 160ms both; }
  @keyframes hero-in {
    from { opacity: 0; transform: translateY(24px); }
    to   { opacity: 1; transform: none; }
  }
}

/* ---- 5.4 Lead line ---------------------------------------------------- */
/* Playfair appears here and NOWHERE else. One line per section, under the
   heading. Never a paragraph, never a heading. Visual Standards §3. */

.lead {
  font-family: var(--font-lead);
  font-size: var(--text-lead);
  line-height: var(--leading-lead);
  font-weight: 400;
  margin: 0 0 var(--space-6);
  max-width: var(--measure);
}

.eyebrow {
  font-size: var(--text-small);
  color: var(--color-muted);
  margin: 0 0 var(--space-2);
}

/* ---- 5.5 Stats -------------------------------------------------------- */

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--grid-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}
.stats li { margin: 0; }

.stat {
  padding: var(--space-5);
  background: var(--color-mist);
  color: var(--color-ink);
  border-radius: var(--radius);
}

/* Plain variant — no card fill. On a dark section a grey box read as a
   separate, slightly disconnected element sitting ON the black; the original
   site set its numbers directly against the background instead. Reuses the
   hairline-top language already established by .stage (Approach page) so the
   design system has one "small metric" vocabulary rather than two. */
.stats--plain .stat {
  padding: var(--space-4) 0 0;
  background: none;
  border-radius: 0;
  border-top: 2px solid var(--color-ink);
}
.stat__figure {
  display: block;
  font-size: var(--text-stat);
  font-weight: 700;
  line-height: 1.1;
  /* Stops six animating counters jittering against each other. */
  font-variant-numeric: tabular-nums;
}
/* The label is not decoration — an unlabelled figure undermines its neighbours.
   See Requirements/Portfolio.md. */
.stat__label {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-small);
  /* 1.5, not --leading-snug: this is 15px body-grade text and our own standard
     requires 1.5–1.6 below 18px. Caught by a11y-audit. */
  line-height: 1.5;
}

/* ---- 5.6 Cards -------------------------------------------------------- */

.card {
  padding: var(--space-5);
  /* Fixed, not the regular tokens — same reasoning as .endorsement: a card
     stays a light surface regardless of the section it sits in (the
     Ventures cards now live on a dark section). Same literal values as
     before, so light-section usage (Approach's capability cards) is
     unaffected; only the on-dark behaviour differs. */
  background: var(--color-paper-fixed);
  color: var(--color-ink-fixed);
  border: 1px solid var(--color-ink-fixed);
  border-radius: var(--radius);
}
.card__title { font-size: var(--text-card-title); font-weight: 700; margin-bottom: var(--space-2); }
.card__meta { font-size: var(--text-small); color: var(--color-muted-fixed); margin: 0; }

/* Ventures cards — each carries a faint, desaturated photo from that
   company's own site as a texture, not a dominant image. A near-white wash
   over the photo keeps the card reading as a light surface and keeps text
   contrast effectively unchanged; the photo is felt more than seen.
   Sourced from each company's live site — see Brand & Visual Standards/
   Images/Ventures/ for the original screenshots and Decisions/_index.md
   for the reasoning (requested twice before being built). */
.card--photo {
  background-blend-mode: normal;
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
}
/* The "Visit X" link's hit area was just its text line-height (22px) —
   under WCAG 2.2 SC 2.5.8's 24px minimum. Padding it out, not changing the
   text size. Found by a11y-audit. */
.card--photo p a {
  display: inline-block;
  padding-block: var(--space-1);
}
.card--mochi    { background-image: linear-gradient(var(--card-photo-wash), var(--card-photo-wash)), url('../assets/img/card-bg-mochi.webp'); }
.card--bc       { background-image: linear-gradient(var(--card-photo-wash), var(--card-photo-wash)), url('../assets/img/card-bg-bc.webp'); }
.card--ecora    { background-image: linear-gradient(var(--card-photo-wash), var(--card-photo-wash)), url('../assets/img/card-bg-ecora.webp'); }
.card--goclean  { background-image: linear-gradient(var(--card-photo-wash-goclean), var(--card-photo-wash-goclean)), url('../assets/img/card-bg-goclean.webp'); }
.card--bullseye { background-image: linear-gradient(var(--card-photo-wash), var(--card-photo-wash)), url('../assets/img/card-bg-bullseye.webp'); }

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--grid-gap);
}

/* ---- 5.7 Endorsements ------------------------------------------------- */
/* The dedicated Endorsements page was removed (2026-09-16) once all ten
   quotes moved onto Home's carousel — see Decisions. .endorsements is now
   only the carousel's no-JS / pre-init fallback grid (below). */

.endorsements {
  display: grid;
  gap: var(--grid-gap);
  /* No-JS / pre-balance fallback. main.js overrides with an exact divisor
     of the real item count once it runs — see "Balanced grids", section 8. */
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
}


.endorsement {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: 0;
  padding: var(--space-6);
  /* Fixed, not the regular tokens — this card stays a light island even
     inside a dark section (see the fixed-token comment in section 1). */
  background: var(--color-mist-fixed);
  color: var(--color-ink-fixed);
  border-radius: var(--radius);
}
/* Set in Playfair Display, not the sans used for the rest of the card — the
   site's one "literary" register, otherwise reserved for lead lines. That
   alone was enough to stop a quote reading as plain body copy in a grey box;
   a second, larger decorative mark on top of literal quotes would be two
   treatments doing the same job. Real curly quotes via the `quotes`
   property, not straight ones — matches the sitewide copy-editing rule. */
.endorsement__quote {
  margin: 0;
  font-family: var(--font-lead);
  font-size: 1.0625rem;
  line-height: var(--leading-lead);
}
.endorsement__quote p {
  quotes: "\201C" "\201D";
}
.endorsement__quote p::before { content: open-quote; }
.endorsement__quote p::after  { content: close-quote; }
.endorsement__quote p:last-child { margin-bottom: 0; }
.endorsement figcaption { margin-top: auto; }
.endorsement__name { display: block; font-family: var(--font-display); font-weight: 700; }
.endorsement__role { display: block; font-size: var(--text-small); color: var(--color-muted-fixed); }

/* ---- 5.7b Endorsements carousel (Home only) ---------------------------- */
/* No-JS / pre-init fallback: .carousel__track carries the .endorsements
   grid class, and .carousel__slide is `display: contents` — so each
   .endorsement card sits directly in that grid, same as the static page,
   and every quote is visible with nothing clipped or auto-moving. main.js
   adds .carousel--active once it has wired up real prev/next/pause
   controls, and only then turns the track into a one-at-a-time strip.
   Client asked for "a slow cyclic carousel, check original site" — the
   original runs a continuous drifting marquee with no pause control and
   no arrows (Splide + auto-scroll), which fails WCAG 2.2 SC 2.2.2 (Pause,
   Stop, Hide: auto-moving content over 5s needs a visible pause) outright.
   This keeps the "all ten, cycling, slow" brief without repeating that —
   discrete slides that hold for 7s then snap to the next, a real pause
   button, and pausing on hover/focus. See Decisions. */

.carousel__slide { display: contents; }

.carousel--active .carousel__viewport {
  overflow: hidden;
}
.carousel--active .carousel__track {
  display: flex;
  flex-wrap: nowrap;
  /* Overrides the gap inherited from .endorsements (kept on this element
     for the no-JS grid fallback). Left in, every translateX(-N * 100%)
     step lands one gap short of the real slide position — the error
     compounds with N, which is exactly what showed up as neighbouring
     cards bleeding in and getting worse deeper into the set. */
  gap: 0;
  transition: transform var(--duration) var(--ease);
}
.carousel--active .carousel__slide {
  display: block;
  flex: 0 0 100%;
  min-width: 0;
}
.carousel--active .carousel__slide .endorsement {
  max-width: 38rem;
  min-height: 16rem;
  margin-inline: auto;
}

.carousel__controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  margin-top: var(--space-5);
}
.carousel__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0 var(--space-3);
  background: none;
  border: 1px solid var(--color-rule-strong);
  border-radius: var(--radius-pill);
  color: var(--color-ink);
  font: inherit;
  font-size: var(--text-small);
  font-weight: 700;
  cursor: pointer;
}
.carousel__btn:hover { border-color: var(--color-ink); }
.carousel__status {
  margin: 0;
  font-size: var(--text-small);
  color: var(--color-muted);
  font-variant-numeric: tabular-nums;
}

/* ---- 5.8 Buttons ------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: var(--space-3) var(--space-6);
  background: var(--color-ink);
  color: var(--color-paper);
  border: 2px solid var(--color-ink);
  border-radius: var(--radius-pill);
  font: inherit;
  font-size: var(--text-small);
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
}
.btn:hover { background: transparent; color: var(--color-ink); text-decoration: none; }

.btn--ghost { background: transparent; color: var(--color-ink); }
.btn--ghost:hover { background: var(--color-ink); color: var(--color-paper); }

/* ---- 5.9 Forms -------------------------------------------------------- */
/* Errors are MONOCHROME by design — weight, border, icon and words, never
   colour. No red is introduced anywhere on this site. Visual Standards §7. */

.form { display: flex; flex-direction: column; gap: var(--field-gap); max-width: 34rem; }

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

.field__label { font-weight: 700; font-size: var(--text-small); }

.field__hint { font-size: var(--text-small); color: var(--color-muted); margin: 0; }

.field__input,
.field__textarea {
  width: 100%;
  min-height: 44px;
  padding: var(--field-pad);
  background: var(--field-bg);
  color: var(--color-ink);
  border: var(--field-border);
  border-radius: var(--field-radius);
  font: inherit;
}
.field__input:hover,
.field__textarea:hover { border: var(--field-border-hover); }

.field__textarea { resize: vertical; min-height: 9rem; }

.field__input[aria-invalid="true"],
.field__textarea[aria-invalid="true"] { border: var(--field-border-error); }

.field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--text-small);
  font-weight: 700;
}
.field__error svg { flex: none; width: 1.1em; height: 1.1em; margin-top: 0.2em; }

/* Error summary above the form on submit failure. Receives focus. */
.form__summary {
  padding: var(--space-4);
  border: var(--field-border-error);
  border-radius: var(--radius-sm);
}
.form__summary h2 { font-size: var(--text-card-title); margin-bottom: var(--space-2); }

/* Netlify honeypot — hidden from people, visible to bots. No CAPTCHA.
   Off-screen alone is not enough: the markup also carries aria-hidden and
   tabindex="-1" so screen reader and keyboard users never meet it. */
.honeypot { position: absolute; left: -9999px; }

/* Contact methods. A list, not bare paragraph links — these are navigation
   targets, not text in a sentence, so WCAG 2.5.8's inline exception does not
   apply and they need 24px. */
.contact-list { list-style: none; padding: 0; margin: 0 0 var(--space-4); }
.contact-list li { margin-bottom: var(--space-2); }
.contact-list a { display: inline-flex; align-items: center; min-height: 24px; }


/* ---- 5.11 Portrait & photo strip -------------------------------------- */

.portrait { border-radius: var(--radius); width: 100%; }

.photo-strip {
  display: grid;
  /* No-JS / pre-balance fallback only — main.js overrides this with an exact
     column count once it runs. See "Balanced grids" in section 8. */
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-4);
}
.photo-strip img {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: var(--radius);
}
/* When JS balances this grid down to a single column (only possible outcome
   for a prime item count below its natural per-row width — see main.js),
   don't let one photo stretch edge to edge. */
.photo-strip.is-single-col img { max-width: 26rem; margin-inline: auto; }

/* ---- 5.12 CSTS stages -------------------------------------------------- */
/* Numbered because the stages ARE a sequence — the letter carries the acronym,
   which is the information. Not decoration. */

.stages {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--grid-gap);
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: stage;
}
.stage {
  margin: 0;
  padding-top: var(--space-4);
  border-top: 2px solid var(--color-ink);
}
.stage__letter {
  display: block;
  font-family: var(--font-lead);
  font-size: var(--text-stat);
  line-height: 1;
  margin-bottom: var(--space-2);
}
.stage__name { font-size: var(--text-card-title); margin-bottom: var(--space-2); }
.stage p { font-size: var(--text-small); margin: 0; }

/* ---- 5.13 Outcomes ----------------------------------------------------- */

.outcomes {
  display: grid;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
  max-width: 34rem;
}
.outcome {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin: 0;
  padding: var(--space-4);
  /* Fixed, not the regular tokens — same reasoning as .card and
     .endorsement (section 1). This tile is meant to be a light surface
     with a black icon regardless of the section around it; .color-mist
     itself is NOT fixed (it goes near-black under .on-dark), so a tile
     that read the regular token went near-invisible — near-black icon on
     a near-black tile on a black section. Found on the Approach page,
     where this section is already dark. */
  background: var(--color-mist-fixed);
  color: var(--color-ink-fixed);
  border-radius: var(--radius);
}
/* Icons are black SVGs on the always-light tile above — safe regardless
   of the surrounding section's background. */
.outcome img { flex: none; width: 2rem; height: 2rem; }
.outcome span { font-size: var(--text-card-title); }

/* ---- 5.14 Capability cards -------------------------------------------- */

.card--cap { text-align: center; }
.card--cap img {
  width: 2.5rem;
  height: 2.5rem;
  margin: 0 auto var(--space-3);
}
/* Fixed, not the regular token — same reason as .outcome above. This text
   sits on the always-light .card surface, so it needs the light-context
   muted grey regardless of the section's own background. Reading the
   regular --color-muted meant it silently picked up the ON-DARK value
   (#D0D0D0, meant for grey text on black) once this section went dark —
   showing as pale, low-contrast grey on the card's white background. */
.card--cap p { font-size: var(--text-small); color: var(--color-muted-fixed); margin: 0; }

/* ---- 5.10 Footer ------------------------------------------------------ */

.site-footer {
  padding-block: var(--space-8);
  border-top: 1px solid var(--color-rule);
  font-size: var(--text-small);
}
.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4) var(--space-6);
  align-items: center;
  justify-content: space-between;
}
.site-footer__nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.site-footer__nav li { margin: 0; }
/* WCAG 2.5.8 Target Size (Minimum) — 24x24 CSS px. These sit in a navigation
   list, so the "inline text" exception does not apply. Caught by a11y-audit. */
.site-footer__nav a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}
.site-footer__copyright { margin: 0; color: var(--color-muted); }


/* ==========================================================================
   6. UTILITIES
   ========================================================================== */

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

.stack > * + * { margin-top: var(--space-4); }
/* Top margin on a block relative to a PRECEDING sibling — for a sub-section
   nested inside a larger one (e.g. "Boards" following the record's own
   split within the same dark section) that needs separation without being
   a whole new .section. */
.subsection { margin-top: var(--space-10); }
.stack-lg > * + * { margin-top: var(--space-8); }
.measure { max-width: var(--measure); }
/* The one current ul.measure (Approach's "We do this through") — sized up
   from body text to match the bump on .outcome span next to it, so the
   two sit at the same visual weight in the split. */
ul.measure li { font-size: var(--text-card-title); }
.text-muted { color: var(--color-muted); }
.no-wrap { white-space: nowrap; }

/* Scroll reveal. Default state is VISIBLE — JS may animate it in, but content
   is never parked at opacity 0 waiting on an observer. Visual Standards §8.

   Fades DIRECT CHILDREN, staggered, rather than the container as one block —
   a whole grid fading as a single flat unit read as barely-there in review.
   JS only ever toggles .is-visible on the container; the stagger is CSS-only. */
.reveal { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  /* Duration and distance benchmarked against the original GoHighLevel site's
     Animate.css reveal (1000ms, translate3d 100% of element height) — see
     Decisions/_index.md, 2026-09-16. Not copied outright: a full-height slide
     reads as busy against this site's restrained, monochrome character. This
     splits the difference — long enough to register as deliberate motion,
     far short of the original's sweeping travel distance. */
  .js .reveal > * { opacity: 0; transform: translateY(32px); }
  .js .reveal.is-visible > * {
    opacity: 1;
    transform: none;
    transition: opacity 700ms var(--ease), transform 700ms var(--ease);
  }
  .js .reveal.is-visible > *:nth-child(1) { transition-delay: 0ms; }
  .js .reveal.is-visible > *:nth-child(2) { transition-delay: 90ms; }
  .js .reveal.is-visible > *:nth-child(3) { transition-delay: 180ms; }
  .js .reveal.is-visible > *:nth-child(4) { transition-delay: 270ms; }
  .js .reveal.is-visible > *:nth-child(5) { transition-delay: 360ms; }
  .js .reveal.is-visible > *:nth-child(6) { transition-delay: 450ms; }
  .js .reveal.is-visible > *:nth-child(7) { transition-delay: 540ms; }
  .js .reveal.is-visible > *:nth-child(8) { transition-delay: 630ms; }
}


/* ==========================================================================
   7. PRINT
   Board appointments and advisory pitches get printed. Make it legible.
   ========================================================================== */

@media print {
  .site-header, .site-footer__nav, .skip-link, .nav__toggle, .btn { display: none; }
  body { background: #fff; color: #000; font-size: 11pt; }
  .section, .section--dark, .hero { background: #fff; color: #000; padding-block: 0; }
  .hero::after, .hero__media { display: none; }
  .endorsement, .stat { background: #fff; border: 1px solid #000; }
  .card, .endorsement, .stat { break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; }
  /* Print the destination of links, since they can't be clicked on paper. */
  main a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; }
}
