/* ============================================================
   Hannah Graham Portfolio — Main Stylesheet
   Shared across all pages (index.html, /pages/*.html)

   Contents
   1.  TOKENS — colour, type, spacing
   2.  RESET & BASE
   3.  "THE GRAPH" — shared grid-pattern background
   4.  CUSTOM CURSOR — flower + context pop-up
   5.  BUTTONS
   6.  HEADER / NAVIGATION
   7.  FOOTER
   8.  ABOUT PAGE
   9.  PROJECT PAGE TEMPLATE
   10. LIGHTBOX
   11. HOME PAGE
   12. RESPONSIVE — mobile-first tablet/desktop overrides for everything
       above (cross-component; some components keep their own @media
       blocks colocated with their base rules instead — see 12's own
       intro comment for which is which)
   ============================================================ */

/* =====================================================
   1. TOKENS — colour, type, spacing
   ===================================================== */

:root {
  /* Primary */
  --color-green:        #A2B445;
  --color-green-light:  #BBC48F;
  --color-green-dark:   #667326;

  /* Secondary */
  --color-pink:         #FF007F;
  --color-pink-light:   #FD8EBD;
  --color-pink-dark:    #C90064;

  /* Tertiary */
  --color-yellow:       #FDAF04;
  --color-yellow-light: #E6C374;
  --color-blue:         #B5DFED;

  /* Neutral */
  --color-cream:  #FAF4ED;
  --color-sand:   #E0D9CF;
  --color-forest: #3F4D33;
  --color-dark:   #2C2C2C;
  --color-grid:   #131412; /* base for "the graph" pattern, see section 3 */

  /* Type */
  --font-heading: 'Bricolage Grotesque', sans-serif;
  --font-body:    'Space Grotesk', sans-serif;

  /* Spacing — 12px base unit, per STYLE.md. Defined in rem (not px) so
     spacing scales along with the user's root font-size/browser zoom,
     same reasoning as the rem-based type scale below. */
  --space-1:  0.75rem; /* 12px */
  --space-2:  1.5rem;  /* 24px */
  --space-3:  2.25rem; /* 36px */
  --space-4:  3rem;    /* 48px */
  --space-5:  3.75rem; /* 60px */
  --space-6:  4.5rem;  /* 72px */
  --space-7:  5.25rem; /* 84px */
  --space-8:  6rem;    /* 96px */
  --space-9:  6.75rem; /* 108px */
  --space-10: 7.5rem;  /* 120px */
  --space-11: 8.25rem; /* 132px */
  --space-12: 9rem;    /* 144px — extends past STYLE.md's documented 12-132px scale, added per explicit feedback */

  --max-width: 1200px; /* body content — reverted from 1500px per feedback (that value now only applies to header/footer, see --max-width-wide) */
  --max-width-wide: 1500px; /* header/footer only — see .site-header__inner/.site-footer__top/.site-footer__copy overrides below .container */
}

/* Single source of truth for horizontal alignment. Every section wraps its
   content in exactly ONE element with this class — never split max-width
   and padding across an outer/inner pair, or sections drift out of sync
   with each other at wide viewports. */
.container {
  max-width: var(--max-width);
  margin: 0 auto;
  padding-left: var(--space-2); /* 24px on mobile, per feedback — was 36px. Restored to 36px at tablet+ */
  padding-right: var(--space-2);
}
/* Header/footer opt into the wider cap — everything else using
   .container (body content sections) stays at the narrower default
   above, per feedback splitting the two. */
.site-header__inner,
.site-footer__top,
.site-footer__copy {
  max-width: var(--max-width-wide);
}

/* =====================================================
   2. RESET & BASE
   ===================================================== */

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

html {
  scroll-behavior: smooth;
}

body {
  font-family: var(--font-body);
  background-color: var(--color-cream);
  color: var(--color-dark);
  cursor: none; /* replaced by custom cursor, see section 4 */
  overflow-x: hidden;
  line-height: 1.875; /* Body tier: 30px / 16px, per STYLE.md */
  letter-spacing: -0.03em; /* Body tier: -3%, per STYLE.md */
}

@media (hover: none) {
  /* Touch devices have no real pointer — restore the native cursor/tap targets */
  body { cursor: auto; }
}

a { text-decoration: none; color: inherit; }
img { display: block; max-width: 100%; }
ul { list-style: none; }
button { font: inherit; color: inherit; background: none; border: none; cursor: none; }

h1, h2, h3, h4 { font-family: var(--font-heading); font-weight: 700; line-height: 1.1; }

::selection {
  background-color: rgba(253, 142, 189, 0.3); /* --color-pink-light @ 30%, per STYLE.md — was 100%, made pink text illegible */
  color: var(--color-pink); /* regular pink, per feedback — reverted from the pink-dark pass applied to other light-background text elsewhere on the site */
}

/* Visible focus state for keyboard users — required regardless of the
   custom cursor hiding the native pointer (WCAG 2.1 AA) */
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--color-pink);
  outline-offset: 2px; /* per STYLE.md */
  border-radius: 2px;
}

/* Skip-to-content link — invisible until keyboard-focused, lets keyboard
   users jump past the repeated header/nav straight to <main> instead of
   tabbing through it on every page. */
.skip-link {
  position: absolute;
  top: var(--space-1);
  left: var(--space-1);
  z-index: 1000;
  padding: var(--space-1) var(--space-2);
  background: var(--color-dark);
  color: var(--color-cream);
  font-family: var(--font-body);
  font-size: 0.875rem;
  text-decoration: underline;
  transform: translateY(-150%);
  transition: transform 200ms ease-in-out;
}
.skip-link:focus {
  transform: translateY(0);
}

/* =====================================================
   3. "THE GRAPH" — shared grid-pattern background
   0.5px white stroke, 30px cells, 10% opacity, on --color-grid
   Used behind: hero, footer, About stat section
   ===================================================== */

.bg-graph {
  background-color: var(--color-grid);
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.10) 0.5px, transparent 0.5px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.10) 0.5px, transparent 0.5px);
  background-size: 30px 30px;
  color: var(--color-cream);
}

/* Primary background gradient. Rebuilt using a conic-gradient (per
   assets/css/style.css's .hero__headline, an earlier draft's smooth
   multi-color sweep) instead of layered radial ellipses — a conic
   gradient transitions continuously through every color stop by
   construction, which is what "blended/merging" actually needs; getting
   that from separate blurred blobs meant constant fighting over overlap
   and paint order. The layers live on a ::before (not directly on
   .bg-gradient-primary) so opacity/blur only affect the gradient, not
   the heading text sitting on top of it. */
.bg-gradient-primary {
  position: relative;
  background: var(--color-cream);
  overflow: hidden;
}
/* Two concerns were tangled onto one element before and that's what
   caused the hard line to come back at narrower widths: (1) the
   conic-gradient needs a FIXED height to keep its color-wheel geometry
   (and therefore which colors are visible) consistent across breakpoints
   — .about-hero's real height swings from ~133px (tablet) to ~262px
   (desktop), and conic angles are computed against whatever box they're
   painted on. But (2) the fade-to-transparent MASK has to complete
   before the box's real, breakpoint-correct edge, or overflow:hidden
   clips it mid-fade — a hard line. One fixed-height element can't
   satisfy both at once (at tablet/mobile widths the fixed height is
   taller than the real box, so the mask — sized against that same fixed
   height — hadn't finished fading by the time the real, shorter box
   clipped it). Splitting them across two elements fixes this: this
   outer .hero-gradient-layer div is sized to the REAL box (inset:0) and
   owns the mask + overflow:hidden, so the mask always completes exactly
   at the real edge, whatever that is at any given breakpoint. */
.hero-gradient-layer {
  position: absolute;
  inset: 0;
  overflow: hidden;
  opacity: 0.55;
  mask-image: radial-gradient(ellipse 70% 100% at 50% 0%, black 0%, black 50%, transparent 95%);
  -webkit-mask-image: radial-gradient(ellipse 70% 100% at 50% 0%, black 0%, black 50%, transparent 95%);
  pointer-events: none;
}
/* The inner layer owns the FIXED-height conic-gradient + blur — this is
   what keeps the color-wheel geometry (and therefore which colors show)
   consistent across breakpoints, cropped by the outer layer's
   overflow:hidden+mask above rather than needing to manage its own
   fade-out. */
.hero-gradient-layer::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: clamp(220px, 20vw, 320px);
  /* Blur in vw (clamp()'d) so it scales with viewport size — a fixed px
     blur is proportionally much softer on a big screen than a small one,
     since the hero box scales with the viewport but a fixed radius
     doesn't (this was the bug behind "only visible on the big screen"). */
  filter: blur(clamp(90px, 9vw, 160px));
  /* Center sits at the top edge, so only the bottom half of the wheel is
     ever visible — but with the center at the TOP, increasing angle
     sweeps RIGHT→DOWN→LEFT (90deg=right, 180deg=bottom, 270deg=left), so
     "increasing angle" is actually right-to-left on screen. Stops below
     are written in the order the LEFT-EDGE-to-RIGHT-EDGE sweep needs,
     i.e. decreasing angle from 270deg down to 90deg: cyan (small) → pink
     (largest) → white/sand → yellow → green (small). Cyan isn't at the
     very left edge (270deg itself is green, cyan peaks at 258deg) —
     bookending an edge with cyan reads heavier/cooler; a small green
     edge instead keeps the light/airy feel. */
  background: conic-gradient(from 0deg at 50% 0%,
    #A2B445 90deg,
    #A2B445 100deg,
    #FDAF04 110deg,
    #FDAF04 125deg,
    #E0D9CF 140deg,
    #FF007F 150deg,
    #FF007F 235deg,
    #00E5FF 255deg,
    #00E5FF 265deg,
    #A2B445 270deg,
    #A2B445 290deg,
    #A2B445 360deg
  );
}
/* Light/cream tint over the whole gradient to mute/harmonize it — tried
   as an alternative to just lowering the gradient's own opacity, which
   risks re-flattening it toward invisible the way an earlier pass
   already did once. This keeps every color's relative position/
   visibility intact and instead lightens the whole palette as a group.
   Lives on .bg-gradient-primary directly (not inside .hero-gradient-layer)
   so it's a plain flat tint across the whole section, unaffected by the
   gradient layer's own opacity/mask. */
.bg-gradient-primary::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--color-cream);
  opacity: 0.5;
  pointer-events: none;
}
.about-hero__heading {
  position: relative;
  z-index: 1;
}

/* =====================================================
   4. CUSTOM CURSOR — flower + context pop-up
   ===================================================== */

#cursor-flower {
  position: fixed;
  top: 0;
  left: 0;
  width: 18px; /* per STYLE.md */
  height: 18px;
  pointer-events: none;
  z-index: 9999;
  transform: translate(-50%, -50%);
  transition: width 0.15s ease, height 0.15s ease, opacity 0.15s ease;
}

#cursor-flower img {
  width: 100%;
  height: 100%;
  display: block;
  transition: transform 0.25s ease;
}

body.is-pressing #cursor-flower {
  width: 12px; /* per STYLE.md */
  height: 12px;
  opacity: 0.6;
}

/* Context pop-up: appears when hovering anything with [data-cursor-label].
   Position and entrance-animation are split across two elements on
   purpose — #cursor-context (outer) only ever gets a `translate` set by
   JS on every mousemove, with NO transition/animation of any kind, so it
   cannot ever visibly travel/slide to catch up with the cursor. The pop
   entrance lives entirely on the inner span, which has no position of
   its own — that way the two can never interfere with each other. */
#cursor-context {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9998;
  pointer-events: none;
  opacity: 0; /* instant show/hide, not transitioned */
}
#cursor-context.is-visible {
  opacity: 1;
}

#cursor-context__inner {
  display: block;
  max-width: 230px;
  width: max-content;
  background: var(--color-dark);
  color: var(--color-cream);
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1.3;
  padding: 0.5rem 0.8rem;
  border-radius: 12px;
  box-shadow:
    -3px -3px 0 0 rgba(162, 180, 69, 0.9),
     3px  3px 0 0 rgba(255, 0, 127, 0.9);
  transform-origin: center;
  scale: 0.7; /* pops out from the center on show */
  transition: scale 0.12s ease-out;
}
#cursor-context.is-visible #cursor-context__inner {
  scale: 1;
}

/* Custom text-highlight effect (used inline within copy, e.g. hero headline) */
.text-highlight {
  background-color: rgba(253, 142, 189, 0.3); /* --color-pink-light @ 30% */
  color: var(--color-pink); /* regular pink, per feedback — reverted from the pink-dark pass applied to other light-background text elsewhere on the site */
  padding: 0 0.15em;
}

/* =====================================================
   5. BUTTONS
   ===================================================== */

.btn {
  font-family: var(--font-body);
  font-weight: 500; /* Medium, per STYLE.md */
  font-size: 0.75rem; /* 12px — lowered to match .link-btn, per feedback; STYLE.md still says 16px, hold off updating until confirmed */
  letter-spacing: 0.05em; /* 5%, per STYLE.md */
  text-align: center;
  text-transform: uppercase;
  padding: var(--space-1) var(--space-3); /* 12px vertical / 36px horizontal, per STYLE.md */
  border: 1.5px solid; /* weight 1.5, per STYLE.md */
  border-radius: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  cursor: none;
  user-select: none;
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}

/* Icon indicating what the button/link does — every button/link except
   header nav gets one, on the right/trailing side, per feedback. Sized
   in em so it tracks whatever text size it's placed next to (16px in a
   .btn, smaller in footer/mobile-nav links). */
.icon-copy {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  display: inline-block; /* atomic inline, so ancestor underlines (footer/mobile-nav links) don't draw across it */
}
/* Footer/mobile-nav links aren't flex (they rely on text-decoration:underline,
   which flex would break), so the icon needs manual spacing instead of a
   flex gap. */
.site-footer__links .icon-copy,
.mobile-nav__links-bottom .icon-copy {
  margin-left: 0.35em;
  vertical-align: -0.15em;
}

/* Per STYLE.md: default is outline-only (no fill), hover/clicked both fill
   solid green-light — a previous pass had this filled by default, citing
   a since-superseded reference image; STYLE.md is the current source of
   truth for this variant. */
.btn--green {
  border-color: var(--color-green-light); /* per STYLE.md */
  color: var(--color-forest);
  background: transparent;
}
.btn--green:hover {
  background: var(--color-green-light);
  border-color: var(--color-cream);
  color: var(--color-dark);
  box-shadow: 5px 5px 0 0 var(--color-green);
}
.btn--green:active {
  background: var(--color-green-light);
  border-color: var(--color-green);
  box-shadow: none;
}

/* "Light" variant for use on dark/olive backgrounds (e.g. CTA bars) —
   not in STYLE.md. Default state is its own (cream border/text, since
   .btn--green's forest/green-light default is nearly invisible on this
   dark bg — measured ~1.7:1 contrast), but hover/clicked match
   .btn--green exactly, per feedback. */
.btn--light {
  border-color: var(--color-green); /* per feedback */
  color: var(--color-cream);
  background: transparent;
}
.btn--light:hover {
  background: var(--color-green-light);
  border-color: var(--color-cream);
  color: var(--color-dark);
  box-shadow: 5px 5px 0 0 var(--color-green);
}
.btn--light:active {
  background: var(--color-green-light);
  border-color: var(--color-green);
  box-shadow: none;
}

/* Underlined link-style button, e.g. "MORE ABOUT ME →" / "DOWNLOAD RESUME →" */
.link-btn {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.05em; /* was 0.06em — matched to .btn's 5%, per feedback */
  text-transform: uppercase;
  color: var(--color-green-dark);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: none;
}
/* default -> brighter -> darkest, per the "link button" reference image */
.link-btn:hover { color: var(--color-green); }
.link-btn:active { color: var(--color-forest); }

.link-btn--light {
  color: var(--color-cream);
}
.link-btn--light:hover { color: var(--color-sand); }
.link-btn--light:active { color: var(--color-yellow-light); }

