/* ============================================================
   IShowSpeed Foundation — main.css
   Mobile-first (390px reference). Tokens are layered:
     1. Primitives  — raw values
     2. Semantics   — purpose-named, alias primitives
     3. Component   — defined inline next to component rules
   Change a primitive to ripple a theme; reach for semantics in
   component code so intent reads cleanly.
   ============================================================ */


/* ---------- 1. TOKENS ---------- */

:root {
  /* --- Primitive: color --- */
  --color-black:        #000;
  --color-white:        #fff;
  --color-green-500:    #a9f42f;
  --color-green-300:    #d8e46e;
  --color-gray-900:     #202020;
  --color-gray-800:     #282828;

  /* --- Primitive: spacing (4px scale) --- */
  --space-1:  4px;
  --space-2:  8px;
  --space-3:  12px;
  --space-4:  16px;
  --space-5:  20px;
  --space-6:  24px;
  --space-7:  28px;
  --space-8:  32px;
  --space-9:  36px;

  /* --- Primitive: radius --- */
  --radius-sm:    8px;
  --radius-md:    10px;
  --radius-lg:    12px;
  --radius-pill:  999px;

  /* --- Primitive: type families ---
     Site-wide swap to Plus Jakarta Sans (both display + body — it has
     a strong display range at Medium/SemiBold/Bold/ExtraBold) and
     Atkinson Hyperlegible Mono for small uppercase tags. */
  --font-display: "Plus Jakarta Sans", system-ui, -apple-system, sans-serif;
  --font-body:    "Plus Jakarta Sans", system-ui, -apple-system, sans-serif;
  --font-mono:    "Atkinson Hyperlegible Mono", ui-monospace, "SF Mono", monospace;

  /* --- Primitive: font size — body scale (prose, UI, labels) ---
     Fluid via clamp(min, intercept + slope·vw, max). The intercept is
     tuned so the value LOCKS at min until viewport ≥560px and reaches
     max at 1200px. Mobile (<560) keeps the hand-tuned designed sizes;
     larger viewports scale up linearly. */
  --fs-body-sm:  16px;  /* button, tag, tracker label, goal subtitle */
  --fs-body-md:  clamp(18px, 16.25px + 0.313vw, 20px);   /* default body copy */
  --fs-body-lg:  clamp(20px, 16.5px + 0.625vw, 24px);    /* attribution */
  --fs-body-xl:  clamp(26px, 13.75px + 2.188vw, 40px);   /* pull quote */

  /* --- Primitive: font size — display scale (headlines, wordmarks) --- */
  --fs-display-xs:  clamp(25px, 18.875px + 1.094vw, 32px);  /* header wordmark */
  --fs-display-sm:  clamp(48px, 20px + 5vw, 80px);          /* tracker amount */
  --fs-display-md:  clamp(50px, 13.25px + 6.563vw, 92px);   /* hero headline */
  --fs-display-lg:  clamp(56px, 14px + 7.5vw, 104px);       /* "The Africa Fund" */

  /* --- Primitive: font weight --- */
  --fw-medium:     500;
  --fw-semibold:   600;
  --fw-bold:       700;
  --fw-extrabold:  800;

  /* --- Primitive: line height (unitless) --- */
  --lh-display:  1;   /* hero headline */
  --lh-flat:     1;      /* aligned wordmarks / tags */
  --lh-button:   1.25;   /* button, attribution, tracker label */
  --lh-quote:    1.4;    /* pull quote, tracker goal */
  --lh-body:     1.45;    /* default prose */

  /* --- Primitive: letter spacing --- */
  --tracking-default:   -0.01em;  /* body copy */
  --tracking-tight:     -0.03em;  /* large body, attribution */
  --tracking-tighter:   -0.04em;  /* display amounts, tags */
  --tracking-tightest:  -0.05em;  /* display headlines */
  --tracking-logo:      -0.06em;  /* header wordmark */

  /* --- Semantic: surface & text --- */
  --bg:              var(--color-black);
  --fg:              var(--color-white);
  --accent:          var(--color-green-500);
  --accent-soft:     var(--color-green-300);

  --text-strong:     rgba(255, 255, 255, 1);
  --text-muted:      rgba(255, 255, 255, 0.7);
  --text-subtle:     rgba(255, 255, 255, 0.6);
  --text-faint:      rgba(255, 255, 255, 0.5);
  --text-logo:       rgba(255, 255, 255, 0.85);
  --text-on-card:    rgba(255, 255, 255, 0.83);
  --text-on-track:   rgba(255, 255, 255, 0.4);
  --text-card-lede:  rgba(255, 255, 255, 0.64);

  --surface-card:    var(--color-gray-900);
  --surface-inner:   rgba(0, 0, 0, 0.6);
  --surface-track:   var(--color-gray-800);

  --border-subtle:   rgba(255, 255, 255, 0.07);
  --border-fainter:  rgba(255, 255, 255, 0.08);
  --border-track:    rgba(255, 255, 255, 0.29);

  --divider:         rgba(255, 255, 255, 0.07);

  /* --- Layout ---
     Re-declared at min-width breakpoints (see §16 RESPONSIVE) so all
     layout-driven values scale through these tokens. Component code
     reads these; primitives stay constant. */
  --page-gutter:    20px;             /* static mobile padding (no centring bands) */
  --content-max:    100%;             /* fluid on mobile; capped from ≥560 up: 520 → 760 → 1200 */
  --section-pad-y:  var(--space-9);   /* 36px → 64 → 96 at desktop */
  --col-gap:        var(--space-6);   /* multi-col gutters at desktop */
  --hero-image-h:   193px;            /* 193 → 320 → 460 */

  /* --- Motion: easing --- */
  --ease-out-soft:     cubic-bezier(0.16, 1, 0.3, 1);   /* entrance settle */
  --ease-in-out-snap:  cubic-bezier(0.65, 0, 0.35, 1);  /* write-on cubic */

  /* --- Motion: live pulse (campaign tag) ---
     Ring stagger is derived from the total cycle (½ cycle), so all
     animations stay locked in sync no matter what you tweak.
       • --pulse-duration  → one ring's pulse animation time
       • --pulse-rest      → silent gap added between consecutive pulses
     Total cycle = duration + rest. Set rest to 0 for a continuous
     heartbeat; raise it for a more deliberate pulse-pause-pulse beat. */
  --pulse-duration:            3.7s;
  --pulse-rest:                1.2s;
  --pulse-cycle:               calc(var(--pulse-duration) + var(--pulse-rest));
  --pulse-ring-ease:           var(--ease-out-soft);
  --pulse-ring-delay:          calc(var(--pulse-cycle) / 2);
  --pulse-ring-start-opacity:  0.35;
  --pulse-ring-max-scale:      4.2;
  --pulse-icon-scale-peak:     1.42;
  --pulse-icon-scale-dip:      0.96;
  --pulse-icon-opacity-dip:    0.65;
  --pulse-icon-glow-blur:      20px;
  --pulse-icon-glow-color:     rgba(169, 244, 47, 0.55);
}


