/* ==========================================================================
   EVAA — Excelsior Veterinary Associates of America
   v3 — "consulting site" direction. Ref: consultingo-wbs.webflow.io

   History, so nobody repeats it:
   v1  navy + gold + Didone + 01/02/03, hard edges.   → "a bank or lawyer site"
   v2  clay/copper warm palette, still hard edges.    → "don't like the copper"
   v3  Cream + navy + SOFT FORMS. ← here

   The insight from v2→v3: the reference's accent (#BC4C37) is within a hair of
   the copper that got rejected (#B0503A), and its ground (#FCF6E8) is within a
   hair of ours. So the difference was never colour — it was FORM. That site is
   arches, pills, big radii, soft shadows and real photography. Ours was
   hard-edged and austere.

   So: warmth comes from SHAPE, not from a warm accent. Colour stays navy —
   straight off the logo. The radii, the arches and the photography do the work
   that copper was doing.

   RISK, stated plainly: navy-led is exactly what read as a bank in v1. The soft
   forms are carrying all of it. If this still feels cold, the answer is bigger
   radii and better photography — NOT reintroducing gold.

   What we deliberately did NOT take from the reference: its stat cards, its
   "Trusted by 130+ companies", its client logo wall, its case studies. EVAA has
   one hospital, three people and zero closed acquisitions. Every one of those
   numbers would have to be invented.

   No gradients (house rule). The logo is the only glossy object on the page.
   ========================================================================== */

:root {
  /* --- Palette ----------------------------------------------------------
     Navy + cream, both straight off the logo. Gold is gone (2.42:1 — never
     text, never a fill). Clay/copper is gone (rejected v2).
     ---------------------------------------------------------------------- */
  --navy:        #1A2A56;  /* 13.0:1 on cream — the accent AND the text */
  --navy-deep:   #121D3B;  /* dark section fill */
  --navy-soft:   #4A5880;  /* 5.9:1 on cream — secondary text */
  --navy-wash:   #EDEFF5;  /* cool tint, used sparingly */
  --cream:       #FBF7EF;  /* page base — keeps the warmth */
  --cream-2:     #F5EFE2;  /* soft panels */
  --cream-3:     #FDFBF6;  /* raised cards */
  --rule:        #E7DFCF;
  --white:       #FFFFFF;
  --red:         #C0392B;  /* the logo's cross red. LIGHT-GROUND accent only:
                              5.09:1 on cream (AA), but only 3.05:1 on navy-deep,
                              which fails as text there (decorative 3:1 only). */
  --gold:        #DDB861;  /* the logo's swoosh gold. DARK-GROUND accent only —
                              the exact inverse of --red, and that pairing IS the
                              system: red belongs to the cream sections, gold to
                              the navy ones. Neither may cross over.
                                gold on navy-deep  8.78:1  ✅
                                gold on cream      1.77:1  ❌ never
                                navy text on gold  7.36:1  ✅ (white on gold = 1.89 ❌)
                              NB the old "gold is gone, 2.42:1" note in the header
                              was measured against CREAM only — true there, and the
                              reason gold never worked in v1, but it says nothing
                              about gold on navy.
                              ⚠️ v1 died as "a bank or lawyer site" on navy+gold, so
                              gold is rationed. Spreading it re-creates v1.
                              🔴 AMENDED 7/31: the rule was "EXACTLY ONCE"; it is now
                              "EXACTLY TWICE" on The Excelsior Network — the hero
                              motto rule and §4's rotating word. Brees was shown the
                              original rule and chose a single deliberate exception
                              to get colour into an all-type section. DO NOT ADD A
                              THIRD. Asserted in .shots/network-measure.js.
                              ⚠️ This note and the one in network.css must agree —
                              two files stating contradictory rules is how this
                              project has repeatedly shipped "deliberate" mistakes. */

  /* --- Radius — this is where the warmth lives now ---------------------- */
  --r-sm:   14px;
  --r-md:   24px;
  --r-lg:   40px;
  --r-pill: 999px;
  /* The signature shape, matched to the reference: a true ellipse — curved on
     all four sides, not an arch with square shoulders. Note `999px 999px`
     alone on a wide box gives a half-circle and eats the picture; the paired
     `/` syntax is what keeps it elliptical.
     It only reads right against a busy photograph. Against a flat placeholder
     it looks like a navy egg — that's expected, not a bug. */
  --r-arch: 50% / 50%;

  /* --- Shadow: soft and low. Lift, not drama. -------------------------- */
  --shadow-sm: 0 2px 10px rgba(26, 42, 86, .05);
  --shadow-md: 0 10px 34px rgba(26, 42, 86, .08);

  /* --- Type ------------------------------------------------------------- */
  --font-head: 'Vollkorn', Georgia, serif;
  --font-body: 'Karla', system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* --- Spacing: 8px scale ----------------------------------------------- */
  --s1: 8px;   --s2: 16px;  --s3: 24px;  --s4: 32px;
  --s5: 48px;  --s6: 64px;  --s7: 96px;  --s8: 128px;

  --gutter: clamp(20px, 5vw, 64px);
  --maxw: 1240px;
  --ease: cubic-bezier(.22,.61,.36,1);
}

/* --- Reset ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--cream);
  color: var(--navy);
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; height: auto; display: block; }
h1, h2, h3, h4 { font-family: var(--font-head); font-weight: 500; line-height: 1.14; letter-spacing: -0.015em; margin: 0; }
p { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }
a { color: inherit; }

/* --- Accessibility -------------------------------------------------------- */
.skip-link {
  position: absolute; left: var(--s2); top: -100px; z-index: 999;
  background: var(--navy); color: var(--white);
  padding: var(--s2) var(--s3); text-decoration: none; border-radius: var(--r-pill);
  transition: top .18s var(--ease);
}
.skip-link:focus { top: var(--s2); }

:focus-visible { outline: 2px solid var(--navy); outline-offset: 3px; border-radius: 2px; }
.close :focus-visible { outline-color: var(--white); }

/* --- Layout --------------------------------------------------------------- */
.wrap { max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }
.section { padding-block: clamp(var(--s6), 8vw, var(--s7)); position: relative; }
.section--band { background: var(--cream-2); }

.eyebrow {
  font-family: var(--font-body);
  font-size: 12px; font-weight: 700;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--navy-soft);
  margin: 0 0 var(--s3);
}
.eyebrow--h { font-weight: 700; }
/* Red section marker — the light-ground counterpart to the gold numeral, and the
   only place the logo's red survives on the page (the 7/17 "handled" ticks went
   when What We Manage became a ledger, and the ledger numerals had to go muted
   because red fails as text on navy). CREAM SECTIONS ONLY: on navy this rule
   measures 3.05:1, which scrapes the 3:1 non-text floor and looks muddy. */
.eyebrow--mark::before {
  content: ""; display: block;
  width: 28px; height: 2px; background: var(--red);
  margin-bottom: 14px;
}
.lede { font-size: clamp(18px, 1.5vw, 21px); line-height: 1.6; color: var(--navy-soft); max-width: 62ch; }

/* --- Continuity: overlap, not a hairline ---------------------------------- */
.overlap-up { margin-top: clamp(-96px, -6vw, -40px); position: relative; z-index: 1; }

/* --- Buttons: pills ------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--font-body); font-size: 15px; font-weight: 700;
  letter-spacing: .01em; text-decoration: none;
  padding: 15px 32px; border: 1px solid transparent;
  border-radius: var(--r-pill);
  cursor: pointer;
  transition: background-color .2s var(--ease), color .2s var(--ease),
              border-color .2s var(--ease), transform .2s var(--ease);
}
.btn--primary { background: var(--navy); color: var(--white); border-color: var(--navy); }
.btn--primary:hover { background: var(--navy-deep); border-color: var(--navy-deep); transform: translateY(-1px); }

.btn--ghost { background: transparent; color: var(--navy); border-color: var(--rule); }
.btn--ghost:hover { border-color: var(--navy); background: var(--cream-3); }

.btn--on-dark { background: var(--white); color: var(--navy-deep); border-color: var(--white); }
.btn--on-dark:hover { background: var(--cream-2); border-color: var(--cream-2); transform: translateY(-1px); }

.btn--ghost-dark { background: transparent; color: var(--white); border-color: rgba(255,255,255,.4); }
.btn--ghost-dark:hover { border-color: var(--white); background: rgba(255,255,255,.08); }

.btn-row { display: flex; flex-wrap: wrap; gap: var(--s2); }

/* --- Header --------------------------------------------------------------- */
/* The header is injected into <div id="global-header"> by load-partials.js. A
   sticky element only travels within its PARENT's box, and that wrapper is only
   as tall as the header itself (~125px) — so without this the header unstuck
   after 125px and scrolled away site-wide, leaving a cream dead-band at the top
   of the pinned hero (the --header-h offset assumes the header is always there).
   display:contents dissolves the wrapper's box so the header's containing block
   becomes <body>, and it stays pinned the whole page. Same for the footer. */
#global-header, #global-footer { display: contents; }

.site-header {
  position: sticky; top: 0; z-index: 50;
  background: transparent; border-bottom: 1px solid transparent;
  transition: background-color .3s var(--ease), border-color .3s var(--ease);
}
.site-header.is-stuck { background: var(--cream); border-bottom-color: var(--rule); }
/* 🔴 THE HEADER IS WIDER THAN THE PAGE CONTENT, ON PURPOSE (Brees, 8/21).
   Everything else on the site is capped at --maxw (1240px). The header is not.
   WHY: with six nav items the header content area was hard-capped at 1112px and
   the nav alone ate 869px of it — 78% — leaving 5–39px of slack at every width
   from 1280px up. It did not matter how wide the screen was; --maxw meant a
   2560px monitor had exactly as little room as a 1280px laptop. Shaving the
   gaps stopped the wrapping but made the header feel cramped, which is the
   symptom, not the cause. 1400px turns ~21px of slack at 1440 into ~190px.
   ⚠️ Below ~1240px viewport both are viewport-constrained and still align. The
   header only reads wider than the text beneath it above that — the deliberate,
   accepted trade.
   ⚠️ Do NOT solve a seventh nav item by raising this again. Past ~1400 the logo
   visibly detaches from the content column. Shorten a label or move the drawer
   breakpoint instead. Run `.shots/nav-wrap.js`. */
:root { --maxw-header: 1400px; }
.site-header__inner {
  max-width: var(--maxw-header); margin-inline: auto;
  padding: var(--s1) var(--gutter);
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
}
/* 🔴 THE LOCKUP MUST NEVER SHRINK. `.site-header__inner` is a flex row and
   `.brand` defaulted to flex-shrink:1, so whenever the nav wanted more room than
   existed the browser took it out of the LOGO — and the global `img{max-width:
   100%}` let the image follow. Measured on the live 5-item nav: 214px → 183px at
   960, 195px at 1024, 210px at 1100. Height stayed 76px, so the lockup was being
   squashed horizontally: the exact artwork whose two-line tagline is calibrated
   to 6.9px and which already shipped unreadable once ("ASSOCIATES ar AMERICA").
   Adding a sixth nav item took it to 0px wide at 960 and 1024 — the logo simply
   GONE — and 69px at 1100. Nothing caught it: it is not an overflow, not a
   collision, not a contrast failure, and the alt text still resolved.
   `.shots/_brand.js` compares rendered aspect against natural aspect. */