/* --- 404 page: reuses .project-hero/.bg-gradient-primary as-is (same
   full-viewport-minus-header sizing, same left-aligned layout as every
   project page hero) so the footer lands exactly at the bottom of the
   first viewport with no page content after it, and only needs its own
   CTA spacing. --- */
.not-found__cta {
  display: inline-block;
  margin-top: var(--space-3);
}

/* =====================================================
   6. HEADER / NAVIGATION
   ===================================================== */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--color-cream);
  border-bottom: 1px solid var(--color-sand);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: var(--space-2); /* equal padding on all 4 sides, matching the side margin at each breakpoint */
  padding-bottom: var(--space-2);
}

/* Mobile: icon forms its own column, vertically centered against the
   stacked name+tagline column beside it. Desktop reverts to a single
   inline row (natural DOM order) — see the 1024px override below. */
.site-header__brand {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 0.5rem;
}
.site-header__brand strong,
.site-header__brand span {
  font-family: var(--font-heading);
  font-size: 1.05rem;
  line-height: 1.1; /* was inheriting body's 1.875 — caused a big gap between the wrapped name/tagline rows on mobile */
  grid-column: 2;
}
.site-header__brand strong {
  grid-row: 1;
  font-weight: 700;
  letter-spacing: -0.0595em; /* -1px at 1.05rem, expressed as em so it scales with the element's own size */
  color: var(--color-dark);
}
.site-header__brand span {
  grid-row: 2;
  font-weight: 500;
  letter-spacing: -0.0893em; /* -1.5px at 1.05rem */
  color: var(--color-green-dark);
}
.site-header__flower {
  width: 20px;
  height: 20px;
  grid-column: 1;
  grid-row: 1 / 3;
  align-self: center;
}

.site-nav__links {
  display: flex;
  gap: 2rem;
}

.site-nav__links a {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.875rem; /* 14px, per STYLE.md (confirmed smaller than the original 16px reference) */
  letter-spacing: -0.0357em; /* -0.5px at 14px */
  text-transform: uppercase;
  color: var(--color-dark);
  padding: 6px 12px; /* per STYLE.md Nav Links: 12px horizontal / 6px vertical */
  border-radius: 0; /* squared off, per STYLE.md Nav Links */
  border: 1px solid transparent;
  transition: border-color 0.18s ease, background 0.18s ease, color 0.18s ease;
}

.site-nav__links a:hover {
  border-color: var(--color-green); /* per feedback — no fill on hover, stroke is green not green-dark */
}

.site-nav__links a:active {
  background: rgba(162, 180, 69, 0.3); /* --color-green @ 30%, per reference */
  border-color: var(--color-green-dark);
}

.site-nav__links a.is-current {
  color: var(--color-pink-dark); /* dark variant on the light header background, per feedback */
  text-decoration: underline;
  text-underline-offset: 3px;
  border-color: transparent;
}

.hamburger {
  display: none;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
}
.hamburger img { width: 100%; height: 100%; }

/* --- Mobile nav panel --- */
.mobile-nav {
  position: fixed;
  inset: 0;
  z-index: 200;
  visibility: hidden;
  /* Without this, visibility:hidden applies instantly when .is-open is
     removed, hiding the panel before its slide-out transform transition
     ever gets to play — looked like a snap-closed instead of a slide
     back out. Delaying the hide by the same 0.3s as the panel's own
     transition lets the slide finish first; .is-open below zeroes the
     delay so opening still shows instantly. */
  transition: visibility 0s linear 0.3s;
}

.mobile-nav__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(44, 44, 44, 0.2); /* --color-dark @ 20% */
  backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity 0.25s ease;
}

.mobile-nav__panel {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: min(68vw, 360px); /* was 85vw — reference image is 264px of a 390px frame, ~68% */
  background: var(--color-cream);
  padding: var(--space-3) var(--space-2);
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.3s ease;
}

.mobile-nav.is-open {
  visibility: visible;
  transition-delay: 0s; /* show instantly when opening — only closing delays */
}
.mobile-nav.is-open .mobile-nav__backdrop { opacity: 1; }
.mobile-nav.is-open .mobile-nav__panel { transform: translateX(0); }

.mobile-nav__close {
  align-self: flex-end;
  font-size: 1.8rem;
  line-height: 1;
  width: 40px;
  height: 40px;
}

.mobile-nav__brand {
  margin: var(--space-3) 0 var(--space-5);
}
.mobile-nav__brand a {
  display: block;
  margin-left: auto; /* right-aligned with the nav links, per feedback */
  width: max-content;
}
.mobile-nav__brand img {
  width: 160px;
  height: auto;
}

/* All three groups (logo, top links, bottom links) share the same right
   edge. */
.mobile-nav__links-top {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
  margin-bottom: auto;
}
.mobile-nav__links-top a {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.85rem;
  letter-spacing: -0.0368em; /* -0.5px at 0.85rem */
  text-transform: uppercase;
  color: var(--color-dark);
  text-align: right;
}
.mobile-nav__links-top a.is-current { color: var(--color-pink-dark); } /* dark variant on the light mobile-nav panel, per feedback */

.mobile-nav__links-bottom {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
  padding-top: var(--space-3);
}
.mobile-nav__links-bottom a,
.mobile-nav__links-bottom button {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 0.85rem; /* was 0.75rem — matches .mobile-nav__links-top, confirmed same size in the reference */
  letter-spacing: -0.0368em; /* -0.5px at 0.85rem */
  text-transform: uppercase;
  color: var(--color-dark);
  text-align: right;
}

/* =====================================================
   7. FOOTER
   ===================================================== */

.site-footer {
  padding: var(--space-3) 0 var(--space-2);
}

.site-footer__top {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
}

.site-footer__logo {
  width: 150px;
  height: auto;
}

.site-footer__links {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.site-footer__links a,
.site-footer__links button {
  font-family: var(--font-heading); /* switched to Bricolage, per feedback */
  font-weight: 700;
  font-size: 0.75rem; /* 12px — smaller than header nav's 14px, per feedback */
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-green-light);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-align: left;
}
.site-footer__links a:hover,
.site-footer__links button:hover { color: var(--color-pink-light); }

.site-footer__copy {
  position: relative;
  padding-top: var(--space-2);
  font-size: 0.65rem; /* smaller than Labels tier on purpose — fine-print copyright line, quieter than any real content label */
  letter-spacing: 0; /* matches the Labels-tier convention (was its own one-off 0.08em) — the smaller size alone is enough to read as quieter, per feedback */
  text-transform: uppercase;
  color: var(--color-sand);
}
/* Border can't respect padding on its own box (it follows the border-box
   edge), so it's drawn on a pseudo-element inset by the same left/right
   padding as the .container class — otherwise the line runs wider than
   the text content above and below it. */
.site-footer__copy::before {
  content: '';
  position: absolute;
  top: 0;
  left: var(--space-2); /* matches .container's mobile padding — restored to var(--space-3) at tablet+ */
  right: var(--space-2);
  border-top: 1px solid rgba(255, 255, 255, 0.15);
}

@media (min-width: 769px) {
  .site-footer__top {
    flex-direction: row;
    align-items: flex-start;
    justify-content: space-between;
    flex-wrap: wrap;
  }
  .site-footer__links {
    flex-direction: row;
    gap: var(--space-3);
  }
}

/* =====================================================
   8. ABOUT PAGE
   ===================================================== */

/* --- 8a. Hero band ---
   Top: --space-12 (144px), per feedback — space before the "I design
   with purpose..." heading itself, between it and the sticky header.
   No bottom padding — .about-intro right below it now owns that gap via
   its own padding-top (project-page section-spacing standard: the
   entering element owns the gap, generally via padding-top), instead of
   splitting ownership between this section's bottom padding and the
   next one's top padding. */
.about-hero {
  padding: var(--space-12) 0 0;
}

.about-hero__heading {
  font-size: 2rem;
  line-height: 1.15;
  letter-spacing: -0.0417em; /* Hero tier: -3px at 72px, expressed as em so it scales down (not fixed-tight) at mobile sizes */
  color: var(--color-dark);
}

.about-hero__heading .is-pink { color: var(--color-pink-dark); } /* dark variant on the light hero background, per feedback */
.about-hero__heading .is-green { color: var(--color-green-dark); }
.about-hero__heading .hero-break { display: none; }
.project-hero-break { display: none; } /* shown at desktop only, see media query — mobile wraps naturally */

/* --- 8b. Welcome / intro ---
   Mobile: photo + heading share a row, paragraphs run full-width below.
   Tablet/desktop: photo spans both rows, heading+paragraphs together
   beside it (see the grid-template-areas overrides in the responsive
   section). */
.about-intro {
  max-width: var(--max-width);
  margin: 0 auto;
  /* Top: --space-4 (48px), matching project pages' "grouped-top" tier —
     this section is a continuation of the hero's personal-intro topic
     (same relationship as a project-hero flowing into its cover-trio),
     not a fresh topic break, so it uses the smaller grouped value rather
     than a full --space-12 break. Bottom: --space-12 via margin (not
     padding) below, since the section AFTER this one (.about-stats) has
     its own dark background — margin doesn't paint, so this owns the
     exit without tinting the gap the wrong color. */
  padding: var(--space-4) var(--space-2) 0;
  margin-bottom: var(--space-12);
  display: grid;
  grid-template-columns: 110px 1fr;
  grid-template-areas:
    "photo heading"
    "text  text";
  column-gap: var(--space-2);
  row-gap: var(--space-3);
  align-items: start;
}

.about-intro__photo {
  grid-area: photo;
  position: relative;
  box-shadow:
    -6px -6px 0 0 var(--color-green),
     6px  6px 0 0 var(--color-pink);
}
.about-intro__photo img { width: 100%; height: auto; display: block; }

.about-intro__heading {
  grid-area: heading;
  align-self: center;
  font-size: 1.625rem; /* H4 tier: 26px, per STYLE.md — confirmed against about-desktop.jpg, not H2 */
  letter-spacing: -0.0192em; /* -0.5px at 26px, expressed as em per STYLE.md's H4 row */
}
/* Forced break after "Welcome," — mobile/tablet only, where the heading
   column is narrow enough that it wraps anyway; desktop has room to stay
   on one line so the break is hidden there. */
.about-intro__heading br { display: inline; }

.about-intro__copy {
  grid-area: text;
}
.about-intro__copy p + p { margin-top: var(--space-2); }

/* --- 8c. Stat grid (dark, "the graph" background) --- */
.about-stats {
  /* Not part of the topic-break gap — .about-intro's own margin-bottom
     above already owns that (144px). This is purely this dark band's own
     internal breathing room around its content — doubled to --space-8
     (96px) per feedback, so no longer matching .stat-feature's --space-4
     elsewhere. */
  padding: var(--space-8) 0;
}

.about-stats__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* Column gap doubled per feedback (my first pass doubled the wrong
     axis) — the gutter between the left/right cards, not the row gap.
     --space-6 (72px) = --space-3 (36px) doubled. */
  gap: var(--space-5) var(--space-6);
}

.stat-card {
  border-left: 3px solid var(--stat-color, var(--color-green));
  padding-left: var(--space-2);
}
.stat-card h3 {
  /* Heading uses the LIGHT variant of the accent; the border strip keeps
     the strong/saturated version. Blue has no light variant — same value
     for both. */
  color: var(--stat-heading, var(--color-green-light));
  font-size: 1.3rem; /* 20.8px, sized down to match .cta-bar__label ("Any questions?"), per feedback */
  letter-spacing: -0.024em; /* -0.5px at 20.8px */
  margin-bottom: 0.5rem;
}
.stat-card p { font-size: 0.9rem; line-height: 1.6; }

.stat-card--green  { --stat-color: var(--color-green);  --stat-heading: var(--color-green-light); }
.stat-card--pink   { --stat-color: var(--color-pink);   --stat-heading: var(--color-pink-light); }
.stat-card--yellow { --stat-color: var(--color-yellow); --stat-heading: var(--color-yellow-light); }
.stat-card--blue   { --stat-color: var(--color-blue);   --stat-heading: var(--color-blue); }

/* --- 8d. CTA bar --- */
.cta-bar {
  background: var(--color-green-dark);
  color: var(--color-cream);
}
/* Opt-in: the second CTA bar (before the footer) follows a light section
   (.about-contact) and owns its own entrance via margin-top, matching
   .project-cta's identical margin-top:var(--space-12) pattern — margin,
   not padding, since this section has its own distinct background and
   margin doesn't paint, so the true page-background shows through the
   gap. Opt-in (not the base rule) because the FIRST cta-bar follows the
   already-dark .about-stats — applying the same margin there would drop
   a light-page-background stripe between two dark sections, which has no
   established precedent on this site and needs a design call, not an
   assumption; left alone for now. */
.cta-bar--owns-entrance { margin-top: var(--space-12); }
.cta-bar__inner {
  padding-top: var(--space-3);
  padding-bottom: var(--space-3);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
}
.cta-bar__label {
  font-family: var(--font-heading);
  font-size: 1.3rem;
  font-weight: 700;
}
.cta-bar__actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* --- 8e. Outside of working --- */
.fun-facts {
  max-width: var(--max-width);
  margin: 0 auto;
  /* Top: --space-12 (144px) true topic break from the CTA bar above —
     light section entering after a colored one, so it safely owns the
     gap via its own padding-top (the default case, no color-bleed risk).
     Bottom: 0 — .about-contact right after this owns THAT gap instead
     (both light, so ownership is arbitrary; keeping one consistent owner
     — "the entering element" — throughout this page rather than mixing). */
  padding: var(--space-12) var(--space-2) 0;
}

.fun-facts__heading {
  font-size: 2.5rem; /* H2 tier: 40px, per STYLE.md */
  letter-spacing: -0.02em; /* -0.8px at 40px, per STYLE.md's H2 row */
  margin-bottom: var(--space-3);
}

.fun-facts__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

.fun-fact {
  display: flex;
  gap: 0.9rem;
  align-items: center;
}
.fun-fact img { width: 32px; height: 32px; flex-shrink: 0; }
.fun-fact__label {
  font-size: 0.875rem; /* Labels tier: 14px, per STYLE.md */
  font-weight: 440;
  letter-spacing: 0;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--color-green-dark);
  margin-bottom: 0.2rem;
}
.fun-fact__answer {
  font-size: 0.875rem; /* Body SM tier: 14px, per STYLE.md — closest match, not a named scale row */
  font-weight: 400;
  line-height: 1.4;
}

/* Mosaic layout — matches the reference mockups exactly per breakpoint,
   not a generic masonry algorithm. Mobile/tablet: 3 columns, 3 rows.
   Desktop: 6 columns, 2 rows (paris and blossoms each span the full
   height of one column; storr spans 2 columns AND both rows, forming a
   square exactly 4 unit-cells big; muffins/jump and lumineers/cafe each
   split their own column into two stacked squares).
   Every unit cell is a true square (aspect-ratio on the whole grid, evenly
   split across equal-fr tracks) so spanning cells come out exactly 2:1
   (or 2:2 for storr) in their spanning direction. min-width/min-height: 0
   on the images is required — grid items default to min-height:auto, so a
   tall source photo would otherwise force its row taller than the square
   track instead of being cropped by object-fit, which was causing the
   distorted/cropped-looking cells. */
.photo-strip {
  display: grid;
  gap: 0; /* flush on mobile, per feedback — restored at tablet+ */
  aspect-ratio: 1 / 1;
  /* Flush to the screen edges on mobile — cancels the parent .fun-facts
     padding. Reset back to inset at tablet+ (see media query below). */
  margin-left: calc(-1 * var(--space-2));
  margin-right: calc(-1 * var(--space-2));
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  grid-template-areas:
    "muffins   jump      blossoms"
    "storr     storr     cafe"
    "lumineers paris     paris";
}

.photo-strip img {
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  object-fit: cover;
  overflow: hidden;
  cursor: none;
}