/* ---------- 2. RESET ---------- */

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

html, body {
  margin: 0;
  padding: 0;
}

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

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

p { margin: 0; }

h1, h2, h3, h4 { margin: 0; font-weight: var(--fw-extrabold); }


/* ---------- 3. BASE ---------- */

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--fs-body-md);
  font-weight: var(--fw-medium);
  line-height: var(--lh-body);
  letter-spacing: var(--tracking-default);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}


/* ---------- 4. LAYOUT ---------- */

.page {
  position: relative;
  max-width: var(--content-max);
  margin: 0 auto;
}

.divider {
  width: 100%;
  height: 1px;
  background: var(--divider);
  border: 0;
  margin: 0;
}

/* Vertical side rails (Figma Vector 29 + 30): start at the second
   horizontal divider and run to the bottom of the page. */
.rails {
  position: relative;
}
.rails::before,
.rails::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--divider);
  pointer-events: none;
}
.rails::before { left: var(--page-gutter); }
.rails::after  { right: var(--page-gutter); }


/* ---------- 5. HEADER ----------
   Sticky on scroll. Two states: rest (full-size logo, donate CTA
   hidden) and `.is-compact` (logo scales down, donate CTA slides in
   from the right). The trigger flips when the hero donate button
   scrolls above the viewport — see /assets/js/header.js. */

.site-header {
  position: sticky;
  top: 0;
  z-index: 20;

  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);

  /* 24px all around so the logo sits with equal breath from the
     viewport top + sides + the hero card below. Desktop (§16c)
     tightens the verticals for the wider hero grid. */
  padding: var(--space-6);
  /* Transparent at rest so the navbar sits over the hero photo
     (mobile) or the dark page bg (desktop); fades to a translucent
     dark glass when the user starts scrolling. */
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: 1px solid transparent;
  transition:
    background 280ms var(--ease-out-soft),
    backdrop-filter 280ms var(--ease-out-soft),
    -webkit-backdrop-filter 280ms var(--ease-out-soft),
    border-color 280ms var(--ease-out-soft);
}
.site-header.is-scrolled {
  background: rgba(0, 0, 0, 0.96);
  backdrop-filter: blur(15px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom-color: var(--divider);
}
.site-header__logo {
  position: relative;
  width: 189px;
  height: 40px;
  font-family: var(--font-body);
  font-weight: var(--fw-extrabold);
  font-size: var(--fs-display-xs);
  line-height: 24px;
  letter-spacing: var(--tracking-logo);
  color: var(--text-logo);

  transform-origin: left center;
  /* Rest: nudge down so visible space above the mark matches the gap
     below it (header bottom pad + hero top pad). On scroll, drop the
     translate so the logo snaps up to true vertical center inside the
     now-bordered header, in the same 220ms transform interpolation. */
  transform: translateY(6px);
  transition: transform 220ms cubic-bezier(0.32, 0.72, 0, 1);
  will-change: transform;
  backface-visibility: hidden;
}
.site-header.is-scrolled .site-header__logo {
  transform: translateY(0) scale(0.82);
}
.site-header__line {
  display: block;
}
.site-header__line--indent {
  padding-left: 56px;
}
.site-header__mark {
  position: absolute;
  top: 1.5px;
  left: 0;
  height: 37px;
}

/* Compact donate CTA — hidden at rest, slides in when `.is-compact`
   is toggled on the header. `visibility` is delayed on hide so the
   element stays focusable through the fade-out, then drops out of
   the tab order once invisible. */
.site-header__cta {
  display: flex;
  opacity: 0;
  transform: translateX(8px);
  pointer-events: none;
  visibility: hidden;
  transition:
    opacity 240ms var(--ease-out-soft),
    transform 320ms var(--ease-out-soft),
    visibility 0s linear 320ms;
}
.site-header__cta .btn {
  padding: 9px 16px;
  font-size: 13px;
  letter-spacing: -0.02em;
  gap: 0;
}
.site-header__cta .btn__arrow {
  display: none;
}

.site-header.is-compact .site-header__cta {
  opacity: 1;
  transform: translateX(0);
  pointer-events: auto;
  visibility: visible;
  transition:
    opacity 280ms var(--ease-out-soft),
    transform 320ms var(--ease-out-soft),
    visibility 0s linear 0s;
}

@media (prefers-reduced-motion: reduce) {
  .site-header__logo,
  .site-header__cta {
    transition: opacity 120ms linear, visibility 0s linear 120ms;
    transform: none !important;
  }
  .site-header.is-compact .site-header__cta {
    transition: opacity 120ms linear, visibility 0s linear 0s;
  }
}


/* ---------- 6. HERO ----------
   Mobile / tablet: full-bleed hero photo as the section background, a
   dark gradient at the bottom for legibility, and copy + mini-tracker
   pill + donate button stacked over the image, anchored to the bottom
   of the card via margin-top:auto.
   Desktop (≥1080, see §16c): switches to a two-column grid — copy on
   the left, the same photo on the right — with no overlay. */

.hero {
  position: relative;
  overflow: hidden;
  /* Hugs the viewport edges on mobile with a 4px breath — gives the
     hero photo a near-edge-to-edge presence while still showing a
     hint of the page bg as a frame. Desktop resets to 0 (§16c). */
  margin: 0 4px;
  padding: var(--space-6);
  border-radius: var(--radius-sm);
  min-height: 580px;
  display: flex;
  flex-direction: column;
}
.hero__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 30%;
  z-index: 0;
}
/* Dark gradient at the bottom of the hero card — fades from clear at
   the top quarter to a near-opaque black past the midline so the
   overlaid title/lede/pill stay readable against any photo. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, transparent 20%, rgba(0, 0, 0, 0.71) 50%);
  pointer-events: none;
  z-index: 1;
}
.hero__inner {
  position: relative;
  z-index: 2;
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.hero__copy {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.hero__title {
  position: relative;
  font-family: var(--font-display);
  font-weight: var(--fw-medium);
  font-size: 44px;
  line-height: 1.1;
  letter-spacing: -0.04em;
  color: var(--text-strong);
  margin: 0;
  max-width: 100%;
}
.hero__title-line {
  display: block;
}
.hero__title-play {
  position: absolute;
  /* em-based so the Play badge tracks the title font-size at every
     breakpoint. Tuned against the 44px mobile baseline:
     51.1 ≈ 1.16em top, 199 ≈ 4.52em left, 102×58 ≈ 2.32em × 1.32em. */
  top: 1.16em;
  left: 4.52em;
  width: 2.32em;
  height: 1.32em;
  pointer-events: none;
}
.hero__lede {
  font-family: var(--font-body);
  font-weight: var(--fw-medium);
  font-size: 18px;
  line-height: 1.5;
  letter-spacing: -0.02em;
  color: var(--text-strong);
  margin: 0;
  max-width: 100%;
}
.hero__lede-muted {
  color: rgba(255, 255, 255, 0.6);
}