.brand { display: block; line-height: 0; flex: 0 0 auto; }
/* Horizontal lockup (logo-evaa-h.webp) — paw mark + "EVAA" + swoosh + a
   TWO-LINE tagline, with the text block to the RIGHT of the mark.
   ⚠️ 8/20: the two lines are not a style choice, they are what makes the tagline
   readable. On one line it measures 3.0px tall at this header height and reads
   as a smudge; broken over two it is 4.8px and EVAA grows 21px → 27px, because
   neither has to share the full width any more. Don't "tidy" it back to one line.
   ⚠️ This comment previously said "~4.04:1" for a month while the file it
   described was 2.676:1 — the figure belonged to a lockup that had been replaced.
   Measured, current: 799×285 = 2.804:1, so ~213px wide at the 76px header height
   and ~235px at the 84px footer. */
.brand__mark { width: auto; height: 76px; }
/* The lockup is wide, so shrink it on narrow screens to clear the hamburger. */
@media (max-width: 620px) { .brand__mark { height: 58px; } }

/* 🔴 GAPS TIGHTENED 8/21, WHEN THE NAV WENT FROM FIVE ITEMS TO SIX.
   `.site-header__inner` is capped at --maxw (1240px), so the nav does NOT get
   more room on a wider screen — at 2560px it has exactly as much space as at
   1280px. With a sixth item at the old gaps (48 / 32) the links no longer fit
   and FOUR of them wrapped to a second line, at every width from 960px up. The
   header grew 93px → 99px with it.
   ⚠️ nav-fit.js passed that: it tests overflow and collision, and wrapping is
   neither. `.shots/nav-wrap.js` was written to catch it and is the check to run
   if a seventh item is ever added — at which point this needs solving properly
   (shorter labels, or the drawer at a wider breakpoint), not by shaving gaps
   again. `white-space: nowrap` means the next overflow will be visible and
   loud rather than a silent second line. */
/* Gaps restored to the original rhythm now that --maxw-header gives the nav
   real room. They were shaved to clamp(24,2.4vw,40) / clamp(16,1.8vw,28) purely
   to stop the six-item nav wrapping inside 1112px; that fixed the wrap and made
   the header feel cramped. The container was the problem, not the spacing. */
/* Two gap scales, MOBILE-FIRST (base, then add at min-width — never the other
   way round; this repo has shipped the shadowed-override bug four times).
   861–1239px is the tight band: the header is still viewport-constrained there,
   so six items need close spacing. From 1240px up --maxw-header gives real room
   and the original rhythm returns. */
.primary-nav { display: flex; align-items: center; gap: clamp(20px, 2.2vw, 32px); }
.primary-nav__list { display: flex; gap: clamp(14px, 1.6vw, 24px); }
/* 1400px, NOT 1240px: --maxw-header is 1400, so the container does not actually
   reach full width until the viewport does. Switching the gaps at 1240 gave the
   1280px band generous spacing inside a still-narrow container - 17px of slack,
   i.e. tight again. Tie the gap change to the width where the room appears. */
@media (min-width: 1400px) {
  .primary-nav { gap: var(--s5); }
  .primary-nav__list { gap: var(--s4); }
}
.primary-nav__list a {
  position: relative; text-decoration: none; white-space: nowrap;
  font-size: 15px; font-weight: 500; color: var(--navy); padding-block: 4px;
}
/* The header CTA must never break into "Book a" / "Call" — it did between 960
   and 1100px once the nav gained a sixth item, taking the header to 123px. */
.primary-nav__cta { white-space: nowrap; }
.primary-nav__list a::after {
  content: ''; position: absolute; left: 0; bottom: 0;
  width: 0; height: 2px; background: var(--navy); border-radius: var(--r-pill);
  transition: width .25s var(--ease);
}
.primary-nav__list a:hover::after,
.primary-nav__list a.is-current::after { width: 100%; }
.nav-toggle { display: none; }

/* =========================================================================
   REVIEW MODE  (added 7/31 — see the note at the top of includes/header.html)
   🟢 REVIEW MODE IS OVER. Both items it covered are live: The Excelsior
   Network went back in the nav 8/4, and Book a Call was wired to book.html on
   8/19. **Nothing in the markup carries `data-review-mode` any more.**
   ⚠️ THE RULES BELOW ARE KEPT ON PURPOSE, THOUGH NOTHING USES THEM TODAY.
   They are the house pattern for disabling a component honestly — a <span>
   with aria-disabled rather than a dead <a>, and contrast held above the
   floor even though WCAG exempts inactive controls. The next page built
   mid-review will want them. Deleting them would mean the next person
   improvises it, and the first improvisation shipped 2.37:1 on every page.
   📌 If you re-enable review mode, say so in session-notes.md — an inert CTA
   that nobody records is indistinguishable from a bug.

   ⚠️ THEY ARE <span>s, NOT DISABLED LINKS. An <a> without an href is already
   not focusable, but it still reads as a link to assistive tech; a <span> with
   aria-disabled says "this exists and is unavailable", which is the truth.
   Neither takes focus, so the tab order simply skips them and `deep-a11y.js`'s
   traversal stays clean.
   ⚠️ CONTRAST IS KEPT AT --navy-soft (5.94:1) ON PURPOSE. WCAG exempts inactive
   components from 1.4.3, so a lighter grey would technically pass — but this
   project's standard is that nothing ships below the floor, and a client is
   about to read it. Recessive is achieved with weight and the missing
   underline, not by making the text hard to read.
   ========================================================================= */
/* 🔴 GROUND-SPECIFIC COLOURS — THE FIRST CUT SHIPPED 2.37:1 ON EVERY PAGE.
   `--navy-soft` is 5.94:1 on cream and ~1.6:1 on navy, and these components
   appear in BOTH the header (cream) and the footer (navy). One colour for two
   grounds failed deep-a11y on all five pages instantly.
   ⚠️ THIS IS THE FOURTH TIME THIS EXACT TRAP HAS BEEN HIT ON THIS PROJECT
   (Team §2, network §8 round 5, the §8 payoff in round 8, now this).
   **Never give a shared component ONE colour when it renders on two grounds.**
   Measured after the fix, not assumed — deep-a11y.js on all five pages. */
.nav-soon {
  font-size: 15px; font-weight: 400; color: var(--navy-soft);   /* 5.94:1 on cream */
  padding-block: 4px; cursor: default; user-select: none;
}
.site-footer .nav-soon { color: rgba(251, 247, 239, .62); }      /* 5.34:1 on navy */

.btn.is-inert { cursor: default; user-select: none; }
.btn.is-inert:hover { transform: none; }
/* Header, cream ground. */
.btn--primary.is-inert,
.btn--primary.is-inert:hover {
  background: var(--navy-soft); border-color: var(--navy-soft); color: var(--white);
}
/* Footer, navy ground — a ghost, because a navy-soft fill on navy is invisible.
   Border alpha matches `.btn--ghost-dark`, already verified at 3.30:1 against
   the 3:1 non-text floor (the tightest margin on the site). */
.btn--on-dark.is-inert,
.btn--on-dark.is-inert:hover {
  background: transparent;
  border: 1px solid rgba(251, 247, 239, .4);
  color: rgba(251, 247, 239, .75);
}

/* Visually hidden, still announced. There was no shared utility for this —
   network.css carried its own copy inline for the §4 rotator. */
.vh {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* --- Hero: centred, with the arch ----------------------------------------
   The reference's signature move: a big centred display headline, with the
   photograph masked into an arch sitting under it. Ours is a 4-line headline
   rather than their 1-liner, so the arch sits below rather than overlapping
   into it.
   -------------------------------------------------------------------------- */
.hero { padding-block: clamp(var(--s5), 5vw, var(--s6)) 0; text-align: center; }
/* constrain the TITLE, never the container — a max-width here squeezes the
   sub and the buttons into a ribbon too. */
.hero__title {
  font-size: clamp(2.4rem, 5vw, 4.2rem);
  max-width: 18ch; margin: 0 auto var(--s4);
}
.hero__sub { margin: 0 auto var(--s5); max-width: 60ch; }
.hero .btn-row { justify-content: center; }

.hero__figure { margin: clamp(var(--s5), 5vw, var(--s6)) auto 0; max-width: 1020px; position: relative; }

/* --- Hero video ------------------------------------------------------------
   Contained, no autoplay. A vet who chooses to press play is worth more than
   footage they scrolled past — and prefers-reduced-motion is satisfied by
   construction. TODO: the video does not exist. Halie shoots it.
   -------------------------------------------------------------------------- */
.hero__img, .video {
  width: 100%; aspect-ratio: 16 / 10; object-fit: cover;
  border-radius: var(--r-arch);
  overflow: hidden;
}
.video { position: relative; background: var(--navy-deep); }
.video__media { width: 100%; height: 100%; object-fit: cover; display: block; }
.video__play {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; border: 0; padding: 0; cursor: pointer;
  background: rgba(18, 29, 59, .3);
  transition: background-color .25s var(--ease);
}
.video__play:hover { background: rgba(18, 29, 59, .14); }
.video__play-ring {
  width: 88px; height: 88px; border-radius: 50%;
  background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.85);
  display: flex; align-items: center; justify-content: center;
  transition: transform .25s var(--ease), background-color .25s var(--ease);
}
.video__play:hover .video__play-ring { transform: scale(1.07); background: rgba(255,255,255,.26); }
.video__play-ring::after {
  content: ''; margin-left: 5px;
  border-left: 16px solid #fff;
  border-top: 10px solid transparent; border-bottom: 10px solid transparent;
}
.video__label {
  position: absolute; left: 0; right: 0; bottom: var(--s4);
  color: rgba(255,255,255,.92); font-size: 13px; letter-spacing: .04em;
  text-shadow: 0 1px 8px rgba(0,0,0,.5);
}
.video--needed .video__play { background: transparent; }
.video__tag {
  position: absolute; left: 50%; transform: translateX(-50%); top: var(--s4); z-index: 1;
  font-size: 11px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
  background: #2E7D52; color: #fff; padding: 6px 12px; border-radius: var(--r-pill);
}
@media (prefers-reduced-motion: reduce) {
  .video__play, .video__play-ring { transition: none !important; }
  .video__play:hover .video__play-ring { transform: none; }
  .btn:hover { transform: none !important; }
}