.photo-strip__paris     { grid-area: blossoms; } /* swapped with blossoms, per feedback */
.photo-strip__muffins   { grid-area: muffins; }
.photo-strip__jump      { grid-area: jump; }
.photo-strip__storr     { grid-area: storr; }
.photo-strip__lumineers { grid-area: lumineers; }
.photo-strip__cafe      { grid-area: cafe; }
.photo-strip__blossoms  { grid-area: paris; } /* swapped with paris, per feedback */

/* --- 8f. Contact section --- */
.about-contact {
  max-width: var(--max-width);
  margin: 0 auto;
  /* Top: --space-12 (144px) true topic break from .fun-facts above (both
     light, entering element owns via padding-top, per the page's
     consistent ownership rule). Bottom stays --space-5 — the CTA bar
     right after this owns ITS OWN entrance via margin-top (below), so
     this bottom value is just this section's own internal floor, not
     part of that gap. */
  padding: var(--space-12) var(--space-2) var(--space-5);
  min-height: 62vh; /* slightly reduced on mobile so this section + the CTA bar below both fit in one screen; restored to 70vh at tablet+ */
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.about-contact__eyebrow {
  font-size: 1rem; /* Eyebrow tier: 16px, per STYLE.md */
  font-weight: 600; /* Eyebrow tier: Space Grotesk Semibold, per STYLE.md */
  letter-spacing: 0.0625em; /* +1px at 16px, per STYLE.md's Eyebrow row */
  text-transform: uppercase;
  color: var(--color-forest);
  margin-bottom: var(--space-2);
}
.about-contact__heading {
  font-size: clamp(2rem, 7vw, 4.5rem);
  line-height: 1.1;
  margin-bottom: var(--space-3);
}
.about-contact__heading .is-email,
.about-contact__heading .is-linkedin {
  cursor: none;
  text-decoration: underline;
  text-underline-offset: 4px;
  transition: opacity 0.18s ease;
}
.about-contact__heading .is-email:hover,
.about-contact__heading .is-linkedin:hover {
  opacity: 0.85; /* -15%, keeps the same hue instead of swapping to the lighter token (too washed out to read) */
}
.about-contact__heading .is-email { color: var(--color-pink-dark); } /* dark variant on the light page background, per feedback */
.about-contact__heading .is-linkedin { color: var(--color-green-dark); }
.about-contact > p:last-child { font-size: 1rem; } /* Body tier: 16px, per STYLE.md */

/* =====================================================
   9. PROJECT PAGE TEMPLATE
   Shared across all individual project pages (sexual-health-campaign,
   mental-health-campaign, myhealthnb-launch, breastfeeding-guide) — built
   once here on sexual-health-campaign.html, meant to be copy-and-fill for
   the rest per CLAUDE.md's "share a common template" instruction. Generic
   class names on purpose, not page-specific ones.
   ===================================================== */

/* --- 9a. Eyebrow (section label) ---
   Reused for the hero's category label and each body section's
   OVERVIEW/OUTCOME/APPROACH-style label. The icon variant prepends the
   same pink flower mark used elsewhere on the site (header brand, cursor)
   — per the reference, OVERVIEW/OUTCOME/APPROACH get the icon but REAL
   WORLD CONDITIONS doesn't, so it's an opt-in modifier, not the default. */
.project-eyebrow {
  font-size: 1rem; /* Eyebrow tier: 16px, per STYLE.md */
  font-weight: 600;
  /* Inheriting body's 1.875 line-height gave this a lot of invisible
     leading above/below the glyphs (30px line box around 16px text) —
     that's what read as "not flush" against an adjacent image's hard
     edge, even though the box itself was already top-aligned. */
  line-height: 1.2;
  letter-spacing: 0.0625em; /* +1px at 16px */
  text-transform: uppercase;
  color: var(--color-green-dark);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: var(--space-2);
}
.project-eyebrow--icon::before {
  content: '';
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  background: url('/assets/global/flower-pink.svg') center / contain no-repeat;
}

/* --- 9b. Hero --- */
.project-hero {
  /* Fills the viewport (100dvh, falling back to 100vh) and centers its
     content vertically via flex — content stays left-aligned since flex
     only affects the cross-axis position of the block, not its own
     text-align. Padding stays symmetric (not the usual "next section
     owns the gap" pattern) because the generous whitespace above/below
     the centered content already comes from the vh-vs-content-height
     difference; padding here is just a floor for short/landscape
     viewports where content nearly fills the box.
     Subtracts the sticky header's real rendered height (93px below
     1024px, 67px at 1024px+ — see .vertical-carousel for the same
     measurement) so the hero fills exactly the space visible on load,
     not the full screen height plus whatever the header covers. */
  min-height: calc(100vh - 93px);
  min-height: calc(100dvh - 93px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--space-5) 0;
}
.project-hero__inner {
  position: relative;
  z-index: 1;
  /* .project-hero is display:flex (column), and .container's own
     margin:0 auto sets auto margins on the cross axis (left/right, in a
     column flex) — per the flexbox spec, auto cross-margins absorb
     align-items:stretch instead of letting the item stretch, so without
     an explicit width this shrank to whatever this page's OWN content
     needed (a shorter tools/meta list = a narrower box) instead of
     always filling up to .container's 1200px cap like every other use
     of .container on the site. Confirmed as the cause of
     breastfeeding-guide's hero reading narrower than the other project
     pages, per feedback — width:100% forces the intended fill-to-cap
     behavior regardless of this page's own content length. */
  width: 100%;
}
.project-hero__heading {
  /* Hero tier, per feedback — same scale as .about-hero__heading, not
     Heading 1. Base is mobile-sized; desktop override lives in the
     1024px+ media query alongside .about-hero__heading's. */
  font-size: 2rem;
  line-height: 1.15;
  letter-spacing: -0.0417em; /* Hero tier: -3px at 72px, expressed as em off the desktop size */
  color: var(--color-dark);
  margin-bottom: var(--space-2);
}
/* Optional second line for a two-part hero title (e.g. "Every Drop
   Counts:" / "A Breastfeeding Guide for You and Your Family") — sized in
   em off the parent heading's own font-size, so it scales at both
   breakpoints automatically instead of needing its own media query. */
.project-hero__heading-sub {
  display: block;
  font-size: 0.65em;
  margin-top: 0.2em;
}
.project-hero__tagline {
  font-size: 1.25rem; /* Body LG tier: 20px, per STYLE.md */
  line-height: 1.4;
  /* no max-width — should only wrap when the heading above it does, not independently */
}

/* --- 9c. Meta row (ROLE / COMPANY / TOOLS / YEAR) --- */
.project-meta {
  margin-top: var(--space-6);
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
}
.project-meta__item {
  padding-top: var(--space-1);
  border-top: 1px solid var(--color-sand);
}
.project-meta__item dt {
  font-size: 0.875rem; /* Labels tier: 14px */
  font-weight: 440;
  text-transform: uppercase;
  color: var(--color-dark);
  margin-bottom: 0.35rem;
}
.project-meta__item dd {
  font-size: 0.875rem;
  font-weight: 440;
  text-transform: uppercase;
  color: var(--color-green-dark);
}

/* --- 9d. Generic image gallery ---
   No object-fit/aspect-ratio cropping — every image here was sized
   intentionally, so it renders at its own natural aspect ratio, full
   image visible, per feedback. align-items:start stops grid's default
   stretch from forcing a shared row height (which would otherwise crop
   shorter/taller images against their neighbours). */
.project-gallery {
  display: grid;
  align-items: start;
  gap: var(--space-1);
  margin: var(--space-4) 0;
}
.project-gallery img {
  width: 100%;
  height: auto;
  display: block;
  cursor: none;
}
.project-gallery--pair { grid-template-columns: 1fr 1fr; }
.project-gallery--trio { grid-template-columns: 1fr; }
/* Opt-in exception to the "no object-fit" default above — this specific
   image (a screenshotted LinkedIn post) runs taller than its two trio
   siblings (print banner mockups), which threw its caption's baseline
   below theirs. Cropped to the siblings' own aspect ratio (2161:2701),
   trimming from the bottom (post's like/comment/repost bar) so the
   header and group photo stay intact. */
.project-gallery img.is-height-matched {
  aspect-ratio: 2161 / 2701;
  object-fit: cover;
  object-position: top;
}
/* The last gallery before a true section break (currently: digital ads,
   right before the closing CTA) needs its own trailing margin zeroed —
   the CTA's padding-top owns that gap instead, per the spacing standard
   on .project-section. Its margin-top stays at the normal 48px "keep
   together" rhythm — the bigger --space-12 gap belongs at the OUTER edges
   of this whole three-gallery group (before the first one, after the
   last one), not between the galleries themselves. See
   .project-gallery--first below for the entering side. */
.project-gallery--last { margin-bottom: 0; }

/* The first gallery in Approach's three-gallery group needs the full
   --space-12 gap above it (this is where the group visually starts,
   coming off the heading/list copy) instead of the default 48px — per
   feedback, the bigger gap belongs at the outer edges of the group only,
   with the 48px rhythm unchanged between the three galleries themselves. */
.project-gallery--first { margin-top: var(--space-12); }

/* Opt-in: cancels .container's side padding so the gallery runs edge to
   edge on mobile only, resetting to the normal inset at tablet+ — same
   breakout technique as .video-carousel__track, just scoped to mobile
   alone instead of mobile+tablet. */
.project-gallery--bleed-mobile {
  margin-left: calc(-1 * var(--space-2));
  margin-right: calc(-1 * var(--space-2));
}
@media (min-width: 769px) {
  .project-gallery--bleed-mobile { margin-left: 0; margin-right: 0; }
}

/* Cover gallery: mobile keeps the bespoke mosaic (posters full-width on
   top, socials+bus split below) — the "stay in one row" feedback was
   specifically for tablet/desktop, not mobile, per follow-up
   clarification. Row layout kicks in at 769px+ (see media query). */
.project-gallery--cover {
  /* No margin of its own — sits inside the .project-block wrapper, which
     owns the section-start gap via padding-top instead (see the spacing
     standard on .project-section). A margin here would add on top of
     that, not replace it. */
  margin: 0;
  grid-template-columns: 1fr 1fr;
  grid-template-areas:
    "posters posters"
    "bus socials";
}
.project-gallery--cover__socials { grid-area: socials; }
.project-gallery--cover__bus     { grid-area: bus; }
.project-gallery--cover__posters { grid-area: posters; }

/* --- 9e. Section wrapper (Overview / Outcome / Real World / Approach) --- */
.project-section {
  /* top/bottom only — this element also carries .container in markup,
     and a shorthand `padding: X 0` here would silently zero out
     .container's own left/right padding (later in the cascade wins),
     which is exactly what was pushing every eyebrow/heading flush to
     the viewport edge on mobile. */
  /*
     SECTION-SPACING STANDARD (project pages):
     Every true topic-to-topic break gets exactly --space-12 (144px), and
     that gap is owned by ONE side only — the entering section's own
     padding-top. The preceding element contributes 0 on its own bottom
     edge for this purpose. This is deliberate: padding never collapses
     the way adjacent margins do, so as long as exactly one side
     declares the real value and the other is 0, the total is always
     exactly 144px, never 144+something. Never split a section gap across
     both sides' padding, and never use margin for it — margin/padding
     boundaries stacking (or two independent margins colliding) is what
     produced the uneven 48/60/0 mix and the double-spacing bugs found
     this session (e.g. .project-gallery-band, the heading+paragraph
     margin-collapse bug).

     Content that belongs together *within* one topic (e.g. Approach's
     heading+list and its own supporting galleries) intentionally stays
     OFF this standard and uses the smaller, already-consistent 48px
     rhythm from .project-gallery's own margin instead — that's a
     separate, tighter tier by design, not an oversight.

     .project-block (below) is the equivalent for bare, non-.project-section
     wrappers (image-only blocks like the cover gallery and the Real
     World Conditions grid) that also mark a true section start. */
  padding-top: var(--space-12);
  padding-bottom: 0;
}

/* Two opt-in exceptions to the standard above, kept as generic modifiers
   rather than hardcoded to any one section name — .project-section--overview
   etc. are shared class names across every project page (per the reusable
   template), so a page-specific exception hardcoded to one of those names
   would silently leak onto every other page reusing it. Apply these only
   on the page/section that actually needs them. */
/* For a section whose own colour/texture would tint the entering gap if
   IT owned the standard padding-top (e.g. a section right before a dark
   or fixed-height block that can't take padding without the gap reading
   as "more of that block's colour" instead of empty space) — the PRECEDING
   section owns the gap instead, on its outgoing side. */
.project-section--owns-exit { padding-bottom: var(--space-12); }
/* For a section that reads as one group with the content directly above
   it (not a fresh topic) — uses the tighter 48px "keep together" rhythm
   on its entering side instead of the standard gap. */
.project-section--grouped-top { padding-top: var(--space-4); }

/* Section-start padding for bare (non-.project-section) content blocks —
   see the standard documented on .project-section above. */
.project-block {
  padding-top: var(--space-12);
}
/* Opt-in: cancels the standard gap for a .project-block that sits
   directly below a full-viewport hero (.project-hero, min-height:100vh)
   — the hero itself already fills the screen, so the extra 144px on top
   of that was compounding into a huge stretch of blank scrolling before
   any real content appeared. Scoped to specific instances rather than
   the shared .project-block default, which other pages still rely on. */
.project-block--flush-hero { padding-top: 0; }
.project-section__copy p + p {
  margin-top: var(--space-2);
}
.project-section__heading {
  font-size: 1.625rem; /* H4 tier: 26px — matches About's heading treatment for section titles */
  letter-spacing: -0.0192em;
  margin-bottom: var(--space-1); /* 12px, reduced from --space-2/24px per feedback (Approach's heading-to-body gap) */
}

/* Mobile/tablet only: the section's own intro paragraph(s) — direct
   children of .project-section__copy — sit indented ~48px past the
   container edge, while the eyebrow label and any heading stay flush at
   the edge, per the mobile reference. The child combinator (>) is
   deliberate: it's what keeps this from also indenting paragraphs nested
   deeper inside .project-outcome__stats, .project-quote, or
   .project-approach__list (their own descriptive <p> tags) — those
   should NOT get this indent at all, per feedback, since it broke their
   own internal number/heading/paragraph alignment. Cancelled at tablet+
   (769px) once the grid's own 220px eyebrow column provides the offset
   instead (see media query, selector kept identical there so specificity
   matches and the override actually wins). */
.project-section__copy > p:not(.project-eyebrow) {
  padding-left: 3rem; /* ~48px */
}
/* Exception: a paragraph immediately following the section's own heading
   (e.g. Approach's "Make them do a double take.") sits flush with that
   heading's left edge instead of getting the extra indent — the indent
   is only meant for copy that follows the eyebrow alone, with no heading
   in between (Overview/Outcome's intro paragraphs). Selector specificity
   matches the base rule above (2 class-level selectors + 1 type) so this
   correctly wins by source order rather than needing !important. */
.project-section__copy > .project-section__heading + p {
  padding-left: 0;
}
/* Body copy colour, per feedback — forest, not the inherited body
   default (--color-dark). Direct-child combinator (not a plain
   descendant selector) for the same reason as the padding-left rule
   above: .project-outcome__stats and .project-quote both nest deeper
   inside .project-section__copy and carry their own explicit colour
   treatment (stat text, quote text) — callouts/stats are excluded from
   this, per feedback, and this selector shape excludes them
   structurally rather than needing an override after the fact. */
.project-section__copy > p:not(.project-eyebrow) {
  color: var(--color-forest);
}

/* Real World Conditions: a dedicated 2-column x 2-row grid (not the
   shared eyebrow-column .project-section pattern — this section's layout
   is genuinely different, not a variant of it). Top-left is the copy,
   top-right is the winter-photo pair (itself split 50/50), bottom-left
   and bottom-right are the two colour-block bus images. One uniform gap
   in both directions — matching the winter-photo pair's own internal
   gap (--space-1/12px) so the whole 2x2 block, including the nested
   split, reads as a single consistent mosaic. Mobile stacks all four
   pieces in one column; the 2x2 arrangement only applies at tablet+. */
.realworld-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-1);
  /* No margin of its own — same reasoning as .project-gallery--cover
     above: its .project-block wrapper owns the section-start gap. */
  margin: 0;
}
.realworld-grid__winter {
  margin: 0; /* cancel .project-gallery's own margin — the grid's own gap handles spacing here */
}
/* Tighter heading-to-paragraph gap than the shared .project-section__heading
   default (--space-2/24px) — this copy block needs to fit inside the row
   height set by the winter-photo pair beside it, per feedback. Scoped to
   this section only, not a global change to .project-section__heading. */