/* Mobile donate CTA — full-width inside the hero card with label-left,
   arrow-right, larger padding (20×24), Plus Jakarta Sans Bold per the
   Figma. Desktop (§16c) restores the compact inline button. */
.hero .btn {
  width: 100%;
  justify-content: space-between;
  padding: 20px 24px;
  font-weight: var(--fw-bold);
  letter-spacing: -0.01em;
}
.hero .btn__arrow {
  width: 16px;
  height: 12px;
}


/* ---------- 6b. HERO TRACKER PILL ----------
   Compact glassmorphic pill living inside the hero — shows the live
   Africa Fund total alongside the campaign label. Filled at build time
   from src/_data/fundraiser.js and updated live by fundraiser.js
   (which writes to [data-pill-raised] / [data-pill-goal] on each
   poll). Stretches to fill its column on mobile; caps at 360px on
   desktop. */

.hero-pill {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  /* Full-width on mobile; capped at 360px in the desktop column (§16c). */
  padding: 12px 20px;
  border-radius: 16px;
  background: rgba(97, 97, 97, 0.31);
  backdrop-filter: blur(25.5px);
  -webkit-backdrop-filter: blur(25.5px);
}
.hero-pill__label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-medium);
  font-size: 16px;
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: var(--text-strong);
  white-space: nowrap;
  min-width: 0;
}
.hero-pill__live {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}
.hero-pill__bracket {
  color: rgba(255, 255, 255, 0.36);
}
.hero-pill__live-text {
  color: var(--accent);
}
.hero-pill__fund-name {
  overflow: hidden;
  text-overflow: ellipsis;
}
.hero-pill__stats {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  flex-shrink: 0;
}
.hero-pill__amount {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-medium);
  font-size: 16px;
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: var(--text-strong);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.hero-pill__dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
}
.hero-pill__goal {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-medium);
  font-size: 12px;
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: rgba(255, 255, 255, 0.36);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}


/* ---------- 7. QUOTE ---------- */

.quote {
  padding: var(--space-8) var(--page-gutter) var(--space-9);
  display: flex;
  flex-direction: column;
  gap: var(--space-5); /* default rhythm; specific gaps below */
}
.quote__mark {
  width: 32px;
  height: 30px;
}
.quote__mark--close {
  align-self: flex-end;
  transform: rotate(180deg);
  margin-top: var(--space-5);
}

.quote__text {
  font-family: var(--font-body);
  font-weight: var(--fw-medium);
  font-size: var(--fs-body-xl);
  line-height: var(--lh-quote);
  letter-spacing: var(--tracking-tight);
  color: var(--text-strong);    /* fallback if JS doesn't run */
  width: 278px;
  max-width: 100%;
  margin-top: var(--space-5); /* 20px between quote-mark and text */

  /* Driven by quote-reveal.js: 0..1 as the paragraph travels through
     the reveal window in the viewport. --n is the word count. */
  --reveal: 0;
  --reveal-window: 0.18;
}
/* Per-word gray → white interpolation. Each word claims a slice of
   the [0..1] reveal range starting at --i / --n; once --reveal passes
   that threshold the word's alpha lifts over --reveal-window's length. */
.quote__word {
  --threshold: calc(var(--i) / var(--n) * (1 - var(--reveal-window)));
  --word-t: clamp(0, calc((var(--reveal) - var(--threshold)) / var(--reveal-window)), 1);
  color: rgba(255, 255, 255, calc(0.25 + 0.75 * var(--word-t)));
}

.quote__image {
  position: relative;
  width: var(--quote-frame-w, 100%);
  height: var(--quote-frame-h, 144px);
  /* Auto margins can't go negative, so when the frame is wider than
     100% it would snap to the left edge. Computed inline-margins keep
     it centered through the full width range (sub-100% AND over). */
  margin-left:  calc((100% - var(--quote-frame-w, 100%)) / 2);
  margin-right: calc((100% - var(--quote-frame-w, 100%)) / 2);
  border-radius: var(--radius-sm);
  overflow: hidden;
  margin-top: var(--space-5); /* 20px gap above + below the image */
}
.quote__image-inner {
  position: absolute;
  left: 0;
  width: 100%;
  top: -18%;
  height: 136%;
  object-fit: cover;
  will-change: transform;
}

.quote__attribution {
  font-family: var(--font-body);
  font-weight: var(--fw-medium);
  font-size: var(--fs-body-lg);
  line-height: var(--lh-button);
  letter-spacing: var(--tracking-tight);
  color: var(--text-faint);
  text-align: right;
  margin-top: var(--space-8); /* 32px above attribution */
}


/* ---------- 8. AFRICA FUND ---------- */

.campaign {
  /* Top padding is 0 because .campaign__hero (the green-bg block on
     mobile) carries its own vertical breathing room. The bottom
     padding sits between the lede and whatever follows the section.
     Desktop (≥1080) re-introduces a section padding-top — see §16c. */
  padding: 0 var(--page-gutter) var(--space-9);
  /* Clears the sticky header when the /africa deep link scrolls here. */
  scroll-margin-top: 88px;
}

/* Green-bg "campaign hero" block (mobile/tablet only). Wraps the tag,
   title, and photo cluster on accent green with black type. Extends
   to the viewport edges via negative inline margins (escapes the
   .campaign horizontal padding). At ≥1080 (see §16c) it collapses to
   display:contents so its children become direct grid items of the
   desktop campaign grid. */
.campaign__hero {
  position: relative;
  margin-inline: calc(-1 * var(--page-gutter));
  padding: var(--space-9) var(--page-gutter);
  background: var(--accent);
  color: var(--color-black);
  overflow: hidden;
  margin-bottom: var(--space-8);
}

/* Tag inside the green hero — strip the pulsing-dot decoration, swap
   to the Atkinson Hyperlegible Mono uppercase treatment in black. */