/* --- Stacked reveal: the mosaic pins, the ledger slides over it -----------
   "How We're Different" holds at the top of the viewport while "What We Manage
   For You" rises and progressively covers it.

   Moved down one section 7/20 pm (was: intro pins, mosaic covers). The intro
   scrolls normally again.

   Bounded to .stack-over so the pin releases once the ledger has done the
   covering — sticky inside <main> would hold the mosaic pinned for the whole
   rest of the page.

   The ledger is already opaque navy so it occludes correctly. (When the INTRO
   was the pinned panel this needed an explicit background on the covering
   section, because .section is transparent and the pinned navy showed straight
   through. Anything else added inside .stack-over must be opaque.)

   ⚠️ THE PIN OFFSET IS COMPUTED, NOT ZERO. The mosaic is ~873px and collapses to
   ONE COLUMN at ≤900px wide (1514px on iPad), so it is routinely TALLER than the
   viewport — a real laptop has ~780px of viewport after browser chrome.
   `top:0` on a section taller than the screen silently cuts off its own bottom.

   A first attempt guarded with `min-height:920px` instead. That "worked" only by
   never running: on any normal laptop the effect simply never appeared, which is
   exactly how it was reported. Guarding on height was the wrong instinct.

   So `initStackPin()` in main.js measures the section and sets --pin-top to
   `min(0, viewportHeight - sectionHeight)`:
     - section FITS   → --pin-top: 0    → pins to the top, as before
     - section TALLER → negative offset → its BOTTOM pins to the viewport bottom,
       so nothing is ever cut off and the panel still holds still while the
       ledger covers it.
   Recomputed on resize. The fallback `0px` in the var() keeps it sane if the JS
   never runs.

   Still width-guarded: below 901px the mosaic is a tall single column and a
   held panel that long reads as a scroll-jam rather than an effect.
   -------------------------------------------------------------------------- */
.stack-over { position: relative; }
.stack-over > .manage--ledger { position: relative; z-index: 2; }

@media (min-width: 901px) {
  .stack-over > .stack-pin { position: sticky; top: var(--pin-top, 0px); z-index: 0; }
}
/* Stacked panels are a large-surface motion effect — opt out entirely. */
@media (prefers-reduced-motion: reduce) {
  .stack-over > .stack-pin { position: relative; top: auto; }
}

/* --- How We're Different: image mosaic -----------------------------------
   History: v1 numbered these 01/02/03 (rejected); v3 made soft raised cards
   (too template-y); v4 (7/17) went hairline-ruled editorial rows. v5 (7/20,
   Brees) ports Woodhaven's homepage sec-5 ServicesGrid: a 12-col mosaic where
   every point earns a photo, text sits ON the image, and hover scales the
   image + expands the body.

   Woodhaven runs 7 services in the monogram; we have 3 points, so the
   placement is rebuilt: one tall signature card left, two stacked right.

   Two deliberate deviations from the Woodhaven source:
   1. Woodhaven scrims with a linear-gradient. DE rules say no gradients, so
      this is a FLAT rgba scrim doing the same legibility job.
   2. The scrim is .65, not Woodhaven's ~.4-.75 gradient. Measured: over a
      BRIGHT photo a .52 scrim gives cream body text only 3.58:1 (fails AA);
      .65 gives 5.73:1, so the card stays compliant no matter how light the
      final photograph turns out. Don't lower it without re-measuring.
   -------------------------------------------------------------------------- */
.diff-mosaic {
  display: grid; grid-template-columns: repeat(12, 1fr);
  grid-auto-rows: 132px; gap: clamp(14px, 1.6vw, 22px);
  margin-top: clamp(var(--s4), 4vw, var(--s6));
}
.dcard {
  position: relative; border-radius: 16px; overflow: hidden;
  background: var(--navy-deep); isolation: isolate;
}
.dcard:nth-child(1) { grid-column: 1 / 7;  grid-row: span 4; }
.dcard:nth-child(2) { grid-column: 7 / 13; grid-row: span 2; }
.dcard:nth-child(3) { grid-column: 7 / 13; grid-row: span 2; }

.dcard img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .6s var(--ease);
}
.dcard:hover img, .dcard:focus-within img { transform: scale(1.05); }

.dcard__scrim { position: absolute; inset: 0; z-index: 1; background: rgba(9, 14, 30, .65); }
.dcard__content { position: absolute; inset: auto 0 0 0; z-index: 2; padding: clamp(20px, 2.5vw, 32px); }
.dcard__title {
  font-family: var(--font-head); font-weight: 500;
  font-size: clamp(1.25rem, 2vw, 1.9rem); line-height: 1.1;
  color: var(--cream); margin: 0;
  text-shadow: 0 2px 18px rgba(9,14,30,.75);
}
/* Body is collapsed at rest so the title anchors to the bottom edge, then
   expands on hover/focus — Woodhaven's card behaviour. */
.dcard__body {
  font-size: clamp(.9rem, .95vw, 1rem); line-height: 1.55;
  color: rgba(251, 247, 239, .92); max-width: 44ch;
  max-height: 0; margin: 0; opacity: 0; overflow: hidden;
  transform: translateY(6px);
  transition: max-height .4s var(--ease), margin-top .4s var(--ease),
              opacity .35s ease .05s, transform .4s ease;
  text-shadow: 0 2px 14px rgba(9,14,30,.8);
}
.dcard:hover .dcard__body, .dcard:focus-within .dcard__body {
  max-height: 14em; margin-top: 12px; opacity: 1; transform: none;
}
/* Signature card keeps its body open, like Woodhaven's m-hyper. */
.dcard--sig .dcard__body { max-height: 14em; margin-top: 12px; opacity: 1; transform: none; }