.realworld-grid__copy .project-section__heading {
  margin-bottom: var(--space-1);
}
/* Body copy colour, per feedback — same forest treatment and
   :not(.project-eyebrow) exclusion as .project-section__copy above (this
   grid doesn't use that wrapper class, so it needs its own copy of the
   rule); no stats/quote nest in here so no child-combinator is needed. */
.realworld-grid__copy p:not(.project-eyebrow) {
  color: var(--color-forest);
}
.realworld-grid__busfr,
.realworld-grid__busen {
  width: 100%;
  height: auto;
  display: block;
  cursor: none;
}

/* --- 9f. Outcome stats --- */
.project-outcome__stats {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
  margin-top: var(--space-4); /* 48px, increased from --space-3/36px per feedback */
  /* Left-only (not centered) so this lines up with the indented copy
     paragraph above it on mobile/tablet, per feedback — reset at 769px+
     alongside the general paragraph indent, once the grid column split
     provides that offset instead. */
  margin-left: 3rem;
}
/* Side-by-side from 426px up — a deliberately custom breakpoint (not one
   of the site's standard mobile/tablet/desktop ones) per feedback,
   specific to this component. */
@media (min-width: 426px) {
  .project-outcome__stats {
    grid-template-columns: 1fr 1fr;
  }
}
/* Three-stat variant (mental-health-campaign's 14% / AA / EN-FR row) —
   stays single-column until the standard tablet+ breakpoint since three
   columns get cramped at the 426px point the two-stat variant uses. */
@media (min-width: 769px) {
  .project-outcome__stats--trio {
    grid-template-columns: 1fr 1fr 1fr;
  }
}
.project-outcome__stat {
  border-left: 3px solid var(--stat-color, var(--color-green));
  padding-left: var(--space-2);
}
.project-outcome__stat h3 {
  font-size: 2rem;
  letter-spacing: -0.02em;
  /* --stat-text-color lets a variant use a different shade for the text
     than the border (e.g. --olive below) — falls back to the border's
     own colour for variants that don't set it, so --green/--pink are
     unaffected. */
  color: var(--stat-text-color, var(--stat-color, var(--color-green)));
  margin-bottom: 0.25rem;
}
.project-outcome__stat p { font-size: 0.9rem; }
.project-outcome__stat--green { --stat-color: var(--color-green-dark); }
/* Border/shape stays --color-pink; text explicitly overridden to
   --color-pink-dark (better contrast on the light page background,
   per feedback — pink text specifically, borders/shapes untouched). */
.project-outcome__stat--pink  { --stat-color: var(--color-pink); --stat-text-color: var(--color-pink-dark); }
.project-outcome__stat--olive {
  --stat-color: var(--color-green);
  --stat-text-color: var(--color-forest);
}

/* --- 9g. Quote card ---
   Same offset-shadow language as .about-intro__photo (green top-left,
   pink bottom-right) rather than a new visual idea. */
.project-quote {
  background: var(--color-sand);
  padding: var(--space-3);
  margin-top: var(--space-5); /* 60px, increased from --space-4/48px per feedback */
  /* Left-only, matching .project-outcome__stats — lines up with the
     indented copy paragraph above it on mobile/tablet, per feedback.
     Reset at 769px+ alongside the same paragraph indent. */
  margin-left: 3rem;
  box-shadow:
    -6px -6px 0 0 var(--color-green),
     6px  6px 0 0 var(--color-pink);
}
.project-quote p {
  font-size: 1.25rem;
  line-height: 1.4;
  color: var(--color-dark);
}
.project-quote cite {
  display: block;
  margin-top: var(--space-1);
  font-style: normal;
  font-size: 0.875rem;
  color: var(--color-dark); /* per feedback */
}

/* --- 9h. Approach numbered list --- */
.project-approach__list {
  margin-top: var(--space-3);
}
.project-approach__list li {
  display: grid;
  grid-template-columns: 2.5rem 1fr;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: 1px dashed var(--color-sand);
}
.project-approach__list li:first-child { padding-top: 0; }
.project-approach__number {
  font-size: 0.875rem;
  font-weight: 700;
  color: var(--color-pink-dark); /* dark variant on the light page background, per feedback */
}
.project-approach__list h3 {
  font-size: 1.3rem;
  letter-spacing: -0.024em;
  margin-bottom: 0.5rem;
}
.project-approach__list p { font-size: 1rem; line-height: 1.6; color: var(--color-forest); } /* body copy: 16px, per feedback — headings untouched */
.project-approach__list a {
  color: var(--color-green-dark);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: none;
}
.project-approach__list a:hover { color: var(--color-green); }

/* --- 9i. Full-bleed sand band for the palette/pattern/working-file gallery --- */
.project-gallery-band {
  background: var(--color-sand);
  padding: var(--space-4) 0;
  margin: var(--space-4) 0;
}
.project-gallery-band .project-gallery { margin: 0; }
/* Same "outer edge of a group gets the standard gap, everything between
   stays at the tighter margin" pattern as .project-gallery--first/--last,
   for bands that open/close a sequence rather than sitting in the middle
   of one. margin (not padding) is what makes this safe on a band that
   has its own background colour — margin never paints, so it doesn't
   matter which side declares it, unlike the video-carousel's padding
   exception elsewhere on this page. */
.project-gallery-band--first { margin-top: var(--space-12); }
.project-gallery-band--last { margin-bottom: 0; }
.project-gallery__caption {
  margin-top: var(--space-1);
  font-size: 0.875rem; /* Labels tier: 14px */
  font-weight: 440;
  text-transform: uppercase;
  color: var(--color-dark);
}
/* .project-gallery's own margin-bottom (48px, from the base rule) was
   stacking with this caption's margin-top, pushing the caption far below
   the image instead of hugging it. Zero it whenever a caption directly
   follows, so the caption's own 12px margin-top is the only gap — self-
   maintaining wherever this image+caption pattern gets reused. */
.project-gallery:has(+ .project-gallery__caption) { margin-bottom: 0; }

/* --- 9i-2. Site-wide video UI ---
   Standard interaction pattern for every video on every project page:
   controls are hidden by default (ambient autoplay/loop reads cleaner
   without a permanently-visible seek bar) and only shown on hover, and a
   transient play/pause icon flashes centre-screen on every real state
   change. See main.js's generic .video-ui handler — it works on any
   wrapper with this class, so new videos opt in just by using this
   markup, no per-page JS needed. */
.video-ui {
  position: relative;
  cursor: none;
}
.video-ui__flash {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: rgba(44, 44, 44, 0.55);
  color: var(--color-cream);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease;
}
.video-ui__flash.is-visible {
  opacity: 1;
  transition: opacity 0.1s ease;
}
.video-ui__flash svg { width: 28px; height: 28px; }
.video-ui__flash .icon-play { margin-left: 3px; } /* optically centre the triangle */

/* --- 9j. Video carousel ---
   Spans the full viewport height per feedback (100dvh — adjusts to
   whatever device it's rendered on, falling back to 100vh in browsers
   that don't support dvh). Slides sit side by side in
   .video-carousel__slides and the whole strip translates on navigation
   (300ms ease-in-out, the "larger movements" timing per STYLE.md's
   Animation Timing section — same class of transition as the mobile-nav
   slide-in), rather than swapping display:none, so the sliding motion is
   an actual animation instead of a hard cut.

   Layout is a grid (not flex) so the arrows/dots can be freely
   repositioned per breakpoint via grid-template-areas without touching
   DOM order: mobile stacks the track on top with prev/dots/next below it
   (full width for the video); desktop flanks the track with the arrows,
   dots centered underneath, matching the original two-arrow layout. */
.video-carousel {
  /* Matches .vertical-carousel exactly (see there for the full reasoning):
     min-height, not a hard height, on mobile — a fixed height plus the
     grid's 1fr track risked the same overflow/broken-layout failure mode
     there (1fr's content min-size winning over the intended shrink). Floor
     of viewport-minus-header, free to grow if content needs more; the
     .video-carousel__viewport padding-top/bottom below reserve the visible
     dark-grid space above/below the video content. Tablet+ keeps a hard
     height (see overrides below) since content comfortably fits either way.
     display:flex with DEFAULT align-items (stretch), not align-items:center
     — stretch sizes the single flex child (.video-carousel__viewport)
     structurally to fill/grow with the container. align-items:center
     instead relies on the child's height:100% resolving as a percentage,
     which needs the container's height to be "definite" — min-height alone
     never counts as definite per spec, so with center the child silently
     fell back to content-sizing and got mis-centered as a shorter box
     (confirmed via testing: broke the fixed-px padding math below).
     position:absolute;inset:0 on the child was tried too, but that makes
     the child unable to grow the min-height'd parent past its floor at
     all (out-of-flow elements don't contribute to containing-block sizing)
     — reintroducing the exact "tall content gets clipped" bug this min-
     height/growable approach exists to prevent. Stretch is the only one
     of the three that gets both properties right. */
  min-height: calc(100vh - 93px);
  min-height: calc(100dvh - 93px);
  padding: 0;
  display: flex;
  overflow: hidden;
}
.video-carousel__viewport {
  width: 100%;
  /* No explicit height — align-items:stretch (the flex default on the
     parent) only stretches a child whose own height is auto; setting a
     percentage here would override stretch and reintroduce the same
     indeterminate-percentage problem stretch exists to avoid. */
  display: grid;
  grid-template-columns: auto 1fr auto;
  /* Matches .vertical-carousel__viewport's pattern (breastfeeding-guide):
     track fills remaining space and centers within it (align-items:center
     on a 1fr row taller than the track's own content does this
     automatically); prev/dots/next hugs the bottom via padding-bottom
     instead of a fixed row-gap, so the gap between video and controls
     scales with viewport height instead of staying constant. */
  grid-template-rows: 1fr auto;
  grid-template-areas:
    "track track track"
    "prev  dots  next";
  padding-bottom: var(--space-4); /* 48px from the bottom edge of the section */
  align-items: center;
}
/* Mobile only — matches .vertical-carousel__viewport exactly: tablet+
   already has generous natural top space from the 1fr row, mobile needs
   an explicit reservation so the dark grid texture is visible above the
   video instead of it sitting flush under the header. 24px (--space-2) on
   both edges, not 48px — matches .container's own 24px mobile side
   padding, so the carousel reads as a consistent frame on all four sides
   instead of a heavier top/bottom than left/right. */
@media (max-width: 768px) {
  .video-carousel__viewport {
    padding-top: var(--space-2);
    padding-bottom: var(--space-2);
  }
}
.video-carousel__arrow {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  color: var(--color-green-light);
  cursor: none;
  transition: color 0.18s ease;
}
.video-carousel__arrow svg { width: 100%; height: 100%; }
.video-carousel__arrow:hover { color: var(--color-cream); }
.video-carousel__arrow--prev { grid-area: prev; }
.video-carousel__arrow--next { grid-area: next; }

.video-carousel__track {
  grid-area: track;
  min-width: 0;
  /* No height:100% here (or on .video-carousel__slides/__slide below) —
     that chain of percentage heights, each resolving against an
     ancestor whose own height depended on ITS content, was what kept
     forcing this row to stretch to the full grid-row size (836px) even
     after switching the row itself to "auto" and trying align-content —
     neither actually broke the cycle. Letting track/slides/slide size
     purely by their own content (the video + caption) is what finally
     makes the row height match its real content, so row-gap is the only
     space between the caption and the pagination below it. */
  overflow: hidden;
  /* Full-bleed on mobile/tablet, per feedback — the video should fill as
     much width (and therefore height, at its fixed aspect ratio) as
     possible, cancelling .container's side padding the same way
     .photo-strip breaks out on About. Reset at desktop, where the
     reference shows the video centered with room for the arrows either
     side, not edge-to-edge. */
  margin-left: calc(-1 * var(--space-2));
  margin-right: calc(-1 * var(--space-2));
}
.video-carousel__slides {
  display: flex;
  transition: transform 0.3s ease-in-out;
}
.video-carousel__slide {
  flex: 0 0 100%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}
/* No object-fit crop — the video's own aspect ratio (4:5) is shown in
   full. max-height is a direct viewport-relative unit (not a percentage
   of an ancestor) so it stays robust regardless of how track/slide end
   up sized — mainly a safety cap; max-width:100% is what actually binds
   it in every viewport tested so far. */
.video-carousel__slide video {
  display: block;
  max-height: 70vh;
  max-height: 70dvh;
  max-width: 100%;
  width: auto;
  height: auto;
  background: var(--color-dark);
}
.video-carousel__caption {
  max-width: 340px;
  margin: var(--space-2) auto 0;
  /* Labels tier (14px/440/0), per STYLE.md — was its own one-off
     12px/0.02em combo, an accidental fork of the same caption pattern
     used everywhere else on project pages (.project-gallery figcaption
     etc.), simplified to match per feedback. */
  font-size: 0.875rem;
  font-weight: 440;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--color-sand);
  text-align: left;
}

/* Dot pagination: filled = current slide, empty = the rest */
.video-carousel__dots {
  grid-area: dots;
  /* Vertically centered with the arrows (grid's shared align-items:center
     applies here by default) — per feedback, this takes priority over
     the tighter caption-gap the previous align-self:start version gave,
     since the arrows and dots share the same 32px-tall controls row and
     should read as one aligned row, not offset from each other. */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
}
.video-carousel__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1.5px solid var(--color-green-light);
  background: transparent;
  cursor: none;
  padding: 0;
  transition: background 0.18s ease;
}
.video-carousel__dot.is-active {
  background: var(--color-green-light);
}

/* --- 9k. Closing project CTA --- */
.project-cta {
  /* border-top sits at this box's own top edge, painted BEFORE padding —
     so padding-top alone (the usual section-gap pattern) put the real
     gap between the line and the heading, leaving the line itself flush
     against the content above it with no visible space at all. margin-top
     pushes the line itself away from that content instead (collapses
     against the preceding element's own margin/padding-bottom:0), and
     defaults to the page's standard section-gap (--space-12) like every
     other true section break. Internal padding (line-to-heading,
     content-to-footer) is its own smaller, fixed tier regardless of page
     — --space-4 for both, not tied to the entrance gap. */
  margin-top: var(--space-12);
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}
/* Opt-in: pages where the CTA should read as grouped with the content
   right above it rather than a fresh section break — .project-cta is a
   shared class across every project page, so this has to be an opt-in
   modifier rather than a change to the base rule, or it'd silently
   change every other page reusing it (this is the same bleed bug
   .project-section--grouped-top was introduced to fix). Currently just
   sexual-health-campaign, per feedback ("feels huge, bring down to 48"). */
.project-cta--grouped-top { margin-top: var(--space-4); }
/* Border can't respect .container's own left/right padding on its own
   box (it follows the border-box edge, drawn outside the padding) — same
   issue and same fix as .site-footer__copy::before: draw the line on a
   pseudo-element inset by .container's own side padding instead, so it
   lines up with the content above/below it rather than running the full
   max-width box edge to edge. */
.project-cta::before {
  content: '';
  position: absolute;
  top: 0;
  left: var(--space-2);
  right: var(--space-2);
  border-top: 1px solid var(--color-sand);
}
.project-cta__text h2 {
  font-size: 2.5rem; /* H2 tier: 40px, per feedback — was too small at 2rem */
  letter-spacing: -0.02em;
  margin-bottom: var(--space-1);
}
.project-cta__text h3 {
  font-size: 1.625rem; /* H4 tier: 26px, per feedback ("H3 or H4") */
  letter-spacing: -0.0192em;
  font-weight: 700;
}
.project-cta__text .is-email,
.project-cta__text .is-linkedin {
  cursor: none;
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: opacity 0.18s ease;
}
.project-cta__text .is-email:hover,
.project-cta__text .is-linkedin:hover { opacity: 0.85; }
.project-cta__text .is-email { color: var(--color-pink-dark); } /* dark variant on the light page background, per feedback */
.project-cta__text .is-linkedin { color: var(--color-green-dark); }