.campaign__hero .campaign__tag {
  margin-left: 0;
  margin-bottom: var(--space-6);
}
.campaign__hero .campaign__tag-pulse {
  display: none;
}
.campaign__hero .campaign__tag-text {
  font-family: var(--font-mono);
  font-weight: var(--fw-medium);
  font-size: 16px;
  letter-spacing: 0;
  line-height: 1;
  color: var(--color-black);
  text-transform: uppercase;
}

/* Title inside the green hero — Plus Jakarta Sans Bold (not Extrabold)
   in black, per the Figma. */
.campaign__hero .campaign__title {
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  font-size: 56px;
  line-height: 0.96;
  letter-spacing: -0.05em;
  color: var(--color-black);
  width: auto;
  max-width: 14ch;
  margin-bottom: var(--space-9);
}

.campaign__tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2); /* 6 → use 8 from scale; visual diff is 2px */
  margin-left: -6px; /* dot has glow halo, lift visually flush to gutter */
  margin-bottom: var(--space-5);
}
.campaign__tag-pulse {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
}
.campaign__tag-pulse::before,
.campaign__tag-pulse::after {
  content: "";
  position: absolute;
  inset: 25%; /* originate from the visual center of the 16px icon */
  border-radius: 50%;
  background: var(--accent);
  opacity: 0;
  transform: scale(1);
  animation: tag-pulse-ring var(--pulse-cycle) var(--pulse-ring-ease) infinite;
  pointer-events: none;
}
.campaign__tag-pulse::after {
  animation-delay: var(--pulse-ring-delay);
}
.campaign__tag-dot {
  width: 16px;
  height: 16px;
  position: relative; /* keep icon above the pulse rings */
  animation: tag-pulse-icon var(--pulse-cycle) ease-in-out infinite;
  will-change: transform, opacity, filter;
}

/* Ring active phase ends at 40% of the cycle, then holds invisible
   to 50% (where the staggered partner ring emits). That ~10% slice
   on either side of the cycle is the silent gap controlled by
   --pulse-rest — grow rest to grow the gap. */
@keyframes tag-pulse-ring {
  0% {
    opacity: var(--pulse-ring-start-opacity);
    transform: scale(1);
  }
  40%, 100% {
    opacity: 0;
    transform: scale(var(--pulse-ring-max-scale));
  }
}

@keyframes tag-pulse-icon {
  0%, 50%, 100% {
    transform: scale(var(--pulse-icon-scale-peak));
    opacity: 1;
    filter: drop-shadow(0 0 var(--pulse-icon-glow-blur) var(--pulse-icon-glow-color));
  }
  25%, 75% {
    transform: scale(var(--pulse-icon-scale-dip));
    opacity: var(--pulse-icon-opacity-dip);
    filter: drop-shadow(0 0 0 transparent);
  }
}

@media (prefers-reduced-motion: reduce) {
  .campaign__tag-pulse::before,
  .campaign__tag-pulse::after,
  .campaign__tag-dot {
    animation: none;
  }
}
.campaign__tag-text {
  font-family: var(--font-body);
  font-weight: var(--fw-bold);
  font-size: var(--fs-body-sm);
  letter-spacing: var(--tracking-tighter);
  color: var(--accent);
  line-height: var(--lh-flat);
}

.campaign__title {
  font-family: var(--font-body);
  font-weight: var(--fw-extrabold);
  font-size: var(--fs-display-lg);
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-tightest);
  color: var(--text-strong);
  width: 253px;
  max-width: 100%;
  margin-bottom: var(--space-6);
}

/* Photo cluster — overlapping diagonal layout matching the Figma:
   SPA in upper-right, ANGOLA in lower-left with a ~120px Y offset
   between their tops. Container is positioned absolutely and lives
   inside .campaign__hero, which is overflow: hidden so any edge
   spillover at narrow viewports gets trimmed cleanly.
   At ≥840 the existing same-specificity rule below converts this to
   a 12-col side-by-side grid; at ≥1080 photos go back to flow-based
   with grid-column allocations. */
.campaign__images {
  position: relative;
  height: 417px;
  max-width: none;
  margin: 0;
}
.campaign__image {
  position: absolute;
  border-radius: 0;
  object-fit: cover;
  will-change: transform; /* parallax.js drives translate3d on scroll */
}
.campaign__image--left {
  left: 0;
  top: 120px;
  width: 198px;
  height: 297px;
}
.campaign__image--right {
  left: 131px;
  top: 0;
  width: 211px;
  height: 264px;
}

.campaign__lede {
  font-size: var(--fs-body-md);
  line-height: var(--lh-body);
  letter-spacing: var(--tracking-default);
  color: var(--text-strong);
  width: 339px;
  max-width: 100%;
  /* No bottom margin on mobile — the .tracker-section sibling carries
     its own padding-top, so spacing to the tracker comes from there. */
}
.campaign__lede-muted {
  color: var(--text-card-lede);
}


/* ---------- 9. TRACKER ----------
   The live <africa-fund-tracker> Web Component lives in its own
   section below the Africa Fund story block (see .tracker-section).
   Component styles live in /assets/css/africa-fund-tracker.css. */
.tracker-section {
  padding: var(--space-9) var(--page-gutter) var(--space-9);
  display: flex;
  justify-content: center;
}


/* ---------- 10. BUTTONS ---------- */

.btn {
  --btn-bg: var(--accent);
  --btn-fg: var(--color-black);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2); /* 8 */
  padding: 12px 20px;
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-weight: var(--fw-extrabold);
  font-size: var(--fs-body-sm);
  line-height: var(--lh-button);
  letter-spacing: var(--tracking-tight);
  white-space: nowrap;
  background: var(--btn-bg);
  color: var(--btn-fg);
  cursor: pointer;
  transition: transform 120ms ease, opacity 120ms ease;
}
.btn:hover { opacity: 0.9; }
.btn:active { transform: scale(0.98); }

.btn--primary {
  --btn-bg: var(--accent);
  --btn-fg: var(--color-black);
}
.btn--light {
  --btn-bg: var(--color-white);
  --btn-fg: var(--color-black);
}

.btn__arrow {
  display: inline-flex;
  width: 13px;
  height: 10px;
  color: var(--btn-fg);
}
.btn__arrow svg {
  width: 100%;
  height: 100%;
}

/* ---------- 11. SVG SIZE HELPERS ---------- */

.icon-dot { width: 16px; height: 16px; }


/* ---------- 12. WORLD MAP ---------- */

/* Equirectangular world map — 2:1 is the projection's native ratio
   (longitude 360° wide, latitude 180° tall). Rendered by worldmap.js. */