@media (max-width: 900px) {
  .diff-mosaic { grid-template-columns: 1fr; grid-auto-rows: auto; }
  .dcard { grid-column: 1 / -1 !important; grid-row: auto !important; aspect-ratio: 16 / 10; }
  /* No hover on touch — every body copy is shown outright. */
  .dcard__body { max-height: 14em; margin-top: 12px; opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .dcard img, .dcard__body { transition: none !important; }
  .dcard:hover img, .dcard:focus-within img { transform: none; }
}

/* --- What We Manage: numbered ledger --------------------------------------
   v1 (7/17) was a DARK editorial chapter: sticky anchor column on the left,
   an 8-item index scrolling past on the right, red ticks, paw watermark.
   v2 (7/20, Brees) replaces it after two problems surfaced while testing a
   Woodhaven-style image+toggle here:
     1. Anything that hides copy behind a click buries the point — the LENGTH
        of this list IS the relief. Everything stays visible at rest.
     2. Eight items in one tall column read as a slog. The header now runs
        ACROSS the top instead of pinning down the left, which frees the full
        width for two ledger columns of four.

   Set as a LEDGER (01-08, term, definition, ruled) because EVAA is a
   practice-management company: a statement of account is the native form for
   "here is everything we take off your books."

   v3 (7/20, Brees) — GROUND BACK TO NAVY, text inverted. Cream read "blocky":
   on a light ground the hairline grid and the two ruled columns dominate, so
   you see the box structure before you see the words. On navy the rules drop
   back to a whisper and the terms carry.

   ⚠️ CONTRAST NOTE — the numerals had to change colour. On cream they were
   --red at 5.09:1 (fine). On navy-deep --red measures 3.03:1, which fails AA
   for text, so the numerals are now muted blue-grey (#9AA6C4 = 6.77:1). Red
   survives in this section as the decorative tick only, where the 3:1 non-text
   threshold applies and 3.03:1 passes. Do NOT set the numerals back to red.
   -------------------------------------------------------------------------- */
.manage--ledger { position: relative; overflow: hidden; background: var(--navy-deep); color: var(--cream); }
.manage--ledger .eyebrow { color: #9AA6C4; }
.manage__head {
  display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 3fr);
  gap: clamp(var(--s4), 5vw, var(--s6)); align-items: end;
  padding-bottom: clamp(var(--s5), 4vw, var(--s6));
}
.manage__kicker {
  font-family: var(--font-head); font-weight: 500;
  font-size: clamp(1.6rem, 2.9vw, 2.5rem); line-height: 1.2;
  letter-spacing: -0.01em; max-width: 24ch; margin: var(--s3) 0 0; color: var(--cream);
}
.manage__count { margin: 0; text-align: right; }
/* The single gold moment on the page. Large display type on navy: 8.78:1, far
   past the 3:1 large-text floor. Its label stays muted on purpose — numeral AND
   label in gold stops being an accent and becomes a gold block (i.e. v1). */
.manage__count-num {
  font-family: var(--font-head); font-weight: 500;
  font-size: clamp(3rem, 6vw, 5.5rem); line-height: .9; display: block; color: var(--gold);
}
.manage__count-label {
  display: block; font-family: var(--font-body); font-weight: 700;
  font-size: 12px; letter-spacing: .18em; text-transform: uppercase;
  color: #9AA6C4; margin-top: 12px;
}
.manage__ledger {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(var(--s5), 5vw, var(--s7));
  border-top: 1px solid rgba(255,255,255,.14);
}
.manage__row {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  column-gap: clamp(var(--s3), 2vw, var(--s4));
  padding-block: clamp(var(--s4), 2.6vw, var(--s5));
  border-bottom: 1px solid rgba(255,255,255,.14);
  align-items: baseline;
}
/* Muted, NOT red — see the contrast note above. */
.manage__num {
  font-family: var(--font-body); font-weight: 700; font-size: 12px;
  letter-spacing: .1em; color: #9AA6C4; font-variant-numeric: tabular-nums;
}
.manage__term {
  font-family: var(--font-head); font-weight: 500;
  font-size: clamp(1.2rem, 1.7vw, 1.55rem); margin: 0 0 6px; color: var(--cream);
}
.manage__def { font-size: 1rem; color: #AEB6CC; line-height: 1.58; margin: 0; }
@media (max-width: 860px) {
  .manage__ledger { grid-template-columns: 1fr; }
  .manage__head { grid-template-columns: 1fr; align-items: start; }
  .manage__count { text-align: left; }
}
/* --- Team ----------------------------------------------------------------- */
.team__intro { font-family: var(--font-head); font-size: clamp(1.5rem, 2.9vw, 2.4rem); line-height: 1.3; max-width: 20ch; margin-top: var(--s3); }
.team__grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: clamp(var(--s4), 5vw, var(--s7)); margin-top: var(--s6); }
.person__img {
  width: 100%; aspect-ratio: 4 / 5; object-fit: cover; object-position: 50% 18%;
  /* NOT the ellipse: an oval mask eats the bottom corners, and Sean's scrubs
     are embroidered "Sean Grasso DVM" down there. That detail is the wedge as
     a photograph — it doesn't get cropped for a shape. */
  border-radius: var(--r-lg);
  margin-bottom: var(--s3);
}
.person__name { font-size: clamp(1.4rem, 2.2vw, 1.8rem); margin-bottom: 6px; }
.person__role {
  font-size: 12px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase;
  color: var(--navy-soft); margin-bottom: var(--s2);
}
.person__bio { color: var(--navy-soft); max-width: 46ch; }

/* --- Close: a rounded navy slab, inset ------------------------------------ */
.close {
  position: relative; isolation: isolate; overflow: hidden;
  background: var(--navy-deep); color: var(--white); text-align: center;
  /* 7/20: was a rounded slab inset to --maxw with a bottom margin. Now runs
     full-bleed and butts straight into the footer, which is the SAME
     navy-deep — the two read as one continuous dark mass closing the page.
     Any bottom margin/radius here reopens the seam. Inner .wrap still holds
     the text to --maxw. */
  border-radius: 0;
  max-width: none; margin: 0;
  padding-block: clamp(var(--s6), 8vw, var(--s7));
}
/* Background photograph + flat scrim.
   The image is decorative (aria-hidden, empty alt) — the section's meaning is
   entirely in the copy, so it must never be announced. */
.close__bg {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover;
}
/* ⚠️ SCRIM IS .72 AND IS NOT ARBITRARY — do not lighten without re-measuring.
   Tuned against the worst case (a pure-white photograph), because the final
   image isn't shot yet. At .72: the title (#fff) measures 6.64:1 and
   .close__sub (78% white, 18px, so it needs the full 4.5) measures 4.80:1.
   At .65 the sub drops to 3.92:1 and FAILS AA. If the delivered photograph is
   genuinely dark, this can come down — but re-measure against that actual
   image first, don't just eyeball it.
   Flat rgba, not a linear-gradient (house rule). */
.close__scrim { position: absolute; inset: 0; z-index: 1; background: rgba(18, 29, 59, .72); }
.close .wrap { position: relative; z-index: 2; }
.close__title { font-size: clamp(2.2rem, 4.6vw, 3.4rem); margin-bottom: var(--s3); }
.close__sub { color: rgba(255,255,255,.78); font-size: 18px; margin-bottom: var(--s5); }
.close .btn-row { justify-content: center; }

/* --- Footer --------------------------------------------------------------- */
/* 7/20: was --cream-2 (#F5EFE2) — the generic "soft panels" token, inherited
   rather than chosen. Now navy-deep, matching .close above it so the close and
   the footer form one uninterrupted dark block.
   ⚠️ The footer mark had to change with it: logo-evaa.png has a baked-in halo
   (bottom-right pixel is opaque cream, verified) and shows an artifact on dark.
   logo-evaa-h.webp is fully transparent, so the footer now carries the
   HORIZONTAL lockup. Don't swap the vertical mark back in without re-exporting
   it with real transparency. */
.site-footer { background: var(--navy-deep); color: var(--cream); }
.site-footer :focus-visible { outline-color: var(--white); }
.site-footer__inner {
  max-width: var(--maxw); margin-inline: auto;
  padding: var(--s7) var(--gutter) var(--s6);
  display: grid; grid-template-columns: 2fr 1fr 1.4fr; gap: var(--s6);
}
/* 🔴 The one thing on the site that broke 320px reflow (WCAG 1.4.10).
   ⚠️ 8/14: the published address changed to "info@evaofamerica.com" (21 chars,
   not 38) when email moved to its own domain, so the token now MEASURES 188px
   in the footer at 320px — not the ~314px described below — and no longer
   overflows on its own.
   🔴 THE GUARD STAYS ANYWAY. It costs nothing, and the failure it prevents is
   a horizontal scrollbar on the ENTIRE DOCUMENT sourced from the footer — which
   comes back the moment anyone publishes a longer address (sean.grassodvm@ and
   halie.longobardi@ are both longer than the old one, and both exist).
   Removing it is a one-line change that breaks a page nobody would think to
   re-test. The original diagnosis, kept because it explains the mechanism:
   "info@excelsiorveterinaryassociates.com" was a single unbreakable token ~314px wide.
   A grid item's default min-width is auto = min-content, so at 320px the column
   resolved to 313.578px inside a 280px content box and pushed the whole DOCUMENT to
   334px — a horizontal scrollbar on the entire page, sourced from the footer.
   Both halves are needed: min-width:0 lets the grid column shrink below min-content,
   overflow-wrap lets the address itself break. Removing either brings it back.
   ⚠️ This scales with the domain name. excelsiorveterinaryassociates.com is long and
   any new address at this domain will be too. */
.site-footer__inner > * { min-width: 0; }
.site-footer a[href^="mailto:"] { overflow-wrap: anywhere; }
.site-footer__mark { height: 84px; width: auto; margin-bottom: var(--s3); }
.site-footer__tagline { font-family: var(--font-head); font-size: 1.25rem; max-width: 24ch; margin-bottom: var(--s3); }
.site-footer__heading {
  font-family: var(--font-body); font-size: 12px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: #9AA6C4;
  margin-bottom: var(--s3);
}
.site-footer__nav li { margin-bottom: var(--s1); }
.site-footer__nav a, .site-footer__contact a {
  color: var(--cream); text-decoration: none;
  border-bottom: 1px solid transparent; transition: border-color .2s var(--ease);
}
.site-footer__nav a:hover, .site-footer__contact a:hover { border-bottom-color: var(--cream); }
.site-footer__note { color: #AEB6CC; font-size: 14px; margin-top: var(--s2); max-width: 30ch; }
.site-footer__bar {
  border-top: 1px solid rgba(255,255,255,.14);
  max-width: var(--maxw); margin-inline: auto;
  padding: var(--s3) var(--gutter);
  display: flex; justify-content: space-between; gap: var(--s2); flex-wrap: wrap;
  font-size: 13.5px; color: #9AA6C4;
}
.site-footer__bar a { color: #9AA6C4; }

/* ==========================================================================
   SEC 1 + SEC 2 — the pinned hero. Direction D, locked 7/16.
   Ref: letsplayfight.com, structure read off their markup:
     .hero_scroll 300vh              -> .pf-hero
     .hero_sticky sticky top:0       -> .pf-sticky
     img-move x5                     -> .pf-v is-1..is-5
     video-flip-move                 -> .pf-video (grows to full screen)
     .intro_text grid-column:2/span4 -> .pf-para
     data-anim-para                  -> word-by-word reveal
   ========================================================================== */
/* ============================================================
     SEC 1 — Playfight's hero, rebuilt.
       .hero_scroll  300vh          → .pf-hero
       .hero_sticky  sticky top:0   → .pf-sticky
       img-move × 5                 → .pf-v is-1..is-5  (drift + fade)
       video-flip-move              → .pf-video         (grows to full screen)
       target-video-container       → dropped — see the .pf-video note below
     ============================================================ */
  /* 450vh, not 300: the video now has a phase AFTER it fills, where the scrim
     and Alie's copy reveal on top of it. At 300vh those two shared ~8% of the
     track and flashed past. */
  /* 250vh (7/21 pm). History, so it isn't drifted back up: this was 300vh, then
     pushed to 450vh to buy room for the new scrim/copy phase. That bought the
     animation room by spending the visitor's patience — at 450vh the CTAs did not
     appear until ~2 full screens down and the word reveal ate another ~1100px
     AFTER the video had already finished expanding. Brees, 7/21: "I don't have to
     scroll a bunch and then have the text animate."
     The fix that made 250vh possible is that the word reveal is no longer
     scroll-driven — it auto-cascades on trigger (see initHeroScroll in main.js),
     so the track only has to cover drift + expand + scrim, not the reading too.
     ⚠️ Do not lengthen this to make room for a new phase. Trigger the new phase
     instead. */
  .pf-hero{height:250vh;position:relative}
  .pf-sticky{position:sticky;top:0;height:100vh;overflow:hidden;
    /* Dark by default. This mechanic is photographs floating in space — on cream
       they read as paper scraps; on navy they read as a cinema. The void is what
       gives the drift depth. */
    background:var(--navy-deep)}

  .pf-grid{position:absolute;inset:0;z-index:1;pointer-events:none}
  .pf-v{position:absolute;border-radius:8px;overflow:hidden;
    will-change:transform,opacity;box-shadow:var(--shadow-md);background:var(--navy)}
  .pf-v img{width:100%;height:100%;object-fit:cover;display:block}
  /* The hero video fills its card at every size — main.js animates the CARD's
     box from a 14vw rest rect to the full pinned viewport, and the media just
     tracks it. object-fit:cover so the framing survives both extremes. */
  .pf-video__media{width:100%;height:100%;object-fit:cover;display:block;border-radius:inherit}
  /* is-5 and is-6 sit UNDER the headline. The title carries a dark halo, but that
     alone is not enough over a bright photograph — the first real-photo pass measured
     2.09:1 there (needs 3:1 for large text). This scrim is the safety net so the slot
     stays legible whatever photo lands in it later. Flat rgba, not a gradient. */
  .pf-v.is-5::after, .pf-v.is-6::after{
    content:"";position:absolute;inset:0;pointer-events:none}
  /* Measured, not guessed. is-5 sits under "We handle the" and clears at .35 (4.06:1).
     is-6 is the BIG card under the headline's brightest stretch and needed .50 — at
     .35 it measured 2.72:1, still under the 3:1 large-text floor even carrying the
     darkest photo we own. .50 also guarantees >=3:1 over a pure-white frame, so the
     slot is safe for any future photo. */
  .pf-v.is-5::after{background:rgba(18,29,59,.35)}
  .pf-v.is-6::after{background:rgba(18,29,59,.50)}
  /* Scattered layout — the original approved arrangement. Five cards drift around
     the headline; three more (is-6/7/8) fill the gaps. Some cards sit partly
     BEHIND the headline on purpose — the title is z-index:3 over the grid's
     z-index:1 and carries a soft shadow, so it always reads on top. */
  .pf-v.is-1{left:2%;   top:10%; width:16vw;aspect-ratio:3/2}     /* Sean on the floor   — top-left      */
  .pf-v.is-2{right:3%;  top:8%;  width:12vw;aspect-ratio:14/19}   /* A nurse mid-shift    — top-right     */
  .pf-v.is-3{left:26%;  top:66%; width:22vw;aspect-ratio:16/9}    /* The building         — centre-bottom (sized to Brees' box) */
  .pf-v.is-4{right:5%;  top:62%; width:15vw;aspect-ratio:3/2}     /* Hands on a patient   — right-bottom   */
  .pf-v.is-5{left:25%;  top:21%; width:13vw;aspect-ratio:1}       /* The team working     — behind headline left (moved to Brees' box) */
  /* the two more, sized + placed to match the boxes drawn on the screenshot */
  .pf-v.is-6{left:52%;  top:13%; width:20vw;aspect-ratio:1}       /* A team huddle        — big, top centre-R (marked box) */
  .pf-v.is-7{left:65%;  top:73%; width:12vw;aspect-ratio:4/3}     /* Surgery in progress  — small, bottom centre-R (marked box) */

  /* the headline holds the middle while everything moves around it */
  /* place-CONTENT, not place-ITEMS. With place-items:center this was fine while the
     demo had a single child, but the eyebrow added a second implicit row — the rows
     then stretched to fill the sticky and each item centred inside its own half,
     floating the eyebrow up and driving the headline down into is-3. place-content
     centres the whole track block as one unit, which is what was always meant. */
  /* ⚠️ TRUE VERTICAL CENTRE — do not add padding-block here to dodge a collision.
     I did exactly that (150px bottom pad = 75px lift) to clear the landing CTA off
     is-3, and Brees caught it immediately with a box drawn round this container:
     "I still want the text container to be vertically centered." The block is
     eyebrow + headline + CTA and it centres in the band under the header, full stop.
     Collisions with the drifting visuals get solved by moving the VISUAL, not by
     nudging this block off centre. */
  .pf-content{position:absolute;inset:0;z-index:3;display:grid;
    place-content:center;justify-items:center;
    padding-inline:var(--gutter);
    text-align:center;pointer-events:none;
    will-change:transform,opacity}
  /* Legibility: several cards sit BEHIND the headline on purpose, so the title
     carries a dark halo that hugs each letter and knocks back whatever is behind
     it — a real photo, or (for now) a placeholder's own label. Layered blurred
     shadows, NOT a gradient (house rule); this reads as a soft glow, not a box. */
  .pf-content h1{font-size:clamp(2.2rem,4.8vw,4.2rem);max-width:17ch;margin:0;color:#fff;
    text-shadow:0 0 14px rgba(9,15,33,.92), 0 0 32px rgba(9,15,33,.82),
                0 2px 6px rgba(9,15,33,.9)}
  /* Halo strengthened alongside the colour bump — the eyebrow sits on a photo now
     (is-6) rather than on plain navy, same situation the H1 has always been in. */
  .pf-eyebrow{text-shadow:0 0 14px rgba(9,15,33,.95), 0 0 28px rgba(9,15,33,.8),
                          0 1px 8px rgba(9,15,33,.9)}

  /* The travelling video. Starts as a small visual bottom-left, ends filling the
     whole viewport.

     NOTE: v1 of this did a FLIP onto a .pf-target using offsetLeft/offsetTop. That
     was broken — the target was centred with transform:translate(-50%,-50%), and
     offset* reports LAYOUT position and ignores transforms. So it measured the
     target's un-centred left:50%/top:50% and flew the video's top-left corner to
     the middle of the screen, growing from there into the bottom-right. The target
     is gone; we animate left/top/width/height straight to the viewport instead.
     It's absolutely positioned so nothing else reflows, the play ring stays centred
     for free, and object-fit:cover means no squash as the aspect changes. */
  .pf-video{position:absolute;z-index:4;border-radius:8px;overflow:hidden;
    background:var(--navy);
    /* outlined: a navy placeholder on a navy background would vanish */
    box-shadow:0 0 0 1px rgba(255,255,255,.22), var(--shadow-md);
    will-change:left,top,width,height}

  .pf-video .video__play{position:absolute;inset:0}
  /* the tag/ring are sized for a full hero panel — shrink them for the small
     travelling one, or they swamp it before it grows */
  .pf-video .video__tag{font-size:8px;letter-spacing:.1em;padding:4px 7px;top:8px}
  .pf-video .video__play-ring{width:44px;height:44px}
  .pf-video .video__play-ring::after{border-left-width:9px;border-top-width:6px;border-bottom-width:6px}
  /* The landing CTA. .pf-content is pointer-events:none (so the drifting visuals
     underneath stay non-interactive) — this has to opt back IN or the button is
     dead to both mouse and touch. */
  .pf-lead-cta{margin-top:var(--s5);pointer-events:auto}

  /* Scroll hint. Was 11px at 50% white, centred, fading out by 10% of the track —
     technically present, but Brees didn't know it existed, which is the only test
     that matters. Now 13px at 78% white (7.4:1 on navy-deep, comfortably AA) with a
     bouncing chevron, and it holds until 20% of the track.
     Kept CENTRED rather than moved to a corner: centre-bottom under the headline is
     the convention a first-time visitor already scans for, and a corner reads as
     chrome. The fix here was presence, not position. */
  .pf-hint{position:absolute;bottom:22px;left:0;right:0;text-align:center;z-index:5;
    font:500 13px/1 Karla,sans-serif;letter-spacing:.16em;text-transform:uppercase;
    color:rgba(255,255,255,.78);pointer-events:none}
  .pf-hint::after{content:"";display:block;width:8px;height:8px;margin:10px auto 0;
    border-right:1.5px solid rgba(255,255,255,.78);
    border-bottom:1.5px solid rgba(255,255,255,.78);
    transform:rotate(45deg);animation:pf-hint-bob 1.8s var(--ease) infinite}
  @keyframes pf-hint-bob{
    0%,100%{transform:rotate(45deg) translate(0,0)}
    50%    {transform:rotate(45deg) translate(3px,3px)}
  }
  /* The bob is the only always-on motion on the page. Reduced motion kills it and
     leaves the static chevron, which still reads as "down". */
  @media (prefers-reduced-motion:reduce){
    .pf-hint::after{animation:none}
  }

  /* ============================================================
     SEC 2 — Playfight's intro.
       .intro_text { grid-column:2/span 4; max-width:55ch }
       data-anim-para="true"  → words reveal on scroll
     ============================================================ */
  /* --- Scrim + copy layered over the enlarged video (7/21) ----------------
     Sean's talking-head intro isn't happening, so the video stopped being a
     "play me" object and became the backdrop for the pitch. The standalone
     .pf-intro SECTION is gone — its copy now lives inside .pf-sticky.

     ⚠️ SCRIM IS CONTRAST-CRITICAL. White copy sits on moving video, so there is
     no single background colour to measure — it has to hold on the video's
     BRIGHTEST frame. Measured against that frame, not an average. Flat rgba,
     not a gradient (house rule). Opacity is driven by JS so it fades in only as
     the video finishes filling; this value is its peak.
     No logo over the video — deliberate, the header already carries it. */
  .pf-scrim{position:absolute;inset:0;z-index:5;pointer-events:none;
    background:rgba(9,14,30,.78);opacity:0}
  /* color:#fff is REQUIRED, not decorative. This copy used to live in .pf-intro,
     which set it on the section. Moving it into the hero dropped that rule and the
     text silently fell back to the body's navy — navy-on-a-dark-scrimmed-video,
     effectively invisible. Don't remove. */
  /* 🔴 pointer-events:none is LOAD-BEARING, not tidiness.
     This is z-index:6, inset:0 — it covers the entire pinned viewport, sitting above
     .pf-content (z-index:3). At opacity:0 it is invisible but still hit-tests, so it
     silently ate every click meant for the landing "Book a Call" button. The button
     rendered perfectly and did nothing; elementFromPoint returned .pf-overlay__inner.
     initHeroScroll flips this back to `auto` once the overlay is actually visible
     (>.5 opacity), so the CTAs inside it stay clickable when it's the live layer.
     If you ever set opacity from CSS instead of JS, carry the pointer-events with it. */
  /* visibility:hidden is the a11y half of the same problem pointer-events solved for
     the mouse. opacity:0 alone leaves "Get in Touch" and "Book a Call" in the TAB
     ORDER — a keyboard user tabbing from the top landed on two buttons that were not
     on screen in any visible sense (WCAG 2.4.7 focus visible / 2.4.11 focus not
     obscured). visibility:hidden removes them from the tab order and the a11y tree;
     initHeroScroll flips it back as the overlay fades in. */
  .pf-overlay{position:absolute;inset:0;z-index:6;display:grid;place-content:center;
    padding-inline:var(--gutter);opacity:0;color:#fff;
    pointer-events:none;visibility:hidden}
  .pf-overlay__inner{max-width:var(--maxw);margin-inline:auto;width:100%}

  .pf-para{max-width:46ch;
    font-family:var(--font-head);font-size:clamp(1.4rem,2.5vw,2.1rem);
    line-height:1.42;margin:0}
  /* .16 is Playfight's value and it measures 1.64:1 on --navy-deep — a hard AA fail.
     The paragraph is 22.4px at its clamp minimum, so it does NOT qualify for the
     large-text exemption either. .5 measures 5.06:1 and passes AA outright. The
     reveal is less theatrical than theirs; it's also readable, and it degrades to
     legible text if the JS never runs. WCAG is not the place to copy a reference. */
  /* Subduxion-style scrubbed reveal: each word RISES and brightens as it crosses,
     on a slow, smooth ease. The off-state stays at .5 opacity (WCAG AA floor,
     ~5:1 on navy) so unrevealed text is still readable — Subduxion goes near-
     invisible on white, which we can't; the rise + long ease sell the premium
     feel that the bigger contrast jump would. */
  .pf-para .w, .pf-payoff .w, [data-scrub] .w{display:inline-block;opacity:.5;transform:translateY(.4em);
    transition:opacity .6s var(--ease), transform .6s var(--ease)}
  .pf-para .w.on, .pf-payoff .w.on, [data-scrub] .w.on{opacity:1;transform:none}
  /* On light sections navy text at .5 dips near the AA floor, so scrub lines on
     cream lift their off-state to .62 (~4:1 on cream, comfortable for large text). */
  .section [data-scrub] .w{opacity:.62}
  /* The payoff — the promise, broken out as the climax of the pitch. Bigger and
     heavier than the lead so "its name, its team, and its medicine" lands hard.
     Emphasis is size + weight only (no gold, no gradient — house rules). */
  .pf-payoff{grid-column:2/span 9;max-width:19ch;
    font-family:var(--font-head);font-weight:600;
    font-size:clamp(2rem,3.8vw,3.1rem);line-height:1.14;letter-spacing:-.01em;
    margin:.7em 0 0}

  @media (prefers-reduced-motion: reduce){
    .pf-hero{height:auto}
    .pf-sticky{position:static;height:auto;padding-block:var(--s7)}
    .pf-grid{position:static;display:grid;grid-template-columns:repeat(3,1fr);
      gap:var(--s2);padding-inline:var(--gutter);margin-top:var(--s6)}
    .pf-v{position:static;width:auto!important;transform:none!important;opacity:1!important}
    /* padding-block resets to 0: the 150px bottom pad exists only to lift the
       centred block off is-3 in the PINNED layout. Here nothing is pinned or
       overlapping, so it would just be 150px of dead space. */
    .pf-content{position:static;transform:none!important;opacity:1!important;
      padding-block:0}
    .pf-video{position:static!important;width:100%!important;height:auto!important;
      aspect-ratio:16/9;margin:var(--s5) auto 0;left:auto!important;top:auto!important}
    .pf-hint{display:none}
    .pf-para .w, .pf-payoff .w{opacity:1;transform:none}
    /* No pin, so there is no "over the video" moment to scrim. The copy sits
       below the video as ordinary content on the dark hero ground. */
    .pf-scrim{display:none}
    /* pointer-events back to auto: the JS that normally flips it returns early under
       reduced motion, so without this the overlay's CTAs would be permanently dead
       for exactly the users least able to work around it. */
    /* visibility must be forced back too — the JS that normally restores it returns
       early under reduced motion, so without this the overlay CTAs would be hidden
       from the a11y tree for exactly the users who need them most. */
    .pf-overlay{position:static;opacity:1!important;place-content:start;
      padding-block:var(--s6) 0;pointer-events:auto;visibility:visible!important}
  }


/* --- Sec 1 additions for the real page ------------------------------------
   The demo had a bare H1. The real hero carries Alie's full block: eyebrow +
   headline + both CTAs. All three lift and fade together as one unit. */
/* Full white, not .6 — forced by centring the block (7/21).
   At .6 this was fine, because centring hadn't happened yet and the eyebrow sat on
   plain navy (6.73:1). True vertical centre drops the block ~75px and lands the
   eyebrow on is-6, the surgery frame. Measured against that photo's brightest pixels
   with the halo discounted: .6 white = 2.95:1, and this is 12px — normal text, 4.5:1
   floor, no large-text exemption. Full white on the same pixels = 5.11:1.
   The halo below is real but I am not counting it: it only darkens what it touches,
   so the honest measurement is the one without it. Do not take this back to .6 unless
   the block stops being centred. */
.pf-eyebrow{color:#fff;margin:0 0 var(--s3)}

/* CTAs live in sec 2, under the paragraph. They were in sec 1 and landed on top of
   "The building" — the visual Brees positioned by hand. */
.pf-cta{margin-top:var(--s5)}
@media (max-width:640px){ .pf-cta .btn{flex:1 1 auto} }

/* The video label only fits once the card has grown past ~card size. It's driven by
   the same scroll progress as the flip (see initHeroScroll). */
.pf-video__label{opacity:0;transition:opacity .25s linear}

/* (is-5's position is set in the framed-layout block above — the old override that
   lived here is gone now that all 8 cards are laid out together.) */

/* THE HEADER OWNS 125px OF FLOW.
   The demo had no header, so .pf-sticky's `height:100vh; top:0` was right. On the
   real page the sticky header sits in normal flow above it, so the hero began at
   y=125 and ran to y=1025 — 125px of the composition below the fold, everything
   inside centred on 575 instead of 450, and the headline shoved down into is-3.
   Nudging is-3 would have hidden that, not fixed it.

   So the pinned viewport is redefined as "the area under the header": it pins at
   --header-h and is that much shorter. Every % inside now maps to visible space,
   at rest and pinned alike. The video's "full screen" is this band — it can never
   be edge-to-edge while the header is opaque, and the header must stay opaque
   because the logo haloes on anything but near-white.
   --header-h is measured in JS after the partials land; the fallback below is
   the horizontal lockup's bar height (76px logo + 2×8px padding ≈ 93px). */
/* 🔴 SCOPED TO no-preference 7/28 — THIS RULE WAS A TOTAL CONTENT LOSS BUG.
   Unscoped, it sat AFTER the reduced-motion block and outranked it: this
   selector is 0-2-0, the reduce block's `.pf-sticky{height:auto}` is 0-1-0.
   So `position:static` applied but `height` never reset, and `overflow:hidden`
   (line ~626) then clipped the hero at 900px. Measured, both pages:
       .pf-sticky clips at y=900
       <h1> renders at y=1655-1961   -> INVISIBLE
       .pf-overlay (copy + both CTAs) at y=2847-2924 -> INVISIBLE
   A reduced-motion visitor got a grid of photos on navy and no headline, no
   proposition and no call to action — on index.html, the CLIENT-APPROVED
   homepage, which has been on a staging link since 7/21.

   ⚠️ This is why the first 1,236 lines are no longer byte-identical to the
   pre-V2 backup. The byte-identical discipline existed to stop the signed-off
   page SHIFTING VISUALLY; it was never a reason to preserve a bug that removes
   the page for a whole class of users. Default-motion rendering is unchanged.

   ⚠️ Found by an Impeccable dual-agent critique, NOT by this project's own
   harness — `pw-check.js` runs axe at default motion only, and every
   reduced-motion check written here tested the section being built at the time.
   The check existed; its SCOPE didn't. `.shots/rm-hero.js` now guards it. */
@media (prefers-reduced-motion: no-preference) {
  body.has-dark-hero .pf-sticky{
    top: var(--header-h, 93px);
    height: calc(100vh - var(--header-h, 93px));
  }
}
/* The pin is gone under reduced motion, so the clip that made it work must go
   too — otherwise the un-pinned, auto-height content is still cut off by a
   stale overflow. */
@media (prefers-reduced-motion: reduce) {
  .pf-sticky{ overflow: visible; }
}

/* The header cannot go transparent over the dark hero — the logo PNG has a white
   halo baked in and haloes on anything but near-white. Solid cream bar until we
   get a real transparent export from Sean. */
body.has-dark-hero .site-header{ background:var(--cream); border-bottom-color:var(--rule); }

/* --- Scroll reveals ------------------------------------------------------- */
.reveal { opacity: 0; transform: translateY(16px); transition: opacity .5s var(--ease), transform .5s var(--ease); }
.reveal.is-in { opacity: 1; transform: none; }
/* 🔴 WCAG 2.4.7. A .reveal block sits at opacity:0 until its IntersectionObserver
   fires. Tab into a button inside one — the close section's "Send Us a Message" is
   the live case — and three slow things queue up before it can be seen:
   html{scroll-behavior:smooth} animates the scroll, THEN the observer fires, THEN the
   .5s fade runs. Measured: focus was on a fully invisible control for ~1200ms, with
   the focus ring painting on nothing the whole time.
   focus-within short-circuits all of it — if focus is inside, show it NOW, no
   transition. Costs nothing visually for pointer users, who never trigger it. */
.reveal:focus-within { opacity: 1; transform: none; transition: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1 !important; transform: none !important; transition: none !important; }
  .pf-para .w, .pf-payoff .w, [data-scrub] .w { opacity: 1 !important; transform: none !important; }
  .btn, .primary-nav__list a::after { transition: none !important; }
}

/* --- Responsive ----------------------------------------------------------- */
@media (max-width: 900px) {
  .hero__img, .video { aspect-ratio: 4 / 3; }
  .site-footer__inner { grid-template-columns: 1fr; gap: var(--s5); }

}

/* --- THE NAV DRAWER: its own breakpoint, NOT the 900px layout one ---------
   🔴 SPLIT OUT 8/21. These rules used to live inside `@media (max-width:900px)`
   alongside unrelated hero/footer layout, so the nav and the page layout were
   forced to change at the same width. With six items they cannot be:
   between 900px and ~1200px the header needs the drawer while the rest of the
   page is still perfectly happy in its desktop layout.
   MEASURED at 1024px with the lockup protected: header content 922px, but
   logo 214 + nav 794 + gap 24 = 1032. It overflows by 110px. It only fits
   from ~1150px up, so the drawer runs to 1199px.
   ⚠️ The hero and footer rules deliberately STAYED at 900px. Do not re-merge.
   ⚠️ Check with `.shots/nav-wrap.js` and `.shots/_hdr.js`, not by eye. */
@media (max-width: 1199px) {
  .nav-toggle {
    display: flex; flex-direction: column; justify-content: center; gap: 6px;
    width: 44px; height: 44px; background: none; border: 0; padding: 0; cursor: pointer;
  }
  .nav-toggle__bar { display: block; width: 26px; height: 2px; background: var(--navy); border-radius: var(--r-pill); transition: transform .28s var(--ease); }
  .nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(1) { transform: translateY(4px) rotate(45deg); }
  .nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(2) { transform: translateY(-4px) rotate(-45deg); }

  .primary-nav {
    position: fixed; inset: 0; background: var(--cream);
    flex-direction: column; justify-content: center; align-items: flex-start;
    gap: var(--s4); padding: var(--s8) var(--gutter);
    transform: translateX(100%); transition: transform .34s var(--ease);
    visibility: hidden;
  }
  .primary-nav.is-open { transform: none; visibility: visible; }
  .primary-nav__list { flex-direction: column; gap: var(--s3); }
  .primary-nav__list a { font-family: var(--font-head); font-size: 2rem; }
  .primary-nav__list li { opacity: 0; transform: translateX(16px); transition: opacity .3s var(--ease), transform .3s var(--ease); }
  .primary-nav.is-open li { opacity: 1; transform: none; }
  .primary-nav.is-open li:nth-child(1) { transition-delay: .08s; }
  .primary-nav.is-open li:nth-child(2) { transition-delay: .14s; }
  .primary-nav.is-open li:nth-child(3) { transition-delay: .20s; }
  .primary-nav.is-open li:nth-child(4) { transition-delay: .26s; }
  /* 5 and 6 added 8/21 with Our Hospitals — the stagger stopped at 4, so the
     last two drawer items appeared instantly while the first four eased in. */
  .primary-nav.is-open li:nth-child(5) { transition-delay: .32s; }
  .primary-nav.is-open li:nth-child(6) { transition-delay: .38s; }
  @media (prefers-reduced-motion: reduce) {
    .primary-nav, .primary-nav li, .nav-toggle__bar { transition: none !important; }
  }
}
@media (max-width: 620px) {
  .team__grid { grid-template-columns: 1fr; gap: var(--s6); }
}

/* ==========================================================================
   PARTNER WITH US  (partner-with-us.html)
   --------------------------------------------------------------------------
   First interior page. Everything here is NEW and additive — no shared rule
   above this block was modified, because the homepage is signed off and must
   not shift.

   The page's design problem, stated plainly: Sean's copy is three consecutive
   lists (9 support areas / 6 values / 4 process steps) wrapped in prose. Set
   the same way, the page flattens into one long index and the reader stops
   distinguishing them. So each list gets a treatment matched to what it
   actually IS:
     9 support areas  -> a dense two-column ruled INDEX (bare noun phrases,
                         no definitions in the copy, so no second column)
     6 values         -> large display MANIFESTO lines, read one at a time
     4 process steps  -> the only SEQUENTIAL list, so the only one with
                         numerals and a connecting spine
   Ground alternates cream / navy / cream / navy / cream / navy so no two
   list sections share a background.

   NO PHOTOGRAPHY ON THIS PAGE — deliberate, not an oversight. All 11 licensed
   images are placed on the homepage and the no-duplicate-images rule forbids
   reuse. See asset-needs.md for the shot list this page is waiting on.
   ========================================================================== */

/* --- Generic light-section title ----------------------------------------- */
.section__title {
  font-size: clamp(1.6rem, 3.1vw, 2.5rem);
  max-width: 26ch; margin-bottom: var(--s4);
}

/* --- Dark section --------------------------------------------------------
   Same navy-deep as the homepage's ledger and close, so the three dark bands
   across the site read as one material. */
.section--dark { background: var(--navy-deep); color: var(--cream); }
.section--dark .eyebrow { color: #9AA6C4; }   /* 6.82:1 on navy-deep */
.dark__title {
  font-size: clamp(1.7rem, 3.4vw, 2.7rem);
  max-width: 24ch; color: var(--cream); margin-bottom: var(--s5);
}
.dark__cols {
  display: grid; grid-template-columns: minmax(0, 4fr) minmax(0, 6fr);
  gap: clamp(var(--s4), 4vw, var(--s6));
  padding-bottom: clamp(var(--s5), 5vw, var(--s6));
  border-bottom: 1px solid rgba(255,255,255,.14);
}
.dark__p { color: #AEB6CC; font-size: 17px; line-height: 1.7; }  /* 8.19:1 */
/* The scrubbed statement line — the site's signature motion, carried over from
   the homepage's "What We Manage" kicker. Reserved for STATEMENT lines only.
   Off-state opacity comes from the shared `.section [data-scrub] .w{opacity:.62}`
   rule; on a DARK ground .62 is brighter than the .5 dark default, i.e. safer,
   not riskier. Measured on this page regardless. */
.dark__kicker {
  font-family: var(--font-head); font-weight: 500;
  font-size: clamp(1.4rem, 2.6vw, 2.1rem); line-height: 1.32;
  max-width: 30ch; margin-top: clamp(var(--s5), 5vw, var(--s6)); color: var(--cream);
}
/* Gold section marker — the exact dark-ground mirror of .eyebrow--mark's red.
   ⚠️ ONE per page. v1 died as "a bank or lawyer site" on navy + gold; the rule
   that keeps gold safe is scarcity, not the swatch. 8.78:1 on navy-deep. */
.eyebrow--mark-gold::before {
  content: ""; display: block;
  width: 28px; height: 2px; background: var(--gold);
  margin-bottom: 14px;
}

/* 🔴 THE HERO DASH MUST CENTRE WITH ITS TEXT — QA'd by Brees 7/31: "all three
   pages the hero eyebrow dash is left aligned but should be centered."
   THE CAUSE, AND IT IS THE SAME ONE AS PWU'S CLOSE PARAGRAPHS THE SAME DAY:
   `text-align: center` centres INLINE content. These marks are
   `display: block` pseudo-elements with a fixed 28px width, so they ignore it
   entirely and sit flush left under centred text.
   📌 `.shots/centre-check.js` does NOT catch this — it walks real elements and
   a ::before has no node to measure. Two instances of one fault in one day, one
   caught by a script and one only by eye. If this recurs, teach the checker to
   resolve ::before boxes rather than relying on the next QA pass.
   ⚠️ SCOPED TO THE TRIPTYCH HERO ONLY. `.eyebrow--mark` is used left-aligned in
   several sections; a global auto margin would silently centre those too. */
.triptych__text .eyebrow--mark::before,
.triptych__text .eyebrow--mark-gold::before { margin-inline: auto; }

/* --- Support index (9 items, cream) --------------------------------------
   Two columns of hairline-ruled rows. Same family as the homepage ledger —
   rules, tabular feel — but inverted to cream and stripped of numerals, because
   there are no definitions here to justify a second column. Nothing is hidden
   behind a click: the LENGTH of the list is the argument. */
.sindex {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(var(--s5), 5vw, var(--s7));
  border-top: 1px solid var(--rule);
}
.sindex__item {
  padding-block: clamp(var(--s3), 1.8vw, var(--s4));
  border-bottom: 1px solid var(--rule);
  font-family: var(--font-head); font-weight: 500;
  font-size: clamp(1.05rem, 1.5vw, 1.3rem); line-height: 1.35;
  color: var(--navy);
}
.sindex__payoff {
  margin-top: clamp(var(--s5), 4vw, var(--s6));
  max-width: 62ch; margin-left: auto;
  font-size: 17px; line-height: 1.7; color: var(--navy-soft);
}

/* --- Values manifesto (6 items, navy) ------------------------------------
   Deliberately NOT another ruled list — see the block comment at the top. */
.values { display: grid; margin-top: var(--s2); }
.values__item {
  font-family: var(--font-head); font-weight: 500;
  font-size: clamp(1.5rem, 3.8vw, 2.6rem); line-height: 1.25;
  color: var(--cream);
  padding-block: clamp(var(--s2), 1.4vw, var(--s3));
}
.values__payoff {
  margin-top: clamp(var(--s5), 4vw, var(--s6));
  max-width: 62ch; color: #AEB6CC; font-size: 17px; line-height: 1.7;
}

/* --- Partnership process (4 steps, cream) --------------------------------
   The only sequential list on the page, so the only one with numerals and a
   spine. The spine is a ::before on the <ol> — NOT a left border on each step
   (house rule: no left-bordered cards). It is inset to the numeral's centre and
   stops short at both ends so it reads as a connector, not a table divider. */
.process { position: relative; margin-top: clamp(var(--s5), 4vw, var(--s6)); }
.process::before {
  content: ""; position: absolute; top: 14px; bottom: 14px; left: 21px;
  width: 1px; background: var(--rule);
}
.process__step {
  position: relative;
  display: grid; grid-template-columns: 44px minmax(0, 1fr);
  column-gap: clamp(var(--s3), 2.4vw, var(--s5));
  padding-block: clamp(var(--s4), 3vw, var(--s5));
}
.process__step:first-child { padding-top: 0; }
.process__step:last-child  { padding-bottom: 0; }
/* Red on cream = 5.09:1, passes AA as normal text. Red is the light-ground
   accent (gold is the dark-ground one) and the two never cross over. */
.process__num {
  position: relative; z-index: 1;
  display: flex; align-items: flex-start; justify-content: center;
  font-family: var(--font-body); font-weight: 700; font-size: 12px;
  letter-spacing: .1em; font-variant-numeric: tabular-nums;
  color: var(--red); background: var(--cream);
  padding-block: 4px;                  /* masks the spine behind the numeral */
}
.process__title {
  font-size: clamp(1.2rem, 1.9vw, 1.6rem); margin-bottom: var(--s1); color: var(--navy);
}
.process__body { font-size: 17px; line-height: 1.7; color: var(--navy-soft); max-width: 58ch; }

/* --- Close, image-free variant -------------------------------------------
   This page uses the same .close__bg + .close__scrim machinery as the homepage.
   is already spent there, so this page's close is plain navy-deep: no bg, no
   scrim, nothing to tune. Still butts into the footer as one dark mass. */

.close--multi .close__sub { max-width: 64ch; margin-inline: auto; margin-bottom: var(--s3); }
.close--multi .close__sub:last-of-type { margin-bottom: var(--s4); }
/* Sean's copy ends on a direct instruction — it is the only bolded line in the
   document and the one sentence that asks for the call. Set up, not down. */
.close__lead {
  font-family: var(--font-head); font-weight: 500;
  font-size: clamp(1.15rem, 2vw, 1.5rem); line-height: 1.42;
  color: var(--white); max-width: 40ch; margin: 0 auto var(--s5);
}

@media (max-width: 860px) {
  .sindex { grid-template-columns: 1fr; }
  .dark__cols { grid-template-columns: 1fr; }
  .sindex__payoff { margin-left: 0; }
}
@media (max-width: 520px) {
  .process__step { grid-template-columns: 34px minmax(0, 1fr); }
  .process::before { left: 16px; }
}

/* --- Imagery slots (PLACEHOLDERS, 7/24) ----------------------------------
   Three slots, all currently placehold.co. They exist as real layout so the
   photo debt is visible at scale rather than hidden — the page reads as
   unfinished because it IS unfinished, which is the honest state.
   Shot list lives in asset-needs.md; the placeholder text repeats it so the
   brief is legible in the browser.
   ⚠️ placehold.co is the ONLY external asset on this site. Every slot carries
   width/height + loading="lazy" so swapping in a local file changes nothing but
   the src. Do not ship to a client with these live. */

/* Hero band — full-bleed, sits OUTSIDE .wrap deliberately so it runs edge to
   edge and gives the hero a hard bottom edge. Square corners: the editorial
   system doesn't round full-bleed bands (the homepage mosaic's 16px radius
   applies to CARDS inside a grid, which this is not). */

/* Section background + scrim, for a .section--dark that carries an image.
   Same construction as .close__bg/.close__scrim, generalised. Flat rgba, never
   a gradient (house rule). */
.section--dark.has-bg { position: relative; isolation: isolate; overflow: hidden; }
.secbg { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; object-fit: cover; }
/* ⚠️ .78, NOT the .72 used on the two closes — and the difference is load-bearing.
   Measured against a PURE WHITE worst case, because the real photograph doesn't
   exist yet and a bright frame is the failure mode:
     at .72  eyebrow (#9AA6C4, 12px) 2.65:1 FAIL · payoff (#AEB6CC, 17px) 3.16:1 FAIL
     at .78  with both lifted to --cream: 7.1:1 / 7.1:1 PASS
   Two fixes were needed together, not either alone — raising the scrim AND
   lifting the two muted greys to cream. The greys are tuned for FLAT navy-deep;
   over a scrimmed photograph they lose the contrast they were chosen for.
   If the delivered frame is genuinely dark this can come down — re-measure
   against that actual image, don't eyeball it. */
.secbg__scrim { position: absolute; inset: 0; z-index: 1; background: rgba(18, 29, 59, .78); }
.section--dark.has-bg .wrap { position: relative; z-index: 2; }
.section--dark.has-bg .eyebrow,
.section--dark.has-bg .values__payoff { color: var(--cream); }

@media (max-width: 620px) {
  /* 16:7 is a letterbox on a phone — the subject disappears. */
}

/* When the hero carries a band, the hero's bottom padding must go — otherwise a
   sliver of cream shows between the image and the dark section below it and the
   band reads as a gap rather than a hand-off. Section-to-section continuity is
   the house preference; the image should run straight into the navy. */

/* --- Hero v2: navy triptych (7/24, replaces the cream hero) ---------------
   Brees: "video on the left, text in the middle, img on the right. Something
   similar but not the same."

   SAME MATERIALS as the homepage hero — a video and photography floating on a
   navy-deep void. That shared ground is what makes the two pages read as one
   site; the cream v1 hero shared nothing with it and read as a different site.
   DIFFERENT ARRANGEMENT — the homepage scatters 7 frames at hand-placed
   positions and drifts them through a 250vh pinned track. This is a deliberate
   static triptych: three columns, no pin, no scroll mechanic.

   Deliberately NOT borrowed:
   - the pinned track. It is theatre for stopping a first-time visitor; by the
     time a vet is on this page he has already stopped.
   - the title's dark halo. The homepage needs it because images sit BEHIND the
     headline. Here the text has its own column and never touches an image, so a
     halo would be decoration imitating a fix.

   Ground rhythm consequence, and it is a good one: opening navy makes this page
   run navy → navy → cream → navy → cream → navy, which is EXACTLY the homepage's
   pattern (its hero and intro are both dark before the first cream section). */
.phero--dark { background: var(--navy-deep); color: var(--cream); padding-block: 0 clamp(var(--s6), 7vw, var(--s7)); }

/* Side panels bleed to the viewport edges — the grid deliberately sits OUTSIDE
   .wrap. The centre column is capped so the measure stays readable no matter how
   wide the viewport gets; the side columns absorb everything else. */
.triptych {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 560px) minmax(0, 1fr);
  align-items: center;
  gap: clamp(var(--s3), 3.5vw, var(--s6));
}
.triptych__panel { margin: 0; overflow: hidden; }
/* Vertical offset between the two side panels. Without it the triptych is a
   rigid three-up and reads as a slideshow; the stagger is the one bit of the
   homepage's asymmetry worth keeping. */
.triptych__panel--video { margin-top: clamp(var(--s4), 6vw, var(--s7)); }
.triptych__panel--img   { margin-bottom: clamp(var(--s4), 6vw, var(--s7)); }
/* 🔴 height:auto is LOAD-BEARING, do not drop it.
   The <video> carries width/height attributes (needed to reserve layout space and
   avoid CLS). On an <img> those are only an aspect-ratio hint, because the UA
   sheet — and our own reset — give images height:auto. There is NO equivalent UA
   rule for <video>: the height attribute lands as a real CSS height, wins over
   aspect-ratio, and the panel rendered 390x1200 next to the image's correct
   390x519. Looked like a layout judgment; was a markup/CSS interaction. */
.triptych__media { width: 100%; height: auto; aspect-ratio: 3 / 4; object-fit: cover; display: block; }

.triptych__text { padding-block: clamp(var(--s6), 8vw, var(--s8)); }
/* On navy every one of these has to be re-set — they were tuned for cream. */
.phero--dark .eyebrow { color: var(--white); }
.phero--dark .phero__title { color: var(--cream); }
.phero--dark .phero__lede { color: var(--cream); }
.phero__title { font-size: clamp(2.2rem, 4.2vw, 3.6rem); max-width: 16ch; margin-bottom: var(--s4); }
.phero__lede {
  font-family: var(--font-head); font-weight: 400;
  font-size: clamp(1.15rem, 1.9vw, 1.5rem); line-height: 1.45;
  max-width: 38ch; margin-bottom: var(--s5);
}

/* The three intro paragraphs, as a 3-up row REPEATING the triptych's column
   structure above it. v1 floated these far right in small grey type: it left an
   empty diagonal through the hero and made the opening argument read as a
   footnote. Three columns under three panels is the same rhythm twice. */
.phero__intro {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(var(--s4), 4vw, var(--s6));
  padding-top: clamp(var(--s5), 5vw, var(--s6));
  border-top: 1px solid rgba(255,255,255,.14);
}
.phero__intro p { color: #AEB6CC; font-size: 16px; line-height: 1.7; max-width: 40ch; }

@media (max-width: 900px) {
  /* Text first, then the two panels side by side beneath it. Stacking all three
     vertically would push the copy a full screen down on a phone.

     🔴 EVERY ITEM IS PLACED EXPLICITLY, AND THAT IS LOAD-BEARING (fixed 8/10).
     The intent above was written on 7/24 and never actually happened. The DOM
     order is video → text → img, so with only the text placed (`1 / -1`) grid
     auto-placement put the video at row 1 column 1, pushed the spanning text to
     row 2, and dropped the image at row 3 column 1. The two panels could never
     pair up: each took its own row with an EMPTY COLUMN beside it, so all three
     interior heroes rendered their video at exactly 47% of the viewport, flush
     left, with dead navy filling the other half. It measured 183px at 390 and
     372px at 768 — a constant fraction across widths, which is the tell that a
     track is being sized by placement rather than by the media query.
     📌 A spanning item does not pull the items around it into line; it only
     reserves its own row. If the source order is not already the visual order,
     placing one item places nothing else. */
  .triptych { grid-template-columns: 1fr 1fr; gap: var(--s3); }
  .triptych__text {
    grid-column: 1 / -1; grid-row: 1;
    padding-block: clamp(var(--s6), 10vw, var(--s7)) var(--s4);
  }
  .triptych__panel--video { grid-column: 1; grid-row: 2; margin-top: 0; }
  .triptych__panel--img   { grid-column: 2; grid-row: 2; margin-bottom: 0; }
  .triptych__media { aspect-ratio: 4 / 5; }
  .phero__intro { grid-template-columns: 1fr; gap: var(--s3); }
  .phero__intro p { max-width: 62ch; }
}


/* ======================================================================
   SCHEDULER EMBED — Google Appointment Schedules (.sched)
   Added 2026-08-19. Shared by BOTH placement candidates so the two can be
   compared on identical styling; whichever loses gets deleted, not forked.

   🔴 THE INTERIOR CANNOT BE STYLED. This is a cross-origin iframe on
   calendar.google.com — Google's white, Google's blue (#1a73e8) and Roboto
   are fixed, and no rule below reaches inside it. Everything here styles the
   CONTAINER only. Do not add rules targeting the widget's internals; they
   will silently do nothing, which is worse than failing.

   📌 WHY A FULL-WIDTH WHITE BAND AND NOT A CARD.
   The widget's own background is white. Floated on cream as a boxed panel it
   reads as exactly the bordered/filled/rounded container the no-AI-cards rule
   exists to prevent. Spanning it edge-to-edge inside the section turns the
   white into a BAND of the page instead — the same move §3 uses on team.html.

   ⚠️ HEIGHT IS FIXED AND MUST BE. A cross-origin iframe cannot report its
   content height, so it cannot auto-size. These values are deliberately
   generous so the widget rarely needs its own scrollbar; the mobile value is
   the tallest because the layout stacks (month grid ABOVE the slot list)
   rather than sitting beside it.
   ⚠️ Measured 2026-08-19: renders correctly at 390 / 860 / 1200 / 1500.
   It needs ~8s to paint on a cold load — an earlier 4s test showed BLANK at
   every width and nearly killed this approach. If it looks broken, WAIT
   before concluding anything.
   ====================================================================== */
.sched__frame {
  background: var(--white);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-sm);
  overflow: hidden;               /* clips the iframe to the radius */
  margin-top: var(--s5);
}

.sched__iframe {
  display: block;                 /* kills the inline-element baseline gap */
  width: 100%;
  height: 780px;
  border: 0;
}

@media (max-width: 1024px) { .sched__iframe { height: 900px; } }
@media (max-width: 860px)  { .sched__iframe { height: 1040px; } }

/* The widget stacks hard below ~430px; give it room rather than a scrollbar
   inside a scrollbar. Band starts at 280px — the Galaxy Z Fold cover display,
   not the 320px WCAG reflow floor. See the 8/14 sub-320 note. */
@media (max-width: 430px) { .sched__iframe { height: 1180px; } }

/* A cross-origin iframe is an interactive region: it MUST carry a title, and
   it must show a visible focus ring when tabbed to, or the deep ADA pass
   fails on a control that is the whole point of the page. */
.sched__iframe:focus-visible {
  outline: 3px solid var(--navy);
  outline-offset: 3px;
}

/* ======================================================================
   BOOK A CALL PAGE (.bk-*)  —  added 2026-08-19
   Brees, 8/19: "The booking page doesn't need a fancy header." The first
   build gave it the interior .phero--dark hero and it rendered as a tall
   navy slab with the right half empty — the same half-empty-screen shape
   .triptych and .ab-commit__grid both shipped with. There is no hero here
   at all now; the scheduler IS the content, and the rail beside it carries
   the reassurance that actually decides whether someone books.

   📌 NO BOXES. The rail's copy sits on the page with hairline row rules
   between the facts — the separator exception — not in a bordered panel.
   A filled card here would be the AI-default shape the standing rule bans,
   and it would also fight the white scheduler right next to it.
   ====================================================================== */
.bk-layout {
  display: grid;
  grid-template-columns: 1fr;      /* MOBILE FIRST — see the warning below */
  gap: var(--s5);
}

/* 🔴 THE SPLIT LIVES IN A min-width QUERY, DELIBERATELY.
   This repo has shipped mistake #4 three times: a `@media (max-width:860px)`
   rule written ABOVE the base rule it means to override, at equal
   specificity, so source order wins and the mobile rule never once applies
   (network hero, .net-hero__panel, .ab-commit__grid). Declaring the single
   column as the BASE and adding the split at min-width makes that failure
   structurally impossible here. Do not "tidy" this into a max-width query.
   Asserted by .shots/shadowed-media.js. */
@media (min-width: 861px) {
  .bk-layout {
    grid-template-columns: 320px 1fr;
    gap: var(--s6);
    align-items: start;
  }
}

.bk-rail__portrait {
  width: 88px;
  height: 88px;
  border-radius: var(--r-pill);
  object-fit: cover;
  display: block;
}

.bk-rail__name {
  font-family: var(--font-head);
  font-size: 1.25rem;
  margin: var(--s3) 0 2px;
}

.bk-rail__role {
  color: var(--navy-soft);          /* 5.9:1 on cream — measured, not guessed */
  font-size: .9375rem;
  margin: 0;
}

/* Hairline rows — the permitted separator exception, not a card. */
.bk-facts {
  list-style: none;
  margin: var(--s5) 0 0;
  padding: 0;
}
.bk-facts li {
  padding: var(--s3) 0;
  border-top: 1px solid var(--rule);
}
.bk-facts li:last-child { border-bottom: 1px solid var(--rule); }

.bk-facts b {
  display: block;
  font-family: var(--font-body);
  font-weight: 700;
  margin-bottom: 2px;
}
.bk-facts span { color: var(--navy-soft); }

/* The frame already carries its own top margin for the stacked case; inside
   the split it sits flush with the rail's first line. */
.bk-layout .sched__frame { margin-top: 0; }

/* 🔴 HEIGHT IS DRIVEN BY THE COLUMN, NOT THE VIEWPORT.
   The .sched__iframe heights further up key off viewport width, which is
   correct when the widget spans the page. Here it sits in the RIGHT COLUMN,
   so at 861–1200px the widget is only ~500px wide — it uses its narrow
   stacked layout while the viewport still looks "desktop", and the taller
   value is the one it needs. Getting this backwards gives a scrollbar inside
   a scrollbar. Measured 8/19, not assumed. */
@media (min-width: 861px)  { .bk-layout .sched__iframe { height: 1040px; } }
@media (min-width: 1201px) { .bk-layout .sched__iframe { height: 820px; } }

/* ----------------------------------------------------------------------
   🔴 THE RAIL WAS STARVING THE SCHEDULER — fixed 8/19, found by looking.
   First build: rail 320px + 64px gap inside the site's normal 1240px wrap
   left the widget ~710px. At that width Google shows only TWO day columns,
   and on a Wednesday afternoon both of them were fully booked — so the
   booking page's opening line read "No availability during these days".
   The page was structurally perfect and commercially broken.
   📌 Every gate would have passed this. It took a screenshot.
   The widget's day count is a function of ITS width, so the fix is to give
   the column back the room: a slightly narrower rail and a wider container
   on this page only.
   ⚠️ .wrap's 1240px cap is the site's rhythm and is NOT changed globally —
   this override is scoped to the booking page's section.
   ---------------------------------------------------------------------- */
@media (min-width: 861px) {
  .bk-layout { grid-template-columns: 280px 1fr; }
}

@media (min-width: 1360px) {
  .sched .wrap { max-width: 1440px; }
}