.project-cta__top {
  flex-shrink: 0;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--color-green-dark);
  color: var(--color-cream);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: none;
  transition: background 0.18s ease;
}
.project-cta__top svg { width: 24px; height: 24px; }
.project-cta__top:hover { background: var(--color-forest); }

/* --- 9l. Mental-health-campaign additions ---
   New, reusable-enough-to-share components introduced for this page —
   kept generic (not "mental-health-"-prefixed) per the template
   philosophy, in case a future project page wants the same shapes. */

/* Stat feature band: cover image + a callout stat, side by side on
   desktop, stat-card-first on mobile (source order already matches
   mobile — the grid areas just relocate the image on top at tablet+, no
   real DOM reordering needed). margin-top (not padding) owns the
   entrance gap since this section has its own sand background — same
   reasoning as .project-gallery-band--first. */
.stat-feature {
  background: var(--color-sand);
  margin-top: var(--space-12);
  padding: var(--space-4) 0;
  /* Mobile-only: top padding (the sand band's own space above the grid)
     matches the container's side margins (24px) instead of the section's
     uniform 48px — per feedback, reset at tablet+ below. */
  padding-top: var(--space-2);
}
.stat-feature__grid {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas: "card" "image";
  gap: var(--space-1);
}
.stat-feature__image {
  grid-area: image;
  width: 100%;
  height: auto;
  display: block;
  cursor: none;
}
.stat-feature__card {
  grid-area: card;
  background: var(--color-cream);
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.stat-feature__icon {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: var(--color-green);
  color: var(--color-cream);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: var(--space-2);
}
.stat-feature__icon svg { width: 28px; height: 28px; }
.stat-feature__headline {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.625rem; /* H4 tier: 26px on mobile — bumped to H2/40px at tablet+ below */
  letter-spacing: -0.0192em;
  line-height: 1.25;
  color: var(--color-dark);
}
.stat-feature__headline .is-pink { color: var(--color-pink-dark); } /* dark variant on the light sand background, per feedback */
/* Locked line breaks — same technique as .project-hero-break: hidden
   (mobile wraps naturally at the smaller H4 size) until tablet+, where
   they're forced on so the headline always breaks "During the months /
   the campaign ran, / calls to the helpline / increased by 14%" exactly,
   regardless of how the natural wrap would otherwise fall at a given
   viewport width. */
.stat-feature__break { display: none; }
.stat-feature__body {
  /* Mobile-only: tighter heading-to-body gap (12px), reset at tablet+
     below — per feedback. */
  margin-top: var(--space-1);
}
@media (min-width: 769px) {
  .stat-feature__grid {
    grid-template-columns: 1fr 1fr;
    grid-template-areas: "image card";
    gap: var(--space-2);
  }
  .stat-feature { padding-top: var(--space-4); }
  .stat-feature__headline {
    font-size: 2.5rem; /* H2 tier: 40px, per feedback */
    letter-spacing: -0.02em;
    line-height: 1.15;
  }
  .stat-feature__break { display: block; }
  /* "increased by 14%" stays on one line at desktop, per feedback —
     mobile is left free to wrap since the narrower column makes that
     unavoidable at this font size. */
  .stat-feature__headline .is-pink { white-space: nowrap; }
  .stat-feature__body { margin-top: var(--space-2); }
}

/* Media pair: two videos, shown at once, no carousel/arrows needed since
   both are visible simultaneously (unlike the sexual-health-campaign
   carousel, which needed navigation for 4). No illustrated device-frame
   chrome and no colour-panel background either, per feedback — just the
   videos themselves filling the width side by side, same shape as
   .project-gallery--pair. margin-top owns the entrance gap, matching
   .project-gallery--first elsewhere on this page. */
.media-pair {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-1);
  margin-top: var(--space-12);
}
.media-pair__video {
  display: block;
  width: 100%;
  height: auto;
  /* Both tv-video.mp4 and social-story.mp4 are 1080x1350 (4:5) —
     reserves the correct box immediately instead of the browser's
     ~150px unloaded-video fallback, since these are preload="none" and
     don't fetch until scrolled into view (see main.js's lazy-play). */
  aspect-ratio: 4 / 5;
}
.media-pair__caption {
  margin-top: var(--space-1);
  font-size: 0.875rem; /* Labels tier: 14px */
  font-weight: 440;
  text-transform: uppercase;
  color: var(--color-dark);
}
@media (min-width: 769px) {
  .media-pair { grid-template-columns: 1fr 1fr; }
}

/* Masonry grid: digital ads on top, static square below it (same
   column), static story spanning both rows in a second, taller column —
   source order (ads, square, story) already matches the mobile stacking
   order, grid-template-areas just relocates them at tablet+. */
.masonry-grid {
  display: grid;
  grid-template-columns: 1fr;
  align-items: start;
  gap: var(--space-4);
}
.masonry-grid img {
  width: 100%;
  height: auto;
  display: block;
  cursor: none;
}
.masonry-grid figcaption {
  margin-top: var(--space-1);
  font-size: 0.875rem; /* Labels tier: 14px */
  font-weight: 440;
  text-transform: uppercase;
  color: var(--color-dark);
}
@media (min-width: 769px) {
  .masonry-grid {
    grid-template-columns: 1fr 1fr;
    grid-template-areas: "ads story" "square story";
    gap: var(--space-2);
  }
  .masonry-grid__ads { grid-area: ads; }
  .masonry-grid__square { grid-area: square; }
  .masonry-grid__story { grid-area: story; align-self: center; }
}

/* Generalizes per-image captions to any .project-gallery (trio or pair)
   whose children are <figure>+<figcaption> instead of bare <img> — same
   Labels-tier treatment as .masonry-grid figcaption, so any gallery on
   any project page can opt into individual captions for free. */
.project-gallery figcaption {
  margin-top: var(--space-1);
  font-size: 0.875rem; /* Labels tier: 14px */
  font-weight: 440;
  text-transform: uppercase;
  color: var(--color-dark);
}

/* --- 9m. MyHealthNB-launch additions ---
   Same "generic, reusable" philosophy as the mental-health-campaign
   additions above. */

/* Plain heading, no eyebrow/icon — used standalone above a gallery
   ("Supporting materials for the Launch", "Phase two") rather than as
   part of the eyebrow+copy .project-section pattern. */
.section-heading {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 2.5rem; /* H2 tier: 40px */
  letter-spacing: -0.02em;
  color: var(--color-dark);
  margin-bottom: var(--space-2);
}
/* Cancels .project-gallery's own 48px margin-top when it directly
   follows a standalone heading (e.g. "Supporting materials for the
   Launch") — same collapsing-margin issue and fix as
   .project-gallery:has(+ .project-gallery__caption), just on the other
   side. Without this the two margins stacked to a much bigger gap than
   the heading's own 24px. */
.section-heading + .project-gallery { margin-top: 0; }

/* Single-video feature: the launch film itself, full-bleed dark/textured
   section, no carousel chrome since there's only one video (unlike
   sexual-health-campaign's 4-video carousel). Caption styling matches
   .video-carousel__caption's dark-bg treatment, except left-aligned as
   a block (not centred) — that video is a full-width horizontal film,
   not a narrow portrait clip, so a centred caption block sat well off
   from the video's own left edge instead of lining up with it.
   Symmetric internal padding, top and bottom, around the video+caption —
   this is independent of the EXTERNAL entering gap, which Overview owns
   via its own padding-bottom (.project-section--owns-exit) so it reads
   as cream space, not more dark box. */
.video-feature {
  padding: var(--space-4) 0;
}
/* Shrink-wraps to the video's own rendered width (the video is the
   widest child — the caption is a short one-liner) and centers that as
   a unit via margin auto, so the caption below stays left-aligned to
   the video's edge instead of the wider .container around it. Per
   feedback: video centered, caption left-aligned within the video's
   width, not the section's. */
.video-feature__media {
  width: fit-content;
  margin: 0 auto;
}
.video-feature__video {
  display: block;
  max-width: 100%;
  max-height: 70vh;
  max-height: 70dvh;
  width: auto;
  height: auto;
  margin: 0;
  background: var(--color-dark);
  /* app-launch-en.mp4 is 1920x1080 (16:9) — same collapse-prevention
     reasoning as .media-pair__video above. */
  aspect-ratio: 16 / 9;
}
.video-feature__caption {
  /* No max-width (unlike .video-carousel__caption) — the container itself
     gives this caption plenty of room to sit on one line instead of
     wrapping, per feedback. */
  margin: var(--space-2) 0 0;
  /* Labels tier (14px/440/0), per STYLE.md — see .video-carousel__caption's
     comment, same fork/fix. */
  font-size: 0.875rem;
  font-weight: 440;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--color-sand);
  text-align: left;
}

/* Text + 2 images, 3 equal columns at tablet+ (the brand-statement quote
   sits in the first column instead of an image) — sand band, reuses
   .project-gallery-band's background/margin handling directly. */
.text-media-trio {
  display: grid;
  grid-template-columns: 1fr;
  align-items: center;
  gap: var(--space-3);
}
.text-media-trio__text {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 1.625rem; /* H4 tier: 26px */
  letter-spacing: -0.0192em;
  line-height: 1.3;
  color: var(--color-dark);
}
.text-media-trio img {
  width: 100%;
  height: auto;
  display: block;
  cursor: none;
}
.text-media-trio figcaption {
  margin-top: var(--space-1);
  font-size: 0.875rem;
  font-weight: 440;
  text-transform: uppercase;
  color: var(--color-dark);
}
@media (min-width: 769px) {
  .text-media-trio { grid-template-columns: repeat(3, 1fr); }
}

/* "Phase two" band: heading, then a video (left) with a stack of three
   supporting stills (right). */
.phase-band__header {
  margin-bottom: var(--space-3);
}
.phase-two-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-2);
}
.phase-two-grid__video {
  display: block;
  width: 100%;
  height: auto;
  /* Real dimensions (1920x1080) — reserves the correct box immediately
     instead of the browser's ~150px unloaded-video fallback, since this
     video is preload="none" and doesn't fetch until scrolled into view. */
  aspect-ratio: 16 / 9;
}
.phase-two-grid__stack {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-1);
}
.phase-two-grid__stack img {
  display: block;
  width: 100%;
  height: auto;
}
@media (min-width: 769px) {
  /* Per feedback: exact height match takes priority over a specific
     stack size — "whatever that means for the right row." A straight
     fr ratio (what earlier attempts used) only matches by coincidence
     at one exact container width; solved algebraically instead, which
     holds at any width.

     Given: the stack's own image ratio (1351:1081), its 2 internal
     gaps (--space-1 each), the gap between the two grid columns
     (--space-2), and the video's fixed 16:9 — there is exactly one
     stack width, as a function of the grid's total width, where a 16:9
     video filling the *remaining* space lands at precisely the stack's
     height with nothing left over. Solving
       stackWidth + columnGap + (stackHeight * 16/9) = totalWidth
     for stackWidth (where stackHeight = 3*(stackWidth/1.2498) +
     2*spaceUnit1) gives the calc() below. The video column is then
     just 1fr (whatever's left) — by construction, exactly the width
     its height needs. */
  .phase-two-grid {
    grid-template-columns: 1fr calc((100% - var(--space-1) * 32 / 9 - var(--space-2)) / 5.267);
  }
  /* No height override on the video needed — and deliberately none.
     An earlier pass tried height:100% (matching the row) here, which
     seemed to work but was actually a false positive from measuring
     before the video's metadata had loaded: once the video is fully
     loaded (readyState 4) its real 1920x1080 intrinsic size feeds into
     the grid's auto-row-height computation (a percentage height on a
     row that's still being computed can't resolve, so the browser falls
     back to the item's intrinsic size for that computation) — the row
     ballooned to 1080px and the video overflowed everything at every
     width once actually loaded. The video keeps the base width:100%/
     height:auto/aspect-ratio:16/9 rule instead — height is computed
     FROM width via the ratio, not from the row, so there's no
     percentage-height circularity to hit at all. This works by
     construction: the calc() above was solved specifically so that the
     video's column width, run through 16:9, lands at exactly the
     stack's own natural (also width-driven, also non-circular) height. */
}

/* Video dropped into a .project-gallery figure (e.g. the Pinterest ad,
   myhealthnb-launch) — same width:100%/height:auto treatment as
   .project-gallery img above, which only targets <img>. */
.project-gallery__video {
  display: block;
  width: 100%;
  height: auto;
  /* Real dimensions 1080x1352 (135:169) — updated when the Pinterest ad
     video was replaced with a new file (was 1000x1500/2:3 before).
     Reasoning otherwise unchanged from .phase-two-grid__video above:
     without this, the preload="none" video collapses to the browser's
     ~150px unloaded fallback height until it's scrolled into view and
     starts loading. */
  aspect-ratio: 135 / 169;
}

/* Reflection's closing statement reads larger than standard body copy —
   Body LG tier, per the reference. */
.reflection-statement {
  font-size: 1.25rem;
  line-height: 1.4;
}

/* --- 9n. Breastfeeding-guide additions --- */

/* Inline callout quote (e.g. "the user" + its description) — a lighter
   touch than .project-quote's sand card + offset shadow: just a bold
   label and a pink left-border, sitting directly in the section copy. */
.callout-quote__label {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1rem;
  margin-top: var(--space-3);
}
.callout-quote {
  border-left: 3px solid var(--color-pink);
  padding-left: var(--space-2);
  margin-top: var(--space-1);
}
.callout-quote p {
  font-size: 1rem;
}

/* True edge-to-edge image, no container/side padding at all — distinct
   from .project-gallery-band, which keeps a colour background behind an
   inset image. Here the image itself IS the full-bleed content. */
.full-bleed-image {
  display: block;
  width: 100%;
  height: auto;
  cursor: none;
  /* Standard entrance gap, owned here since the image itself has no
     wrapping section to put padding on — margin is safe regardless of
     which side owns it (it never paints), so this works the same way
     for both usages on this page (into guide-spine.jpg from Overview,
     and into open-toc-green.jpg from the carousel). */
  margin-top: var(--space-12);
}

/* Approach carousel: identical structure/behaviour to .video-carousel —
   arrows + dots + swipe, JS-driven translateX on a sliding track, same
   grid layout (track spans the top, arrows/dots share a row underneath)
   — just image+text slides instead of video. No scroll/wheel
   involvement (tried that, including a position:sticky hold — reverted,
   per feedback, to match the horizontal carousel exactly). No padding
   on the section itself — it's dark/graph-textured, so any padding here
   would just be more dark space, not a visible gap; the entrance gap is
   owned by Approach's own padding-bottom (.project-section--owns-exit)
   and the exit gap by the closing image's own margin-top
   (.full-bleed-image), same reasoning as everywhere else on this page. */