.worldmap {
  position: relative;
  width: 100%;
  aspect-ratio: 2 / 1;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg);
}
.worldmap__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* Popup overlay — DOM siblings of the canvas, positioned in canvas px.
   Each popup is a stack (pill on top, video card below) that scales
   from 0 → 1 with a bouncy genie pop on entrance, holds, then squeezes
   back into the anchor on exit. */
.worldmap__popups {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.worldmap__popup {
  position: absolute;
  width: 0;
  height: 0;
}

/* The animated element. translate(-50%, -50%) centers the stack on the
   location anchor so the genie scale-from-zero collapses to that exact
   point. Linear timing lets the keyframes do all the bounce shaping. */
.worldmap__popup-stack {
  position: absolute;
  left: 0;
  top: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  opacity: 0;
  transform: translate(-50%, -50%) scale(0);
  transform-origin: 50% 50%;
  animation: worldmap-genie var(--popup-life, 5s) linear forwards;
  will-change: transform, opacity;
}

.worldmap__popup-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 9px 4px 7px;
  background: rgba(40, 65, 10, 0.85);
  border: 0.5px solid rgba(169, 244, 47, 0.45);
  border-radius: var(--radius-pill);
  color: var(--text-strong);
  font-family: var(--font-body);
  font-weight: var(--fw-bold);
  font-size: 11px;
  letter-spacing: 0.04em;
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.35);
}
.worldmap__popup-pill-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 5px rgba(169, 244, 47, 0.85);
  animation: worldmap-pill-dot 1.4s ease-in-out infinite;
}
.worldmap__popup-pill-name {
  display: inline-block;
}

.worldmap__popup-card {
  width: 84px;
  height: 58px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: #000;
  box-shadow:
    0 10px 22px rgba(0, 0, 0, 0.55),
    0 0 0 0.5px rgba(255, 255, 255, 0.12);
}
.worldmap__popup-video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

/* Genie pop: scale-from-zero with bouncy overshoot on enter, hold,
   then anticipation + squeeze back into the anchor on exit. */
@keyframes worldmap-genie {
  0%    { opacity: 0; transform: translate(-50%, -50%) scale(0);    }
  2.5%  { opacity: 1;                                                }
  6%    { transform: translate(-50%, -50%) scale(1.09);             }
  10%   { transform: translate(-50%, -50%) scale(0.97);             }
  14%   { transform: translate(-50%, -50%) scale(1.02);             }
  18%   { transform: translate(-50%, -50%) scale(1);                }
  92%   { opacity: 1; transform: translate(-50%, -50%) scale(1);    }
  94%   { transform: translate(-50%, -50%) scale(1.04);             }
  97%   { transform: translate(-50%, -50%) scale(0.55);             }
  99.5% { opacity: 1; transform: translate(-50%, -50%) scale(0.08); }
  100%  { opacity: 0; transform: translate(-50%, -50%) scale(0);    }
}

@keyframes worldmap-pill-dot {
  0%, 100% { opacity: 1;    transform: scale(1);    }
  50%      { opacity: 0.55; transform: scale(0.82); }
}

/* Reduced motion: skip the bounce — fade in/out at full size. */
@media (prefers-reduced-motion: reduce) {
  .worldmap__popup-stack {
    animation-name: worldmap-genie-reduced;
  }
  .worldmap__popup-pill-dot {
    animation: none;
  }
}
@keyframes worldmap-genie-reduced {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(1); }
  12%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  88%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1); }
}

/* Interactive hover label — shown above the hovered country dot in place
   of the auto-cycling video popups. Plain white text on the map; no
   capsule. Toggled by the .is-visible class. */
.worldmap__hover-pill {
  position: absolute;
  color: var(--text-strong);
  font-family: var(--font-body);
  font-weight: var(--fw-bold);
  font-size: 11px;
  letter-spacing: 0.04em;
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.85);
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, calc(-100% - 10px)) scale(0.92);
  transition:
    opacity 0.16s ease-out,
    transform 0.18s cubic-bezier(0.34, 1.5, 0.64, 1),
    left 0.16s cubic-bezier(0.16, 1, 0.3, 1),
    top 0.16s cubic-bezier(0.16, 1, 0.3, 1);
}
.worldmap__hover-pill.is-visible {
  opacity: 1;
  transform: translate(-50%, calc(-100% - 10px)) scale(1);
}
@media (prefers-reduced-motion: reduce) {
  .worldmap__hover-pill {
    transition: opacity 0.12s linear;
    transform: translate(-50%, calc(-100% - 10px));
  }
  .worldmap__hover-pill.is-visible {
    transform: translate(-50%, calc(-100% - 10px));
  }
}

/* Where We Go — section frame (matches the campaign pattern: gutters,
   title, lede, then the visual). */
.where-we-go {
  padding: var(--space-8) var(--page-gutter) var(--space-9);
}
/* Typography matches the Africa Fund title — Plus Jakarta Sans Bold
   (not Extrabold), tighter line-height. Color stays white since this
   section sits on the dark page bg, not the green Africa Fund block. */
.where-we-go__title {
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  font-size: var(--fs-display-lg);
  line-height: 0.96;
  letter-spacing: var(--tracking-tightest);
  color: var(--text-strong);
  margin-bottom: var(--space-5);
  width: 70%;
}
.where-we-go__lede {
  font-size: var(--fs-body-md);
  line-height: var(--lh-body);
  letter-spacing: var(--tracking-default);
  color: var(--text-strong);
  margin-bottom: var(--space-8);
}
.where-we-go__lede-muted {
  color: var(--text-card-lede);
}
.where-we-go .worldmap {
  margin-bottom: var(--space-7);
}

/* Fund header — names the fund those listed countries belong to.
   Quiet white label, no icon. Reveals in sync with the country-list
   cascade (same easing/duration as .country-list__item, no per-item
   delay) so it lands together with the first row.
   Repeats above each fund's own list as more funds launch. */
.country-list__fund {
  font-family: var(--font-body);
  font-weight: var(--fw-bold);
  font-size: var(--fs-body-sm);
  letter-spacing: var(--tracking-tighter);
  line-height: var(--lh-flat);
  color: var(--text-strong);
  margin: 0 0 var(--space-5);

  opacity: 0;
  transform: translateY(6px);
  transition:
    opacity 520ms var(--ease-out-soft),
    transform 520ms var(--ease-out-soft);
}
.country-list__fund.is-revealed {
  opacity: 1;
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .country-list__fund {
    transition: opacity 220ms linear;
    transform: none;
  }
}