.vertical-carousel {
  /* Section reads as too tall at 100vh/100dvh since the sticky header eats
     into what's actually visible below it — subtract its real rendered
     height (93px below 769px, 81px 769–1023px, 67px at 1024px+, per the
     .site-header__inner/.site-header__brand overrides at each) so this
     section matches the space actually visible on load, not the full screen.
     min-height, not height, on mobile specifically — a hard height plus
     the grid's 1fr track caused a real overflow/broken-layout bug in
     Safari (the track's content min-size won out over the 1fr shrink,
     blowing the section out) once the 2-column content + full text got
     tall enough. min-height is a floor: section still fills the screen
     when content is short, but grows instead of clipping/breaking when it
     isn't. Tablet+ keeps a hard height (see overrides below) since the
     2-column layout there comfortably fits either way.
     display:flex with DEFAULT align-items (stretch), not align-items:center
     — see .video-carousel for the full reasoning: stretch structurally
     sizes the viewport child, while center+height:100% needs the
     container's height to be "definite" (min-height alone never counts),
     and position:absolute;inset:0 (also tried) can't grow a min-height'd
     parent past its floor at all since out-of-flow elements don't
     contribute to containing-block sizing. */
  min-height: calc(100vh - 93px);
  min-height: calc(100dvh - 93px);
  padding: 0;
  display: flex;
  overflow: hidden;
}
.vertical-carousel__viewport {
  width: 100%;
  /* No explicit height — see .video-carousel__viewport for why (stretch
     only stretches a child whose own height is auto). */
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-rows: 1fr auto; /* track fills remaining space and centers within it; prev/dots/next hugs the bottom */
  grid-template-areas:
    "track track track"
    "prev  dots  next";
  padding-bottom: var(--space-4); /* 48px from the bottom edge of the section, not from the content above it */
  align-items: center;
}
/* Mobile only: at tablet+ the 1fr row already leaves generous natural
   space above the content (the row is far taller than what the content
   needs), so no explicit top padding is needed there — but on mobile the
   content nearly fills the (now min-height, growable) box, leaving little
   to no visible grid texture above it without an explicit reservation.
   24px (--space-2) on both edges, not 48px — matches .container's own
   24px mobile side padding (see .video-carousel__viewport's mobile rule
   for the shared reasoning/align-self:start fix). */
@media (max-width: 768px) {
  .vertical-carousel__viewport {
    padding-top: var(--space-2);
    padding-bottom: var(--space-2);
  }
}
.vertical-carousel__arrow {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  color: var(--color-green-light);
  cursor: none;
  transition: color 0.18s ease;
}
.vertical-carousel__arrow svg { width: 100%; height: 100%; }
.vertical-carousel__arrow:hover { color: var(--color-cream); }
.vertical-carousel__arrow--prev { grid-area: prev; }
.vertical-carousel__arrow--next { grid-area: next; }
.vertical-carousel__dots {
  grid-area: dots;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
}
.vertical-carousel__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1.5px solid var(--color-green-light);
  background: transparent;
  cursor: none;
  padding: 0;
  transition: background 0.18s ease;
}
.vertical-carousel__dot.is-active {
  background: var(--color-green-light);
}
.vertical-carousel__track {
  grid-area: track;
  min-width: 0;
  overflow: hidden;
}
.vertical-carousel__slides {
  display: flex;
  transition: transform 0.3s ease-in-out;
}
.vertical-carousel__slide {
  flex: 0 0 100%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}
/* Stacked (image on top, text below) on mobile — not enough width for
   two real columns; two columns (image left, text right) from tablet+. */
.vertical-carousel__content {
  width: 100%;
  display: grid;
  grid-template-columns: 1fr;
  row-gap: var(--space-2);
  align-items: center;
}
.vertical-carousel__content img {
  display: block;
  width: 100%;
  max-width: 100%;
  max-height: 45vh;
  max-height: 45dvh;
  aspect-ratio: 1921 / 1401; /* uniform crop box — source images vary 1.25:1–1.37:1 natively, this pins them all to the same shape */
  object-fit: cover;
  margin: 0 auto;
  cursor: none;
}
.vertical-carousel__text h3 {
  font-size: 1.375rem; /* H5/H6-ish, between the two per the reference */
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--color-cream);
}
.vertical-carousel__text p {
  margin-top: var(--space-1);
  color: var(--color-sand);
}
@media (min-width: 769px) {
  .vertical-carousel__content {
    grid-template-columns: 1fr 1fr;
    column-gap: var(--space-4);
  }
  .vertical-carousel__content img {
    max-height: 60vh;
    max-height: 60dvh;
    margin: 0 auto 0 0;
  }
}

/* --- 9j. View more — links to the site's other project pages, per
   feedback. Sits between the closing CTA and the footer. Reuses
   .project-card/.project-card__caption as-is for the image, hover
   state, and typography, but is pinned to the MOBILE row layout
   (thumbnail left, text right) at every width, including desktop —
   "formatted similar to the mobile project links from the homepage,"
   per feedback — rather than switching to the stacked image-on-top
   layout every OTHER .project-card gets at 769px+ (see the responsive
   section). Testing on mental-health-campaign.html first before this
   goes on every project page. */
.project-viewmore {
  /* No margin-top of its own, per feedback — .project-cta right above it
     already owns a padding-bottom (its "content-to-footer" tier) that
     used to be the real gap before the footer; stacking this section's
     own --space-12 margin-top on top of that doubled up into an
     oversized gap. The divider (::before, painted at this box's own top
     edge) now sits right at the end of that existing padding instead,
     and padding-top alone spaces it from this section's heading. */
  position: relative;
  padding-top: var(--space-4);
  /* This is now the last section before the footer — matches
     .project-cta's own padding-bottom (its "content-to-footer" tier,
     per that rule's comment) so the card row gets the same breathing
     room before the footer that the CTA used to own on its own. */
  padding-bottom: var(--space-4);
}
.project-viewmore::before {
  content: '';
  position: absolute;
  top: 0;
  left: var(--space-2);
  right: var(--space-2);
  border-top: 1px solid var(--color-sand);
}
@media (min-width: 769px) {
  /* .project-section__copy is the grid's 1fr track — CSS Grid's
     automatic minimum-size algorithm floors a track at its content's
     own min-content width unless min-width is set, and the accordion's
     flex row (flex:0 0 auto children, no shrinking) has a large
     min-content since nothing in it is allowed to shrink. Without this,
     the track was expanding past the "fair share" 1fr would otherwise
     give it — wider than .container itself — which is what was pushing
     the accordion off the right edge of the page on narrower "small
     laptop" widths, per feedback, even after the cards' own widths were
     already clamped down to fit. */
  .project-viewmore .project-section__copy {
    min-width: 0;
  }
}
.viewmore-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-2); /* 24px, per feedback — was --space-4/48px */
  margin-top: var(--space-4);
}
@media (min-width: 769px) {
  .viewmore-grid {
    /* Cancels the mobile base rule's margin-top (meant to separate the
       grid from the eyebrow when they're stacked) — once the shared
       .project-section grid puts the eyebrow and this grid side by side
       instead, that margin was pushing the cards down below the
       eyebrow's own top edge instead of sitting inline with it, per
       feedback. */
    margin-top: 0;
  }
  /* Keeps the simple always-expanded row (image left, text right, one
     per line — grid-template-columns stays the mobile base's single
     1fr column) through the tablet/small-laptop range, undoing the
     site-wide 769px+ .project-card switch to the stacked image-on-top
     layout. The interactive multi-column accordion only turns on at
     1024px+ below, once there's reliably enough width for 3 cards side
     by side — a single column of full-width rows can never overflow
     horizontally no matter how narrow the content column gets, so this
     tier is the safe fallback for exactly the "small laptop" range
     that was overflowing before, per feedback. */
  .viewmore-grid .project-card {
    flex-direction: row;
    align-items: center;
    gap: var(--space-2);
  }
  .viewmore-grid .project-card img {
    width: 170px;
    height: auto;
  }
  .viewmore-grid .project-card__caption {
    margin-top: 0;
  }
}
@media (min-width: 1024px) {
  /* Accordion strip, per feedback — replaces the simple row above once
     there's enough width. Every card sits at its natural (image-only)
     width except one, which is wider and reveals a text info panel
     beside its image; hovering a different card expands that one
     instead, and the row falls back to its default-expanded card when
     nothing is hovered. flex (not grid) because each card's width is
     driven by its own content (a fluid image + a caption that's 0 or
     its own fluid width), not an even division of the row.
     Image/caption widths are clamp()'d (not fixed px) so the whole
     strip scales down smoothly on narrower "small laptop" widths
     instead of overflowing — tuned so 3 cards (2 collapsed + 1
     expanded) still fit within the content column's available width
     (.container's 1200px cap minus the 220px eyebrow column minus its
     gap) at this breakpoint's own 1024px floor, with a bit of margin,
     per feedback. */
  .viewmore-grid {
    display: flex;
    align-items: stretch;
    gap: var(--space-1);
  }
  .viewmore-grid .project-card {
    flex: 0 0 auto;
    display: flex;
    flex-direction: row;
    align-items: stretch;
    /* Cancels the 769px tier's gap:var(--space-2) (the simple row
       layout's own image-to-text spacing) — left in place, it added an
       untracked extra 24px inside every card here, which was exactly
       the gap between this fix's calculated widths and the overflow
       still showing on narrow "small laptop" widths, per feedback. The
       expanded caption's own padding provides the image-to-text gap in
       this layout instead. */
    gap: 0;
    overflow: hidden;
    border: 2px solid transparent;
    box-shadow: none;
    /* Overrides the site-wide .project-card:hover treatment below — the
       border/shadow highlight lives on the whole card here (image +
       info panel together), not just the image, since expanding also
       reveals the info panel inside the same highlighted box. */
    transition: border-color 0.35s ease, box-shadow 0.35s ease, width 0.35s ease;
  }
  .viewmore-grid .project-card img {
    width: clamp(125px, 13vw, 170px);
    height: auto;
    aspect-ratio: 360 / 518;
    object-fit: cover;
    flex-shrink: 0;
    border: none;
    box-shadow: none;
  }
  .viewmore-grid .project-card__caption {
    width: 0;
    /* Cancels the general .project-card__caption rule's min-width:100%
       (added for the stacked desktop layout elsewhere, so a long title
       matches the image's width instead of overflowing it) — that rule
       doesn't know about this accordion's own width transition and was
       forcing this panel to 100% of the row regardless, per feedback. */
    min-width: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin-top: 0;
    padding: 0;
    background: var(--color-cream);
    transition: width 0.35s ease, padding 0.35s ease;
  }
  /* Flex items default to min-width:auto, which for a text element means
     "never shrink below your own unwrapped single-line width" — without
     this, the title just overflowed the expanded panel instead of
     wrapping, per feedback. */
  .viewmore-grid .project-card__caption h3,
  .viewmore-grid .project-card__caption p {
    min-width: 0;
  }
  /* Cancels the shared .project-card:hover treatment (dark border/yellow
     shadow on the image alone, yellow highlight behind the heading) —
     both move to the card-level rules below instead, and the reference
     for this expanded state has no highlight strip behind the text. */
  .viewmore-grid .project-card:hover img {
    border-color: transparent;
    box-shadow: none;
  }
  .viewmore-grid .project-card:hover .project-card__caption h3 {
    background-color: transparent;
    padding: 0;
  }
  .viewmore-grid .project-card.is-expanded,
  .viewmore-grid .project-card:hover {
    border-color: var(--color-dark);
    box-shadow: 5px 5px 0 var(--color-yellow);
  }
  .viewmore-grid .project-card.is-expanded .project-card__caption,
  .viewmore-grid .project-card:hover .project-card__caption {
    width: clamp(205px, 21.3vw, 280px);
    padding: 0 var(--space-3);
  }
  /* Only one card expanded at a time: whichever is hovered wins, and the
     default-expanded card (.is-expanded) collapses back while something
     else is hovered. Un-hovering anywhere in the row returns to the
     default. */
  .viewmore-grid:has(.project-card:hover) .project-card.is-expanded:not(:hover) {
    border-color: transparent;
    box-shadow: none;
  }
  .viewmore-grid:has(.project-card:hover) .project-card.is-expanded:not(:hover) .project-card__caption {
    width: 0;
    padding: 0;
  }
}

/* =====================================================
   10. LIGHTBOX — click a photo-strip image to view it large,
   at its natural aspect ratio
   ===================================================== */

#lightbox {
  position: fixed;
  inset: 0;
  z-index: 500;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
  background: rgba(19, 20, 18, 0.85);
  backdrop-filter: blur(4px);
}
#lightbox.is-open { display: flex; }
#lightbox img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
#lightbox__close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  width: 40px;
  height: 40px;
  font-size: 1.5rem;
  color: var(--color-cream);
  cursor: none;
}
/* Prev/next — move through the other images in the same gallery/
   section as the one that opened the lightbox (see main.js). Hidden
   via [hidden] for a single-image group, same as any other optional
   control on the site. */
#lightbox__prev,
#lightbox__next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 48px;
  height: 48px;
  color: var(--color-cream);
  cursor: none;
  transition: color 0.18s ease;
}
#lightbox__prev svg,
#lightbox__next svg { width: 100%; height: 100%; }
#lightbox__prev:hover,
#lightbox__next:hover { color: var(--color-pink-light); }
#lightbox__prev { left: var(--space-3); }
#lightbox__next { right: var(--space-3); }
@media (max-width: 480px) {
  #lightbox__prev,
  #lightbox__next {
    width: 36px;
    height: 36px;
  }
  #lightbox__prev { left: var(--space-1); }
  #lightbox__next { right: var(--space-1); }
}

/* =====================================================
   11. HOME PAGE
   ===================================================== */

/* --- 11a. Hero — dark "graph" bg + layered gradient-glow headline --- */
.home-hero {
  /* Full-viewport, matching the header-height subtraction technique used
     on project pages (.project-hero/.vertical-carousel/.video-carousel) —
     93px below 769px, 81px 769–1023px, 67px at 1024px+, see those for the
     full reasoning. min-height (not height) so it can still grow if
     content ever needs more room. */
  position: relative;
  min-height: calc(100vh - 93px);
  min-height: calc(100dvh - 93px);
  padding: var(--space-9) 0 var(--space-6);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  overflow: hidden;
}
/* Film-grain texture, per feedback — an inline SVG feTurbulence noise
   tile (no image asset/JS needed) as a ::after, not ::before, so it
   paints on top of the heading/scroll-cue rather than behind them (an
   unpositioned ::before would insert before — i.e. visually under — the
   other children in paint order; ::after inserts after/on top). overlay
   blend + low opacity keeps it a subtle grit rather than visible static.
   The position jitters between a handful of offsets on a stepped timing
   function (not eased) so it reads as flickering film noise rather than
   a smoothly sliding texture — classic CSS film-grain technique. */
.home-hero::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.16; /* down from 0.28 — that was reading as a grey wash over the whole hero, not grain */
  /* screen, not overlay — overlay's math collapses toward the backdrop
     when the backdrop is dark (result ≈ 2×backdrop×source, and this
     hero's backdrop is near-black), which was silently suppressing the
     noise almost entirely regardless of opacity. screen (1-(1-backdrop)
     ×(1-source)) fully preserves the noise's own brightness even at a
     near-zero backdrop — the standard technique for grain over a dark
     background. */
  mix-blend-mode: screen;
  /* Dialed back again per feedback ("greying out the content"): dropped
     numOctaves 4→2 (the extra octaves were producing a regular woven-
     fabric-like structure, not organic grain) and raised the gamma
     exponent 2.4→3.2 so more of the tile stays black — black is a no-op
     under screen — instead of mid-grey wash. feColorMatrix still
     desaturates (fractalNoise randomizes each RGB channel independently,
     which reads as colour tint once contrast is pushed) so it stays
     neutral grey/white like a real film scan. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch' result='grain'/%3E%3CfeColorMatrix in='grain' type='saturate' values='0' result='mono'/%3E%3CfeComponentTransfer in='mono'%3E%3CfeFuncR type='gamma' exponent='3.2' amplitude='1' offset='0'/%3E%3CfeFuncG type='gamma' exponent='3.2' amplitude='1' offset='0'/%3E%3CfeFuncB type='gamma' exponent='3.2' amplitude='1' offset='0'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 260px 260px;
  animation: home-hero-grain 0.6s steps(4) infinite;
}
/* CRT scanlines + vignette, layered on a separate real element (not
   folded into the grain ::after) so they stay static while the grain
   above jitters — real scanlines don't drift, only the film grain does.
   A plain last-child div, not another pseudo-element: ::after is always
   painted last regardless of source order, so this only needs to come
   after the heading/scroll-cue in the markup to sit above them, same
   paint-order logic the grain ::after's own comment documents. Together
   with the grid background already on this section, this is what reads
   as 80s/90s CRT-tech rather than just "a dark hero with noise." */
.home-hero__scanlines {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    repeating-linear-gradient(to bottom,
      rgba(0, 0, 0, 0.28) 0px,
      rgba(0, 0, 0, 0.28) 1px,
      transparent 1px,
      transparent 3px
    ),
    radial-gradient(ellipse at 50% 50%, transparent 45%, rgba(0, 0, 0, 0.45) 100%);
}
/* Fixed px offsets, not % — % on a translate is relative to THIS
   element's own box (the full hero, 1000px+ tall), not the 260px noise
   tile, so what looked like a small jitter was actually shifting the
   tile by 15-25px per step: more than a tenth of the tile's own size,
   large enough to visibly "jump" and reveal the repeating tile edges
   (per feedback). A few px is still a full tile-widths' worth of jitter
   at the noise's own grain-sized detail, without moving so far the
   pattern visibly resets. */
@keyframes home-hero-grain {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-3px, 2px); }
  50%  { transform: translate(2px, -2px); }
  75%  { transform: translate(-2px, -3px); }
  100% { transform: translate(0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .home-hero::after { animation: none; }
}
/* Grid texture scales up with viewport too, per feedback — same fluid
   technique as the heading font-size above, so the pattern stays
   visually proportionate next to the now-much-larger hero text instead
   of reading as tiny/dense on a big screen. Two separate length clamps
   (cell size, stroke width) rather than a single unitless multiplier —
   CSS calc()/clamp() can't add a unitless number to a vw length (a type
   mismatch that silently invalidates the whole declaration, which is
   what an earlier --grid-scale-multiplier version of this hit: the
   background-image/background-size came out as none/auto with no
   console warning). Both clamps run from .bg-graph's shared default
   (30px cells, 0.5px stroke) up to 1.6x that (48px, 0.8px) across the
   same 375–1024px range as the heading, then hold flat. Scoped to only
   this section for now (not the shared .bg-graph rule itself, which
   STYLE.md documents as a fixed 30px everywhere else — footer, About
   stats, project carousels) — per feedback, extending this to the rest
   of the site is a later step, not this one. */
.home-hero.bg-graph {
  --grid-cell: clamp(30px, 19.6px + 2.774vw, 48px);
  --grid-stroke: clamp(0.7px, 0.458px + 0.0647vw, 1.12px); /* was 0.5–0.8px, scaled up ~1.4x per feedback (grain still wasn't reading, tried a heavier grid instead) */
  background-size: var(--grid-cell) var(--grid-cell);
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.10) var(--grid-stroke), transparent var(--grid-stroke)),
    linear-gradient(90deg, rgba(255, 255, 255, 0.10) var(--grid-stroke), transparent var(--grid-stroke));
}
.home-hero__heading {
  position: relative;
  margin: 0 auto;
  /* Fluid, per feedback — scales continuously with viewport width instead
     of jumping between fixed sizes at a breakpoint, but never below the
     mobile floor (4.25rem, confirmed against home-mobile.jpg) or above
     the desktop ceiling (6.5rem, confirmed against home-desktop.jpg).
     Formula reaches exactly 4.25rem at 375px and exactly 6.5rem at
     1024px, linearly in between (standard fluid-type clamp math: slope =
     (6.5-4.25)/(1024px-375px) = ~5.546vw, intercept solves for the
     4.25rem value at 375px). Past 1024px the clamp ceiling holds it flat
     at 6.5rem, same as the old fixed desktop value.
     Line-COUNT (4 lines mobile vs 2 desktop) is now handled independently
     via .home-hero__break in the markup, not by wrapping against a
     max-width cap — the two were coupled before, which doesn't hold up
     under continuous font-size scaling (a wider/narrower cap interacting
     with a constantly-changing font-size doesn't reliably land on 2 or 4
     lines, only accidentally at the old fixed sizes). No max-width here
     any more; nothing to constrain now that line breaks are explicit. */
  font-size: clamp(4.25rem, 2.95rem + 5.546vw, 6.5rem);
  line-height: 1.15;
  letter-spacing: -0.0417em; /* Hero tier: -3px at 72px, expressed as em, per STYLE.md */
}
/* Explicit line-break control, decoupled from font-size — shown (mobile/
   tablet, 4-line layout) by default, hidden (desktop, 2-line layout) at
   1024px+ below. The one PERMANENT <br> in the markup (between "without"
   and "empathy") always fires at every size; these two are the only ones
   that toggle. */
.home-hero__break { display: inline; }
/* The real (accessible) text plus one soft glow duplicate share the same
   gradient-clipped look — per feedback, brought back smaller/subtler
   than the original spec (STYLE.md's old two-layer, 50px-blur recipe):
   one layer, a modest 12px blur, moderate opacity, so it reads as a
   gentle glow rather than a halo competing with the text or the
   chromatic-aberration fringe below. */
.home-hero__heading-text,
.home-hero__heading-glow {
  display: block;
  /* Exact colors/stops confirmed against the PRD's dedicated gradient
     reference (Screenshot_2026-07-26_at_6.10.33_PM.png) — the actual
     Figma gradient panel: #C6D4E1 46%, #FD8EBD 54%, #BBC48F 66%,
     #FDA24B 99%, remapped proportionally onto 0–100% for a linear sweep.
     Figma's gradient there is "Angular" (conic), but reproducing that
     literally as a CSS conic-gradient centered near a two-line, natural-
     wrap heading created a visible seam/swirl where the two lines' angles
     diverge — a fixed-frame Figma export doesn't have that problem, but
     live-wrapping HTML text does. Linear, at 90deg, gives the same left-
     to-right color sequence without the artifact. */
  background: linear-gradient(90deg,
    #C6D4E1 0%, /* no matching token — this exact blue-grey is unique to this gradient's Figma stops */
    var(--color-pink-light) 15%,
    var(--color-green-light) 38%,
    #FDA24B 100% /* no matching token — close to but not exactly --color-yellow (#FDAF04) */
  );
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* Ambient glow — always on (per earlier feedback, this used to only
   flare up on hover from a much subtler resting state), and now flares
   further still on hover on top of that constant base — per feedback,
   stronger blur/opacity than the always-on level. */
.home-hero__heading-glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: hard-light;
  filter: blur(28px);
  opacity: 0.85;
  transition: filter 0.4s ease, opacity 0.4s ease;
}
.home-hero__heading:hover .home-hero__heading-glow {
  filter: blur(40px);
  opacity: 1;
}

/* Chromatic-aberration / RGB-split effect, per feedback (inspired by a
   glitch-text reference, "GLOW" — a duotone fringe around the
   letterforms) — two more aria-hidden duplicate layers, same positioning
   technique as the glow layer above, but clipped to flat colours instead
   of the rainbow gradient (a true colour-channel split reads as solid
   fringes at the letter edges, not another gradient). Uses the site's
   own palette (pink + yellow, both already gradient stops elsewhere on
   this heading) rather than literal red/cyan. Static offset, no
   animation — an earlier drifting version read as unwanted constant
   motion, per feedback ("don't like the movement"); the fringe itself
   stays, it just doesn't move. screen blend mode is what makes the
   overlap read as near-white at each glyph's center while still showing
   a visible colour fringe at the edges — the same optical mechanism as a
   real lens's chromatic aberration. */
.home-hero__heading-glitch {
  display: block;
  position: absolute;
  inset: 0;
  pointer-events: none;
  color: transparent;
  -webkit-background-clip: text;
  background-clip: text;
  mix-blend-mode: screen;
}
.home-hero__heading-glitch--pink {
  /* background-color, not the background shorthand — the shorthand
     resets every background-* longhand it doesn't mention (including
     background-clip) back to its initial value, which was silently
     undoing the base rule's background-clip:text and rendering this as
     a solid box instead of clipped-to-text (confirmed via
     getComputedStyle: backgroundClip came back "border-box"). */
  background-color: var(--color-pink);
  opacity: 0.75;
  transform: translate(-4px, 2px);
}
.home-hero__heading-glitch--yellow {
  background-color: var(--color-yellow); /* background-color, not the shorthand — see .home-hero__heading-glitch--pink's comment */
  opacity: 0.75;
  transform: translate(4px, -2px);
}

.home-hero__scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 48px;
  transform: translateX(-50%);
  width: 32px;
  height: 32px;
  color: var(--color-pink-light);
  cursor: none;
  animation: home-hero-bounce 1.6s ease-in-out infinite;
}
.home-hero__scroll-cue svg { width: 100%; height: 100%; }
@keyframes home-hero-bounce {
  /* Combined with the element's own translateX(-50%) centering — a
     keyframe's transform replaces the base value entirely rather than
     composing with it, so both translations have to live here together. */
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50% { transform: translateX(-50%) translateY(6px); }
}
@media (prefers-reduced-motion: reduce) {
  .home-hero__scroll-cue { animation: none; }
}

/* --- 11b. Marquee — infinite-scroll discipline strip --- */
.marquee {
  overflow: hidden;
  background: var(--color-pink); /* per feedback, was --color-green-light */
  padding: 4px 0; /* 24px total band height, per feedback — 16px text (1rem @ line-height:1, see .marquee__track span below) + 4px top/bottom */
}
.marquee__track {
  display: flex;
  width: max-content;
  gap: var(--space-6);
  animation: marquee-scroll 40s linear infinite; /* slowed further from 26s, per feedback */
}
/* Second row scrolls the opposite direction — reusing the same
   marquee-scroll keyframe (0% → -50%) via animation-direction:reverse
   rather than writing a mirrored keyframe, since reversing playback of a
   symmetric loop is visually identical to a "backwards" version of it.
   Much slower than the top row and a green background, per feedback —
   both distinguish it as a second, separate rhythm rather than reading
   as a continuation of the row above. */
.marquee--reverse {
  background: var(--color-green);
}
.marquee--reverse .marquee__track {
  animation-direction: reverse;
  animation-duration: 160s; /* significantly slower, per feedback — was 55s */
}
/* Bottom row is a single repeating word, always emphasized — every span
   is bold, not just one "current" one, so this is a track-level modifier
   rather than the old single-word .is-current class. */
.marquee--bold .marquee__track span {
  font-weight: 700;
}
/* Hovering OR clicking either row pauses BOTH rows together, not just
   the one interacted with — see main.js, which toggles this class on
   both .marquee elements at once regardless of which one triggered it.
   Click is a persistent toggle (stays paused after the mouse leaves,
   independent of hover); hover alone releases as soon as the mouse
   leaves either row. */
.marquee.is-paused .marquee__track {
  animation-play-state: paused;
}
.marquee__track span {
  position: relative;
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1; /* was the browser default (~1.4-1.5) — that alone was most of the old ~78px band height */
  /* Eyebrow tier weight (600), per STYLE.md — was 500, an accidental
     near-miss of Eyebrow given this already shares its 16px size and
     0.0625em letter-spacing; simplified to match exactly, per feedback. */
  font-weight: 600;
  letter-spacing: 0.0625em;
  text-transform: uppercase;
  color: var(--color-cream); /* light text, per feedback — was --color-forest */
  white-space: nowrap;
}
/* Thin vertical divider between words, confirmed against both reference
   images — centered in the gap to this word's left, skipped on the very
   first word (which has no predecessor). */
.marquee__track span:not(:first-child)::before {
  content: '';
  position: absolute;
  left: calc(var(--space-6) / -2);
  top: 50%;
  transform: translateY(-50%);
  width: 1px;
  height: 1em;
  background: var(--color-cream);
}
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); } /* content is duplicated exactly once, so -50% is a seamless loop */
}
@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
}

/* --- 11c. Project scroller — horizontal on desktop, vertical stack on
   mobile, per PRD ("Horizontal scrolling: Home page, through the featured
   project cards... sized down to mobile cards stack vertically"). Native
   overflow scroll, not JS/arrows — manual, no snap-to-fight-the-user. */
.project-scroller {
  /* Section-spacing standard (var(--space-12), owned by the entering
     element's padding-top) applied site-wide, per feedback — same rule
     project pages already use for topic-to-topic breaks. */
  padding: var(--space-12) var(--space-2) var(--space-2);
}
.project-scroller__track {
  display: flex;
  flex-direction: column;
  gap: var(--space-2); /* 24px, per feedback — was --space-4/48px */
}
/* Intro-to-first-card gap stays the full 144px on mobile even though the
   other card-to-card gaps are 24px, per feedback — adds the extra 120px
   on top of the track's own 24px gap (120+24=144) rather than raising
   the track gap itself (which would re-widen every other gap too).
   Reset at 769px+ below, where the row layout's own align-items:flex-
   start would otherwise read this as a vertical offset instead of
   horizontal spacing. */
.intro-card + .project-card {
  margin-top: var(--space-10);
}

/* Intro card: per the mobile reference this is PLAIN text on mobile — no
   box, border, shadow, or dark background at all, just the eyebrow +
   heading sitting directly on the page background. The full dark-card
   treatment (box, border, shadow, arrow) is desktop-only — see the 769px
   override below. */
.intro-card {
  display: block;
  cursor: none;
}
.intro-card__eyebrow {
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0.0625em;
  text-transform: uppercase;
  color: var(--color-pink-dark); /* dark variant on this mobile treatment's light page background — the 769px+ override below switches to pink-light for the dark card background instead, so leave that one alone */
}
.intro-card__text {
  margin-top: var(--space-2);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.375rem;
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: var(--color-dark);
}
.intro-card__arrow {
  display: none; /* desktop-only, see 769px override */
}

/* Project card: mobile is a horizontal row (thumbnail left, title/type
   right — per the mobile reference, not the stacked image-on-top layout
   STYLE.md's Project Cards spec describes, which is the desktop
   treatment). Desktop switches to the stacked layout at 769px+ below. */