/* Country list — two-column hairline grid with indexed rows. CSS
   counter generates the zero-padded number, removing the need for
   per-item template logic. Brutalist/Linear-esque: small uppercase
   type, tight tracking, faint dividers, no decorative glow. Each
   row reveals on scroll-in with a staggered delay driven by --i. */
.country-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--space-5);
  row-gap: 0;
  border-top: 1px solid var(--border-fainter);
  counter-reset: country;
}
.country-list__item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 0;
  border-bottom: 1px solid var(--border-fainter);
  counter-increment: country;
  font-family: var(--font-body);
  font-weight: var(--fw-semibold);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-strong);
  line-height: 1;
  opacity: 0;
  transform: translateY(6px);
  transition:
    opacity 520ms var(--ease-out-soft) calc(var(--i, 0) * 32ms),
    transform 520ms var(--ease-out-soft) calc(var(--i, 0) * 32ms);
}
.country-list__item::before {
  content: counter(country, decimal-leading-zero);
  flex-shrink: 0;
  min-width: 1.4em;
  font-weight: var(--fw-medium);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.country-list__dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
  /* Ambient breathing pulse — asymmetric keyframe (quick rise, slow
     decay) feels organic. Each row sets its own --pulse-delay in the
     template from a hand-shuffled array, so the 20 dots scatter
     across the 4s cycle without any detectable linear pattern. */
  animation: country-dot-breathe 2s ease-in-out infinite;
  animation-delay: var(--pulse-delay, 0s);
}

@keyframes country-dot-breathe {
  0%   { opacity: 0.60; transform: scale(0.89); }
  35%  { opacity: 1.00; transform: scale(1.00); }
  65%  { opacity: 0.85; transform: scale(0.95); }
  100% { opacity: 0.60; transform: scale(0.92); }
}