.project-card {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: none;
}
.project-card img {
  display: block;
  flex-shrink: 0;
  width: 170px; /* measured against home-mobile.jpg */
  aspect-ratio: 360 / 518;
  object-fit: cover;
  border: 2px solid transparent; /* reserves the hover border's space so nothing shifts on hover */
  transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.project-card:hover img {
  border-color: var(--color-dark);
  box-shadow: 5px 5px 0 var(--color-yellow); /* was --color-yellow-light, per feedback */
}
.project-card__caption h3 {
  /* inline, not the default block — the hover highlight below paints
     this element's own box, and a block h3 always spans the FULL width
     of its parent (which, since the caption column was widened to match
     the image per feedback, is now wider than the title text itself),
     so the highlight was reading as a wide bar instead of hugging just
     the text. inline makes each wrapped line report its own tight box
     instead, the same way a text selection highlight works. */
  display: inline;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.25rem; /* Heading 6 tier: 20px, per STYLE.md */
  letter-spacing: -0.03em;
  color: var(--color-dark);
  transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
}
.project-card:hover .project-card__caption h3 {
  background-color: rgba(253, 175, 4, 0.3); /* --color-yellow @ 30% — was --color-yellow-light, per feedback */
  padding: 0 0.15em;
}
/* Two of the four project titles have a shorter mobile-specific wording
   confirmed against the mobile reference (e.g. "Sexual Health Awareness"
   vs desktop's "Provincial Sexual Health Awareness") — same show/hide-span
   technique as .hero-break. */
.project-card__title--long { display: none; }
.project-card__title--short { display: inline; }
.project-card__caption p {
  margin-top: 0.25rem;
  font-family: var(--font-body);
  font-size: 0.875rem; /* Labels tier: 14px */
  font-weight: 440;
  text-transform: uppercase;
  color: var(--color-green-dark);
}

/* --- 11d. About teaser — photo + copy sit side by side at EVERY
   breakpoint per the reference (not stacked-then-row); only the photo's
   own shape/size changes: a small circular avatar on mobile, the same
   square offset-shadow treatment as .about-intro__photo at desktop. */
.home-about-teaser {
  /* Section-spacing standard (var(--space-12), both top and bottom) —
     per feedback, matches the standard topic-break value on both sides
     rather than a smaller bottom value. */
  position: relative;
  max-width: var(--max-width);
  margin: 0 auto;
  padding: var(--space-12) var(--space-2);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  text-align: left;
}
.home-about-teaser__photo {
  flex-shrink: 0;
  width: 72px;
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  overflow: hidden;
  /* Same offset-shadow language as the desktop version below (green
     top-left, pink bottom-right, no blur) instead of the old concentric
     ring — box-shadow follows border-radius automatically, so this
     renders as two circular offset copies behind the photo, per
     feedback. */
  box-shadow:
    -4px -4px 0 0 var(--color-green),
     4px  4px 0 0 var(--color-pink);
}
.home-about-teaser__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.home-about-teaser__copy p {
  font-size: 1rem;
  line-height: 1.5;
  color: var(--color-dark);
}
.home-about-teaser__copy .link-btn {
  display: inline-block;
  margin-top: var(--space-2);
}
/* =====================================================
   12. RESPONSIVE — mobile-first
   Breakpoints: mobile ≤480, tablet ≤768, desktop 1024+

   This section holds cross-component tablet/desktop overrides — anything
   whose base rule already lives in its own component section above (6.
   HEADER, 8. ABOUT, 9. PROJECT PAGE TEMPLATE, 11. HOME PAGE) gets its
   responsive follow-up here instead, grouped by component under the
   labeled dividers below, in the same top-to-bottom order as those
   sections. Kept in one place (rather than split across every
   component's own section) because most breakpoint changes here are
   small, one-line overrides that don't carry enough surrounding context
   to justify a full @media block of their own back where the base rule
   lives — some larger, more self-contained components (the carousels,
   .video-feature, etc.) still keep their own @media overrides colocated
   with their base rules instead, which is why you'll also see @media
   blocks scattered through sections 8/9/11 above, not just here.
   ===================================================== */

/* --- Header & Navigation: below 769px --- */
.site-nav__links { display: none; }
.hamburger { display: flex; }

/* --- About page: below 769px --- */
/* Mobile: one column for stat cards and the fun-facts grid. */
.about-stats__grid { grid-template-columns: 1fr; }
.fun-facts__grid { grid-template-columns: 1fr 1fr; }

@media (min-width: 769px) {
  /* Tablet and up */

  /* --- About page --- */
  .about-intro { grid-template-columns: 220px 1fr; }
  .fun-facts__grid { grid-template-columns: repeat(3, 1fr); }
  .about-intro__heading br { display: none; }

  /* --- Home page --- */
  /* Home: cards row goes horizontal (native scroll, not JS/arrows — the
     PRD calls for manual scroll here, no snap, so it never fights a
     trackpad/mouse-wheel gesture the way the snap-based carousels can).
     Edge-to-edge (not aligned to .container's 1200px max-width) — per
     feedback, "make more use of the horizontal spacing" rather than
     matching the header/footer's centered-container edge. Trying this as
     an experiment; easy to revert to the old max()-against---max-width
     formula (see git history) if it doesn't read well.
     --card-height is shared by .intro-card and .project-card img (below)
     so they can't drift out of sync — "almost fill the vertical space of
     the screen," per feedback. clamp()'s middle value (65vh) does the
     actual scaling; the floor (440px) keeps cards usable on short/
     landscape-phone viewports, the ceiling (720px) is a guessed starting
     point for large/tall screens (feedback flagged this number as
     uncertain — adjust freely). */
  .project-scroller {
    --card-height: clamp(440px, 65vh, 720px);
    overflow-x: auto;
    padding-bottom: var(--space-4);
    padding-left: var(--space-3);
    padding-right: var(--space-3);
  }
  .project-scroller__track {
    flex-direction: row;
    /* Default align-items:stretch was forcing .intro-card to match
       .project-card's TOTAL height (image + caption below it) instead of
       just its own aspect-ratio — flex-start lets each card size to its
       own content/aspect-ratio and top-aligns them instead. */
    align-items: flex-start;
    width: max-content;
    gap: var(--space-1); /* 12px, per feedback */
  }
  .intro-card + .project-card {
    margin-top: 0; /* mobile-only gap bump — row layout uses the track's own gap instead */
  }
  .project-card {
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    /* Overrides the mobile base rule's gap:var(--space-2) (meant for the
       horizontal row layout's img-to-text spacing there) — left unset,
       it was stacking with .project-card__caption's own margin-top
       below, doubling the effective image-to-caption gap to 48px instead
       of the intended 24px. */
    gap: 0;
    /* Overrides the mobile base rule's align-items:center (meant to
       vertically center the row's image against the text beside it) —
       left unset, it was still applying at desktop's column layout too,
       horizontally centering the (narrower, text-width) caption instead
       of left-aligning it flush with the image's left edge above it. */
    align-items: flex-start;
  }

  /* Intro card picks up the full dark-card treatment here — plain text
     only below this breakpoint, see the base rule for why. Width isn't
     set at all: aspect-ratio (360/518) derives it from height
     automatically, same as .project-card img below. */
  .intro-card {
    flex-shrink: 0;
    position: relative;
    display: flex;
    flex-direction: column;
    justify-content: center; /* all text vertically centered, per feedback — arrow is positioned separately below */
    height: var(--card-height);
    aspect-ratio: 360 / 518; /* exact match to .project-card img's own dimensions */
    padding: var(--space-3);
    /* Same background-color/image/size as .bg-graph — not applied via
       that shared class since it would also leak onto the mobile plain-
       text treatment above, which must stay on the page background. */
    background-color: var(--color-grid);
    background-image:
      linear-gradient(rgba(255, 255, 255, 0.10) 0.5px, transparent 0.5px),
      linear-gradient(90deg, rgba(255, 255, 255, 0.10) 0.5px, transparent 0.5px);
    background-size: 30px 30px;
    /* Same offset-shadow language as .about-intro__photo (green top-left,
       pink bottom-right) — no border, per feedback. */
    box-shadow:
      -6px -6px 0 0 var(--color-green),
       6px  6px 0 0 var(--color-pink);
  }
  .intro-card__eyebrow { color: var(--color-pink-light); }
  .intro-card__text { color: var(--color-green-light); }
  .intro-card__arrow {
    display: block;
    position: absolute;
    right: var(--space-3);
    bottom: var(--space-3);
    width: 28px;
    height: 28px;
    color: var(--color-green-light);
  }
  .intro-card__arrow svg { width: 100%; height: 100%; }

  /* Project card switches from the mobile row layout to the stacked
     image-on-top layout STYLE.md's Project Cards spec describes — the
     display:flex/flex-direction:column that makes this work lives on the
     earlier .project-card rule above (grouped with .intro-card's shared
     height clamp), not here. */
  .project-card img {
    height: var(--card-height);
    width: auto;
  }
  .project-card__caption {
    margin-top: var(--space-2);
    /* width:0 removes this from the card's own shrink-to-fit width
       calculation (a flex column card with no explicit width normally
       takes the widest child's own preferred size — with a long
       unwrapped title that was the CAPTION, not the image, so the card
       widened past the image and the title overflowed its right edge,
       per feedback). min-width:100% then re-expands it to match
       whatever width the image (the only remaining, definite-sized
       child) actually resolved to, so the title wraps within the
       image's own width instead of escaping past it. */
    width: 0;
    min-width: 100%;
  }
  .project-card__title--long { display: inline; }
  .project-card__title--short { display: none; }

  /* Home: about-teaser photo grows and switches from a small circular
     avatar to the square offset-shadow treatment shared with
     .about-intro__photo. */
  .home-about-teaser {
    max-width: 1000px; /* per feedback, was 800px */
    margin: 0 auto; /* centered, per feedback */
    gap: var(--space-6);
    padding-left: var(--space-3);
    padding-right: var(--space-3);
  }
  .home-about-teaser__photo {
    width: 220px;
    aspect-ratio: auto;
    border-radius: 0;
    overflow: visible;
    border: none;
    box-shadow:
      -6px -6px 0 0 var(--color-green),
       6px  6px 0 0 var(--color-pink);
  }
  .home-about-teaser__copy p {
    font-size: 1.25rem; /* Body LG tier */
    line-height: 1.4;
  }

  /* --- About page --- */
  .cta-bar__inner {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
  }

  /* --- Shared: side margins restored to 36px at tablet+ (the 24px mobile
     value is too tight once there's more room to work with) — spans
     .container itself plus every page-specific wrapper that doesn't
     inherit it directly (About, footer, project-page CTA). --- */
  .container,
  .about-intro,
  .fun-facts,
  .about-contact {
    padding-left: var(--space-3);
    padding-right: var(--space-3);
  }
  .about-contact { min-height: 70vh; }
  .site-footer__copy::before {
    left: var(--space-3);
    right: var(--space-3);
  }
  .project-cta::before {
    left: var(--space-3);
    right: var(--space-3);
  }
  .project-viewmore::before {
    left: var(--space-3);
    right: var(--space-3);
  }

  /* --- Header & Navigation --- */
  .site-header__inner {
    /* Top/bottom shouldn't scale with the much-larger side margin here —
       half of it reads right, per feedback. */
    padding-top: calc(var(--space-3) / 2);
    padding-bottom: calc(var(--space-3) / 2);
  }

  /* --- Shared: full-viewport sections' header-height subtraction
     (Project pages + Home page) — see each base rule (sections 9b, 9j,
     11a) for the full "why calc(100vh - Npx)" reasoning; this tier just
     updates the pixel value to match the header's real height at this
     breakpoint (81px, per the .site-header__inner padding change just
     above) — a further 1024px+ tier follows below. --- */
  .project-hero {
    min-height: calc(100vh - 81px);
    min-height: calc(100dvh - 81px);
  }
  .vertical-carousel {
    height: calc(100vh - 81px);
    height: calc(100dvh - 81px);
  }
  .home-hero {
    min-height: calc(100vh - 81px);
    min-height: calc(100dvh - 81px);
  }

  /* --- About page --- */
  /* Photo grid stays inset with its original gap from here up (only
     mobile gets the flush-edge-to-edge treatment). */
  .photo-strip {
    margin-left: 0;
    margin-right: 0;
    gap: var(--space-1);
  }

  /* --- Project page template: tablet+ --- */
  /* Larger hero size + the manual line break apply from tablet up, per
     feedback — only true mobile widths keep the smaller, naturally-wrapped
     version. */
  .project-hero__heading { font-size: 4.5rem; }
  .project-hero-break { display: inline; }

  /* Carousel video stays full-bleed at this breakpoint too (arrows/dots
     now live below it at every size, not beside it) — the breakout
     margin just needs to match .container's own side padding, which
     changes to --space-3 here. */
  .video-carousel__track {
    margin-left: calc(-1 * var(--space-3));
    margin-right: calc(-1 * var(--space-3));
  }

  /* Full-viewport immersion restored here (see .video-carousel's base
     rule for why it's dropped on mobile) — no padding at this breakpoint
     either (see the base rule for why). Subtracts the sticky header's
     81px height at this width (67px override at 1024px+, below) so the
     section fills exactly the space visible on load — see
     .vertical-carousel for the same measurement/reasoning. */
  .video-carousel {
    height: calc(100vh - 81px);
    height: calc(100dvh - 81px);
  }

  .realworld-grid {
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "copy   winter"
      "busfr  busen";
  }
  .realworld-grid__copy  { grid-area: copy; }
  .realworld-grid__winter { grid-area: winter; }
  .realworld-grid__busfr { grid-area: busfr; }
  .realworld-grid__busen { grid-area: busen; }

  .project-meta { grid-template-columns: repeat(2, 1fr); }
  .project-gallery--trio { grid-template-columns: repeat(3, 1fr); }
  .project-gallery--cover {
    grid-template-columns: repeat(3, 1fr);
    grid-template-areas: "socials bus posters";
  }

  /* Eyebrow/heading column beside the body copy, same pattern as
     .about-intro's photo/text split. align-items:start (not the grid
     default stretch) keeps the eyebrow's own flex box at its natural
     height so it stays pinned to the top-left instead of vertically
     centering against the much-taller copy column beside it. */
  .project-section {
    display: grid;
    align-items: start;
    grid-template-columns: 220px 1fr;
    column-gap: var(--space-6);
  }
  .project-section .project-eyebrow { margin-bottom: 0; }
  .project-section__copy { grid-column: 2; }
  /* Cancels .project-approach__list's own 36px margin-top (needed on
     mobile, where the list stacks below the eyebrow) once the grid
     column split puts them side by side instead — without this the
     list's first heading sat 36px below the Approach eyebrow instead of
     lining up with it, per feedback. Scoped to :first-child — some
     Approach sections (e.g. mental-health-campaign's) have their own
     intro heading/paragraph before the list, and zeroing the margin
     unconditionally there removed the gap between that intro copy and
     "01" entirely, reading as cramped. Only the myhealthnb-launch case
     (list is the copy block's only/first content) needs this zeroed. */
  .project-section__copy > .project-approach__list:first-child { margin-top: 0; }

  /* Paragraph indent (see mobile-only rule above) doesn't apply once the
     grid column split itself provides the offset. Selector must match
     the mobile rule's specificity exactly (including the child combinator
     and :not(.project-eyebrow)) or the mobile rule — being more specific —
     wins here too regardless of source order, which is exactly what
     leaked the ~48px indent into desktop before this fix. */
  .project-section__copy > p:not(.project-eyebrow) { padding-left: 0; }
  .project-outcome__stats { margin-left: 0; }
  .project-quote { margin-left: 0; }

}

@media (min-width: 1024px) {
  /* Desktop */

  /* --- About page --- */
  .about-hero__heading { font-size: 4.5rem; }
  .about-hero__heading .hero-break { display: inline; }

  /* --- Home page --- */
  /* Font-size no longer jumps here — the base rule's clamp() already
     reaches 6.5rem exactly at this width and holds flat above it. Only
     the line-break STRUCTURE changes here: collapse from 4 lines to 2. */
  .home-hero__break { display: none; }

  /* --- Header & Navigation --- */
  /* Back to a single inline row: name, icon, tagline (natural DOM order) */
  .site-header__brand {
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }

  /* --- Shared: full-viewport sections' header-height subtraction
     (Project pages + Home page) — header shrinks again to 67px here
     (brand collapses to one row, just above); same reasoning as the
     769px tier, just a smaller pixel value. --- */
  .project-hero {
    min-height: calc(100vh - 67px);
    min-height: calc(100dvh - 67px);
  }
  .vertical-carousel {
    height: calc(100vh - 67px);
    height: calc(100dvh - 67px);
  }
  .video-carousel {
    height: calc(100vh - 67px);
    height: calc(100dvh - 67px);
  }
  .home-hero {
    min-height: calc(100vh - 67px);
    min-height: calc(100dvh - 67px);
  }

  /* --- About page --- */
  .about-intro {
    grid-template-columns: 260px 1fr;
    grid-template-areas:
      "photo heading"
      "photo text";
    /* Wider gap than mobile/tablet — measured against about-desktop.jpg,
       the photo-to-text gutter there is much closer to 72px than the
       24px that reads fine when the photo is only 110-220px wide. */
    column-gap: var(--space-6);
  }

  .about-stats__grid { grid-template-columns: 1fr 1fr; }

  /* --- Project page template --- */
  .project-meta { grid-template-columns: repeat(4, 1fr); }

  /* --- About page --- */
  /* Photo mosaic switches from the 3x3 mobile/tablet layout to the
     6-column x 2-row desktop layout — see about-desktop.jpg. */
  .photo-strip {
    aspect-ratio: 3 / 1;
    grid-template-columns: repeat(6, 1fr);
    grid-template-rows: repeat(2, 1fr);
    grid-template-areas:
      "paris muffins storr storr lumineers blossoms"
      "paris jump    storr storr cafe      blossoms";
  }

  /* --- Header & Navigation --- */
  .site-nav__links { display: flex; }
  .hamburger { display: none; }

  /* --- Footer --- */
  /* Links vertically centered against the logo (x-position unchanged —
     still space-between), per feedback. */
  .site-footer__top { align-items: center; }
}