@media (prefers-reduced-motion: reduce) {
  .country-list__dot {
    animation: none;
    opacity: 1;
  }
}
.country-list__name {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.country-list.is-revealed .country-list__item {
  opacity: 1;
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .country-list__item {
    transition: opacity 220ms linear;
    transform: none;
  }
}


/* ---------- 13. HERO ANIMATIONS ---------- */

/* Initial state for all entrance targets: hidden + reset transform.
   CSS is in <head> so this paints synchronously — no FOUC.
   Note: .hero__lede uses a mask sweep instead of opacity (see below). */
.hero__image,
.hero__title-line,
.hero__title-play,
.hero-pill,
.hero .btn {
  opacity: 0;
  animation-fill-mode: both;
  animation-timing-function: var(--ease-out-soft);
}

.hero__image                       { animation: hero-rise    560ms 80ms  both var(--ease-out-soft); }
.hero__title-line:nth-child(1)     { animation: hero-rise-sm 480ms 260ms both var(--ease-out-soft); }
.hero__title-line:nth-child(2)     { animation: hero-rise-sm 480ms 380ms both var(--ease-out-soft); }
.hero__title-play                  { animation: hero-fade    260ms 560ms both var(--ease-out-soft); }
.hero-pill                         { animation: hero-rise-sm 600ms 440ms both var(--ease-out-soft); }
.hero .btn                         { animation: hero-rise-sm 680ms 560ms both var(--ease-out-soft); }

@keyframes hero-rise    { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes hero-rise-sm { from { opacity: 0; transform: translateY(8px);  } to { opacity: 1; transform: translateY(0); } }
@keyframes hero-fade    { from { opacity: 0; } to { opacity: 1; } }

/* Lede top-down sweep — mask gradient (~0.5x element height fade zone)
   slides from below the text upward, so each visual line fades in as the
   edge passes through it. Reads as "loading line by line, top to bottom". */
.hero__lede {
  -webkit-mask-image: linear-gradient(180deg, #000 30%, transparent 70%);
          mask-image: linear-gradient(180deg, #000 30%, transparent 70%);
  -webkit-mask-size: 100% 250%;
          mask-size: 100% 250%;
  -webkit-mask-position: 0% 100%;
          mask-position: 0% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  animation: lede-reveal 1800ms 320ms forwards var(--ease-out-soft);
}

@keyframes lede-reveal {
  to {
    -webkit-mask-position: 0% 0%;
            mask-position: 0% 0%;
  }
}

/* Play SVG write-on — each letter starts with its fill hidden and a
   thin accent stroke. The animation traces the stroke around the
   letter (stroke-dashoffset 100→0, normalized by pathLength="100" in
   the svg), then in the last third of each letter's window crossfades
   the stroke out and the fill in. End state: solid green letter, no
   stroke. */
.play__letter {
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-dasharray: 100 100;
  stroke-dashoffset: 100;
  fill-opacity: 0;
}
/* Letter delays sequenced after the hero entrance — the play container
   itself fades in at 560ms (see hero-fade above); these begin once the
   container is mid-fade so the trace reads as continuous. */
.play__letter--p { animation: play-draw 560ms 640ms  forwards var(--ease-in-out-snap); }
.play__letter--l { animation: play-draw 320ms 920ms  forwards var(--ease-in-out-snap); }
.play__letter--a { animation: play-draw 480ms 1080ms forwards var(--ease-in-out-snap); }
.play__letter--y { animation: play-draw 500ms 1360ms forwards var(--ease-in-out-snap); }

@keyframes play-draw {
  0%   { stroke-dashoffset: 100; fill-opacity: 0; stroke-opacity: 1; }
  70%  { stroke-dashoffset: 0;   fill-opacity: 0; stroke-opacity: 1; }
  100% { stroke-dashoffset: 0;   fill-opacity: 1; stroke-opacity: 0; }
}

/* Reduced motion: full bypass. Everything visible immediately, no
   animations run. */
@media (prefers-reduced-motion: reduce) {
  .hero__image,
  .hero__title-line,
  .hero__title-play,
  .hero-pill,
  .hero .btn {
    opacity: 1;
    transform: none;
    animation: none;
  }
  .hero__lede {
    -webkit-mask-image: none;
            mask-image: none;
    animation: none;
  }
  .play__letter {
    fill-opacity: 1;
    stroke-opacity: 0;
    stroke-dashoffset: 0;
    animation: none;
  }
}


/* ---------- 14. QUOTE ANIMATIONS ---------- */

/* Initial hidden state — paints synchronously, no FOUC.
   Follows the hero's main entrance (peaks 80–560ms) with a 700ms base delay
   so it reads as a continuation, not a competing beat. */
.quote__mark,
.quote__text,
.quote__image,
.quote__attribution {
  opacity: 0;
  animation-fill-mode: both;
  animation-timing-function: var(--ease-out-soft);
}

.quote__mark:not(.quote__mark--close) { animation: hero-rise-sm       480ms 700ms  both var(--ease-out-soft); }
.quote__text:nth-of-type(1)           { animation: hero-rise-sm       480ms 820ms  both var(--ease-out-soft); }
.quote__image                         { animation: hero-rise-sm       480ms 940ms  both var(--ease-out-soft); }
.quote__text:nth-of-type(2)           { animation: hero-rise-sm       480ms 1060ms both var(--ease-out-soft); }
.quote__attribution                   { animation: hero-rise-sm       480ms 1180ms both var(--ease-out-soft); }
.quote__mark--close                   { animation: quote-rise-flipped 480ms 1300ms both var(--ease-out-soft); }

/* Dedicated keyframe preserves the 180° rotation through the rise,
   so the closing mark doesn't un-flip mid-animation. */
@keyframes quote-rise-flipped {
  from { opacity: 0; transform: translateY(8px) rotate(180deg); }
  to   { opacity: 1; transform: translateY(0)   rotate(180deg); }
}

@media (prefers-reduced-motion: reduce) {
  .quote__mark,
  .quote__text,
  .quote__image,
  .quote__attribution {
    opacity: 1;
    animation: none;
  }
  .quote__mark--close {
    transform: rotate(180deg);
  }
}


/* ---------- 14b. FOOTER ----------
   Linear-esque: roomy vertical stack inside the rails, with the
   wordmark and quiet legal copy. Lives under a standard .divider so
   the framed page closes cleanly. */
.site-footer {
  padding: 0 var(--page-gutter) var(--space-9);
  display: flex;
  flex-direction: column;
  gap: var(--space-9);
}

.site-footer__mark {
  display: block;
  width: 148px;
  height: auto;
  opacity: 0.9;
}

.site-footer__legal {
  font-family: var(--font-body);
  font-weight: var(--fw-medium);
  font-size: 12px;
  line-height: 1.6;
  letter-spacing: var(--tracking-default);
  color: var(--text-faint);
  max-width: 320px;
}
.site-footer__legal strong {
  color: var(--text-muted);
  font-weight: var(--fw-semibold);
}

/* ---------- 15. WEBGL REVEAL ---------- */

/* Full-viewport canvas that renders the reveal effect for every
   img[data-webgl] in sync with the DOM. `has-webgl` is set by JS only
   when WebGL2 is supported and motion isn't reduced — otherwise images
   render normally with their existing CSS animations.
   Canvas sits above the body background (z 0) and the page content sits
   above the canvas (z 1); tagged images are opacity 0, so the
   shader-rendered image shows through their footprint. */
#webgl {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

html.has-webgl .page { z-index: 1; }

html.has-webgl img[data-webgl] {
  opacity: 0;
  animation: none;
}


/* ============================================================
   16. RESPONSIVE — mobile → desktop expansion
   Three additive breakpoints, mobile-first. Each :root block
   re-declares layout tokens; the `.rails` hairlines (which read
   --page-gutter) breathe automatically. Typography scales fluidly
   via clamp() in the primitive tokens, so font-size doesn't need
   to be redeclared per breakpoint.
     • 560px — release the 390px cap; relax hardcoded widths
     • 840px — campaign image cluster absolute → grid; multi-col
                country list + tracker body; centered footer
     • 1080px — full desktop: 2-col hero, 2-col campaign (tracker
                spans full), 2-col where-we-go; donate CTA at rest
   ============================================================ */


/* ---------- 16a. ≥560px — release the cage ---------- */

@media (min-width: 560px) {
  :root {
    --page-gutter: var(--space-8);   /* 32px */
    --content-max: 520px;
  }

  /* Strip hardcoded element widths to character-based caps that
     scale with the now-clamped display type. */
  .hero__title       { width: auto; max-width: 22ch; height: auto; }
  .hero__lede        { width: auto; max-width: 56ch; }
  .quote__text       { width: auto; max-width: 40ch; }
  .campaign__title   { width: auto; max-width: 12ch; }
  .campaign__lede    { width: auto; max-width: 52ch; }
  .where-we-go__title{ width: auto; max-width: 16ch; }
}


/* ---------- 16b. ≥840px — campaign images grid, multi-col lists ---------- */

@media (min-width: 840px) {
  :root {
    --page-gutter:   40px;
    --content-max:   760px;
    --section-pad-y: 64px;
    --col-gap:       var(--space-8);
    --hero-image-h:  320px;
  }

  /* More vertical breathing room between sections at tablet+. */
  .hero      { padding: var(--space-6) var(--page-gutter) var(--section-pad-y); }
  .quote     { padding: var(--section-pad-y) var(--page-gutter); }
  .campaign  { padding: var(--section-pad-y) var(--page-gutter); }
  .where-we-go { padding: var(--section-pad-y) var(--page-gutter); }
  .site-footer { padding: 0 var(--page-gutter) var(--section-pad-y); }

  /* Campaign image cluster: from absolute → 12-col grid.
     `align-self: end/start` recreates the staggered Y-offset (left
     low, right high) within a single grid row. The parallax JS
     keeps writing translate3d() — composes cleanly over static
     positioning, so the gentle drift continues unmodified. */
  .campaign__images {
    position: relative;
    height: auto;
    max-width: none;      /* unset the mobile cap — grid spans the section */
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: var(--space-5);
    margin-bottom: var(--space-8);
  }
  .campaign__image {
    position: static;
    width: 100%;
    height: auto;
  }
  .campaign__image--left {
    grid-column: 1 / span 5;
    grid-row: 1;
    align-self: end;
    aspect-ratio: 198 / 297;     /* matches new ANGOLA portrait crop */
    max-width: 280px;
  }
  .campaign__image--right {
    grid-column: 6 / span 7;
    grid-row: 1;
    align-self: start;
    aspect-ratio: 211 / 264;     /* matches new SPA portrait crop */
    max-width: 380px;
  }

  /* Country list: 2 → 3 columns. The reveal cascade uses inline
     --i delays, so the stagger reads correctly across either grid. */
  .country-list { grid-template-columns: repeat(3, 1fr); column-gap: var(--space-6); }
  .country-list__item { padding: 9px 0; }

  /* Footer: roomier, centered. */
  .site-footer { align-items: center; text-align: center; }
  .site-footer__legal { max-width: 56ch; }
}


/* ---------- 16c. ≥1080px — full desktop layouts ---------- */

@media (min-width: 1080px) {
  :root {
    --page-gutter:   64px;
    --content-max:   1200px;
    --section-pad-y: 96px;
    --col-gap:       72px;
    --hero-image-h:  460px;
  }

  /* Header: donate CTA visible at rest; logo a touch larger; the
     .is-compact scroll trigger still fires (driven by hero CTA via
     header.js), but at desktop it only nudges the logo — the CTA is
     already in place, so there's no slide-in. */
  .site-header { padding: var(--space-4) var(--page-gutter); }
  .site-header__logo {
    transform: translateY(4px) scale(1.15);
  }
  .site-header.is-scrolled .site-header__logo {
    transform: translateY(0) scale(1);
  }
  .site-header__cta {
    opacity: 1;
    transform: translateX(0);
    pointer-events: auto;
    visibility: visible;
  }
  .site-header__cta .btn {
    padding: 11px 22px;
    font-size: 14px;
  }

  /* Hero: swap the mobile overlay treatment for a 2-col split. The
     photo moves out of the absolute-background role and into the
     right grid column; the gradient overlay turns off; copy/pill/
     button stack in the left column. */
  .hero {
    margin: 0 auto;
    padding: var(--space-7) var(--page-gutter) var(--section-pad-y);
    border-radius: 0;
    overflow: visible;
    min-height: auto;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: var(--col-gap);
    align-items: center;
  }
  .hero::after { display: none; }
  .hero__image {
    position: static;
    grid-column: 2;
    grid-row: 1;
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 5;
    max-height: 720px;
    border-radius: var(--radius-sm);
    object-fit: cover;
    object-position: center 30%;
    align-self: stretch;
    z-index: 0;
  }
  .hero__inner {
    grid-column: 1;
    grid-row: 1;
    margin-top: 0;
    align-self: center;
    gap: var(--space-5);
    max-width: 540px;
  }
  .hero__copy {
    gap: var(--space-4);
  }
  .hero__title {
    font-size: 84px;
    line-height: 1.01;
    letter-spacing: -0.04em;
  }
  .hero__title-play {
    /* Tuned against 84px desktop title:
       93 ≈ 1.10em top, 372 ≈ 4.43em left, 187×104 ≈ 2.23em × 1.24em. */
    top: 1.10em;
    left: 4.43em;
    width: 2.23em;
    height: 1.24em;
  }
  .hero__lede { max-width: 530px; }
  .hero-pill { max-width: 360px; }
  /* Desktop donate button — hugs its contents (overrides the mobile
     full-width via align-self:start, since the flex column otherwise
     stretches children horizontally) but keeps the mobile padding so
     the touch target reads at the same scale. Extrabold per the
     desktop Figma; mobile uses Bold. */
  .hero .btn {
    width: auto;
    align-self: start;
    justify-content: center;
    padding: 20px 24px;
    font-weight: var(--fw-extrabold);
    letter-spacing: var(--tracking-tight);
  }
  .hero .btn__arrow {
    width: 13px;
    height: 10px;
  }

  /* Quote: stays a centered editorial column, capped narrower than
     the page. The parallax JS still drives --quote-frame-w (width %)
     for the bleed-on-entry effect; we override `height` to a fluid
     desktop value so the frame doesn't stay at the 144→220px mobile
     range that parallax.js hardcodes via --quote-frame-h. */
  .quote { max-width: 880px; margin-inline: auto; }
  .quote__image { height: clamp(260px, 28vw, 460px); }

  /* Africa Fund: 2-col editorial split (tag/title/lede left, photo
     pair right). The tracker is now its own .tracker-section below.
     .campaign__hero collapses to display:contents so its children
     (tag/title/images) participate directly in this grid; the
     mobile green-bg + black-type treatment is suppressed. */
  .campaign {
    padding: var(--section-pad-y) var(--page-gutter);
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      "tag     images"
      "title   images"
      "lede    images";
    column-gap: var(--col-gap);
    row-gap: var(--space-5);
    align-items: start;
  }
  .campaign__hero {
    display: contents;
    background: none;
    color: inherit;
    padding: 0;
    margin: 0;
    overflow: visible;
  }
  /* Restore default styling for tag/title that were overridden inside
     the green hero on mobile. */
  .campaign__hero .campaign__tag-pulse { display: inline-flex; }
  .campaign__hero .campaign__tag-text {
    font-family: var(--font-body);
    font-weight: var(--fw-bold);
    font-size: var(--fs-body-sm);
    letter-spacing: var(--tracking-tighter);
    line-height: var(--lh-flat);
    color: var(--accent);
    text-transform: none;
  }
  .campaign__tag   { grid-area: tag;   margin-bottom: 0; }
  /* Two-class selector so this beats the mobile .campaign__hero rule
     (which paints the title black on the green-bg block). At desktop
     the campaign hero is display:contents, so we want the standard
     white-on-dark editorial treatment. */
  .campaign__hero .campaign__title {
    grid-area: title;
    margin-bottom: 0;
    /* Use --fs-display-md here so the title comfortably fits the
       editorial column on 2 lines. */
    font-size: var(--fs-display-md);
    font-weight: var(--fw-bold);     /* Plus Jakarta Sans Bold */
    color: var(--text-strong);
    line-height: var(--lh-display);
    letter-spacing: -0.04em;
    max-width: 14ch;
  }
  .campaign__lede  { grid-area: lede;  margin-bottom: 0; max-width: 52ch; }
  /* Photo cluster reverts from the mobile diagonal absolute layout to
     the ≥840 12-col grid; both photos sit side-by-side with the
     staggered Y offset. */
  .campaign__hero .campaign__images {
    grid-area: images;
    height: auto;
    margin-top: 0;
    margin-bottom: 0;
    max-width: 480px;
  }
  .campaign__hero .campaign__image--left,
  .campaign__hero .campaign__image--right {
    position: static;        /* unset mobile absolute */
    width: 100%;
    height: auto;
    top: auto;
    left: auto;
  }
  .campaign__image--left  { max-width: 200px; }
  .campaign__image--right { max-width: 280px; }

  /* Tracker section gets its own breathing room at desktop. */
  .tracker-section {
    padding: var(--section-pad-y) var(--page-gutter);
  }

  /* Where We Go: stacked — title, then the full-width world map (a 2:1
     map reads far better wide than boxed beside the list), then the
     country list (3-col from the ≥840 block). */
  .where-we-go__title { max-width: 18ch; }
  .where-we-go .worldmap { margin-bottom: var(--space-9); }

  /* Footer: bigger mark, wider legal, tighter gap against the
     larger desktop type rhythm. */
  .site-footer        { gap: var(--space-8); }
  .site-footer__mark  { width: 200px; }
  .site-footer__legal { max-width: 64ch; font-size: 13px; }
}
