

.map-hero--3d {
  
  --rhf-red: #D22630;          
  --rhf-red-deep: #AF272E;     
  --rhf-ink: #0A0B0B;          

  
  --rhf-surface: #FFFFFF;
  --rhf-surface-alt: #F7F9FB;  
  --rhf-surface-pink: #FAEBEB; 

  
  --rhf-text: #1A1A1A;
  
  --rhf-text-muted: #545963;
  --rhf-text-on-dark: #FFFFFF;

  
  --rhf-border: #D9DEE5;
  --rhf-border-strong: #A9B2BD;
  
  --rhf-fill-soft: #EDF0F5;

  
  --rhf-map-bg: #EDEDED;
  --rhf-map-hover: #FAFAFA;

  
  --wf-placeholder: #E4E8ED;
  --wf-placeholder-ink: #77808C;
  --wf-note-bg: #FFF6D6;
  --wf-note-border: #E0C868;

  
  --rhf-font-display: "Nohemi", "General Sans", -apple-system,
                      BlinkMacSystemFont, "Segoe UI", Roboto,
                      "Helvetica Neue", Arial, sans-serif;
  --rhf-font: "General Sans", -apple-system, BlinkMacSystemFont,
              "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  --fs-display: clamp(2.75rem, 7vw, 5.5rem);   
  --fs-h1: clamp(2rem, 4.5vw, 3.25rem);
  --fs-h2: clamp(1.5rem, 3vw, 2.25rem);
  --fs-h3: clamp(1.125rem, 2vw, 1.5rem);
  --fs-stat: clamp(3rem, 6vw, 5rem);           
  
  --fs-stat-lg: clamp(3rem, 6.4vw, 6.0625rem);
  
  --fs-stat-xl: clamp(3.5rem, 8.2vw, 7.75rem);
  
  --fs-stat-sm: clamp(2.25rem, 4.6vw, 4.25rem);
  
  --fs-h1-xl: clamp(2.5rem, 6.1vw, 5.75rem);
  
  --fs-h1-lg: clamp(2.25rem, 4.25vw, 4rem);
  
  --fs-lede: clamp(1.125rem, 2.05vw, 1.9375rem);
  --fs-body: 1rem;
  --fs-small: 0.875rem;
  --fs-eyebrow: 0.9375rem;

  --lh-tight: 1.05;
  --lh-heading: 1.15;
  --lh-body: 1.55;

  
  --sp-1: 0.25rem;
  --sp-2: 0.5rem;
  --sp-3: 0.75rem;
  --sp-4: 1rem;
  --sp-5: 1.5rem;
  --sp-6: 2rem;
  --sp-7: 3rem;
  --sp-8: 4rem;
  --sp-9: 6rem;

  
  --container: 1352px;
  --gutter: 1rem;      
  --header-h: 76px;

  
  --radius-card: 16px;
  --radius-pill: 999px;
  --radius-img: 12px;

  
  --shadow-card: 0 2px 20px rgba(0, 0, 0, 0.13);

  --focus-ring: 3px solid #005FCC;
}

@media (min-width: 768px) {
  .map-hero--3d {
    --gutter: 2.5rem;
  }
}

@media (min-width: 1200px) {
  .map-hero--3d {
    --gutter: 5rem;   
  }
}

/* ---------- Impact map hero (3D) ----------
   Extruded WebGL map of Canada, white-on-white, read through lighting.
   Markup ships the STATIC state (flat SVG + region list); impact-hero.js
   enhances it and toggles the state classes:
     .is-ready   scene (or static fallback) is up → reveal choreography
     .is-detail  a region is open → title/CTA yield to the panel
   Spec: docs/impact-hero-spec.md. */
.map-hero--3d {
  position: relative;
  background: var(--rhf-map-bg);
  display: flex;
  flex-direction: column;
  /* Full-viewport takeover. The header OVERLAYS this hero on the Impact
     page (see pages.css), so the hero owns the whole first screen.
     svh keeps mobile URL bars honest. */
  min-height: 100vh;
  min-height: 100svh;
}
.map-hero__stage {
  position: relative;
  flex: 1;
  overflow: hidden;
}
/* Vignette: the stage fades to solid white along its top and bottom
   edges. The top band guarantees the transparent header's legibility
   over the map; the bottom band dissolves the grey map field into the
   white page below, so the hero reads as seamless rather than boxed.
   Sits above the canvas, below the text overlay; never captures input. */
.map-hero__stage::before,
.map-hero__stage::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: clamp(90px, 18vh, 180px);
  pointer-events: none;
  z-index: 1;
}
.map-hero__stage::before {
  top: 0;
  background: linear-gradient(to bottom, var(--rhf-surface), transparent);
}
.map-hero__stage::after {
  bottom: 0;
  background: linear-gradient(to top, var(--rhf-surface), transparent);
}
.map-hero__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  touch-action: pan-y;   /* map taps work; page scroll still owns the vertical */
}
/* Static map image — the default state, permanent for no-JS/no-WebGL.
   [hidden] once the 3D scene has rendered its first frame. */
.map-hero__fallback {
  position: absolute;
  inset: var(--sp-6);
  margin: auto;
  max-width: calc(100% - 2 * var(--sp-6));
  max-height: calc(100% - 2 * var(--sp-6));
  object-fit: contain;
  /* The SVG's provinces are white-on-transparent; the hero's grey map
     field (--rhf-map-bg) is what makes them legible in the static state. */
}
.map-hero__fallback[hidden] { display: none; }

/* Text layers over the canvas. pointer-events:none so the map keeps
   hover/click; the panel re-enables them for itself. z-index lifts the
   text above the vignette bands. */
.map-hero__overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
}

/* The lockup — the word and its standfirst, as one block. Its RESTING
   place is the stage's bottom-left CORNER on desktop, and the bottom
   centre on mobile, where there is no quadrant to speak of at that width.
   Breakpoint matches the hero's own 767/768 split (and MOBILE in
   impact-hero.js).

   It has walked twice on 2026-08-25. It sat on the page gutter, which put
   it under British Columbia with the whole prairie gap empty to its
   right; it then moved in to 22%/17%, under the southern prairies, which
   was right for a block at titling size and wrong for the same block at
   twice that — at billboard scale it sat ON the map's south-west shoulder
   instead of under it. 6%/7% is out of the map's way at every viewport,
   in the corner where the land has already fallen away.

   Both are percentages of the stage rather than fixed insets, because
   what the block is aligned to is the MAP, which scales with the stage; a
   fixed inset drifts back under the land as the window changes shape.

   This is also the no-JS state: with no script the lockup is here, both
   parts visible, from the first paint. The overture below is a state
   layered on top of it, never a precondition for it. */
.map-hero__lockup {
  position: absolute;
  bottom: var(--sp-6);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: min(34rem, calc(100% - 2 * var(--gutter)));  /* survives French expansion */
  text-align: center;
}
@media (min-width: 768px) {
  .map-hero__lockup {
    left: 6%;
    /* Down in the corner, but never into the region-name bar sitting at
       bottom: 0 — which is two lines tall on a narrow desktop. The rem
       floor is what clears it: the percentage alone puts the standfirst
       on top of the names on a short viewport. */
    bottom: max(7%, 8.5rem);
    transform: none;
    width: auto;
    /* The lockup grew to twice its type size on 2026-08-25 (below), and
       the box it wraps in has to grow with it or the standfirst turns
       into a column of three-word lines. 46% still ends at 68% of the
       stage — clear of the eastern provinces, which is what the prairie
       gap is for. */
    max-width: min(44rem, 46%);
    text-align: left;
  }
}
.map-hero__title {
  /* Titling size, at rest: the word is a heading with a paragraph under
     it, so it is sized as one. Its monumental size belongs to the
     overture, below. */
  font-size: var(--fs-h1-lg);
  line-height: var(--lh-tight);
  margin: 0 0 var(--sp-3);
}
.map-hero__cta {
  font-size: var(--fs-small);
  color: var(--rhf-text-muted);
  margin: 0;
}

/* The settled lockup is deliberately BILLBOARD scale — twice the sizes
   above — so the word and its standfirst hold the prairies rather than
   sitting in them as a caption. Scoped to the desktop breakpoint, which
   is the one where the lockup is the bottom-left block this is sized
   for: centred under a phone-width map, doubled type would be the whole
   hero. Both rules are outranked by the overture's own size
   (.is-js …:not(.is-settled), higher specificity), so the word still
   arrives monumental and lands here at its own size. */
@media (min-width: 768px) {
  .map-hero__title { font-size: calc(var(--fs-h1-lg) * 2); }
  .map-hero__cta { font-size: calc(var(--fs-small) * 2); }
}

/* The button under the standfirst — the way down to the numbers. It is
   part of the lockup, so it takes the lockup's alignment for free
   (centred on mobile, left in the desktop corner) and its place in the
   arrival order: last.

   pointer-events are turned back on HERE and nowhere wider: the overlay
   drops them for the whole layer so the map keeps its hover and click
   everywhere the text is not, and a paragraph that re-enabled them
   would punch a dead rectangle in the map. */
.map-hero__actions {
  margin: var(--sp-5) 0 0;
}
.map-hero__actions .btn { pointer-events: auto; }
@media (min-width: 768px) {
  /* The lockup's type doubles at this width (above). The button does
     not follow it there — a pill set at 28px is a third headline, not a
     control. One step up, and stop. */
  .map-hero__actions {
    margin-top: var(--sp-6);
  }
  .map-hero__actions .btn {
    font-size: var(--fs-body);
    padding: var(--sp-4) var(--sp-6);
  }
}

/* ---- The hero's figures ----
   Three figures, ONE AT A TIME. The stage's top-right quadrant on
   desktop — the eastern Arctic is the empty half of the map, and that
   emptiness is what makes room for them — right-aligned, because what
   lines up with the stage's edge is the block's edge, not its ragged
   left. On mobile the whole hero splits around the map instead; see the
   ≤767px block further down.

   Each figure counts up, its copy fades up behind the count, the pair
   hold, and the next one takes its place. `assets/js/hero-stats.js`
   moves .is-current along and calls the count; every duration except
   the hold itself is here. Requested by the user 2026-09-02.

   STATIC STATE — no .is-live: all three are simply stacked down the
   block and readable, which is what a visitor with no script (or with
   reduced motion) gets. Same division as the winners' crossfade. */
.map-hero__stats {
  display: grid;
  gap: var(--sp-4);
  justify-items: center;
  text-align: center;
  list-style: none;
  margin: 0;
  /* NB — not `ul.plain`: that rule's padding: 0 outranks a single
     class, which is why this list does not carry it. */
  padding: 0;
}
/* Once the rotator is driving: every figure in the SAME grid cell, so
   the block is as tall as the tallest of the three and nothing under it
   moves as they change. Only opacity is animated. */
.map-hero__stats.is-live { gap: 0; }
.map-hero__stats.is-live .hero-stat {
  grid-area: 1 / 1;
  opacity: 0;
  /* Must match SWAP_MS in hero-stats.js, which is the beat the script
     waits before bringing the next figure up. */
  transition: opacity 0.5s ease;
}
.map-hero__stats.is-live .hero-stat.is-current { opacity: 1; }

@media (min-width: 768px) {
  .map-hero__stats {
    position: absolute;
    /* Hung off the STAGE rather than off the header: the block is aimed
       at the map's top-right shoulder, and the map scales with the
       stage, so a fixed inset drifts away from the land as the window
       changes shape — the same reasoning as the lockup's 6%/7% in the
       opposite corner. The max() is the floor that keeps it clear of the
       transparent header on a short window, where 16% is not enough.
       Moved down from a flat header + --sp-5 on 2026-09-02, at the
       user's request: hard against the header it read as chrome rather
       than as part of the map. */
    top: max(calc(var(--header-h) + var(--sp-5)), 16%);
    /* 6% mirrors the lockup's inset on the other side, so the two
       blocks are hung off the same margin at opposite corners. */
    right: 6%;
    /* Wide enough for a two-line caption, narrow enough that the block
       never reaches the eastern provinces below it. */
    width: min(26rem, 34%);
    justify-items: end;
    text-align: right;
    gap: var(--sp-6);
  }
}

/* The numeral. `.count` on the same element is not decoration: it is
   what carries the per-character mask and the rise, and it is what
   `window.RHFReveal.count()` (reveal.js) animates. These four
   declarations are the ones the hero disagrees with — a two-class
   selector so they outrank `.count`'s own, whatever the source order.
   Everything else about the numeral, tabular figures included, comes
   from `.count` and must keep coming from there. */
.hero-stat .hero-stat__figure {
  font-size: var(--fs-stat-sm);
  line-height: 0.95;
  color: var(--rhf-red);
  margin: 0 0 var(--sp-3);
}
/* A step up from --fs-small (2026-09-02, at the user's request): at 14px
   this sentence read as a footnote to a 68px numeral rather than as the
   half of the pair that says what the number means. It scales with the
   viewport for the same reason the numeral does — the two are one
   lockup and have to keep their proportion. */
.hero-stat__caption {
  font-size: clamp(1rem, 1.25vw, 1.375rem);
  line-height: 1.4;
  color: var(--rhf-text);
  margin: 0;
}
/* The copy arrives BEHIND its number: it fades up once the count is
   effectively over (COUNT_MS in reveal.js is 1.6s), so the figure is
   read first and the sentence explains it second. The delay is the
   whole point of the effect — it is not a stagger for its own sake. */
.map-hero__stats.is-live .hero-stat__caption {
  opacity: 0;
  transform: translateY(0.6em);
  transition: opacity 0.6s ease,
              transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.map-hero__stats.is-live .hero-stat.is-current .hero-stat__caption {
  opacity: 1;
  transform: none;
  transition-delay: 1.5s;
}
/* Optional. Present only where the copy leads with a phrase of its own
   ("Awards & Fellowships"), which is one of the three — see the note in
   pages/impact.html. Caps are applied here rather than typed into the
   field, so the WordPress editor holds a readable sentence. */
.hero-stat__lead {
  display: block;
  font-family: var(--rhf-font-display);
  font-size: clamp(1.125rem, 1.6vw, 1.75rem);
  line-height: 1.15;
  color: var(--rhf-red);
  text-transform: uppercase;
  letter-spacing: 0.01em;
  margin-bottom: var(--sp-2);
}

/* ---- The overture ----
   First load plays the word before it settles: "Impact" alone, at
   monumental size, centred on the stage; then it fades out, and fades
   back in down in the lockup with its standfirst underneath.

   Two states, both driven by classes impact-hero.js puts on the hero:
     (default, .is-js)  the overture — centred and huge
     .is-leaving        the fade out between them
     .is-settled        the resting lockup

   Deliberately a fade-out-then-in and NOT a morph between the two
   positions: the word travels a long way and changes size by a factor of
   five, and any tween of that reads as a swoop across the map rather
   than as one word taking its place. Nothing is transitioned except
   opacity — position and size change while the word is invisible.

   The overture is a .is-js state only, so a visitor with no script never
   sees the word parked in the middle of the map with no way out of it.

   The size: twice what the word used to sit at (2 × --fs-display), with
   a viewport cap so it can never outgrow the stage it is centred on.
   22vw holds "Impact" at ~73% of the stage width at every size, which is
   what the cap ends up doing on anything narrower than about 1600px. */
.is-js .map-hero--3d:not(.is-settled) .map-hero__lockup {
  position: absolute;
  inset: 0;
  left: 0;
  bottom: 0;
  transform: none;
  width: auto;
  max-width: none;
  display: grid;
  place-items: center;
  text-align: center;
}
.is-js .map-hero--3d:not(.is-settled) .map-hero__title {
  font-size: min(calc(var(--fs-display) * 2 * 2), 22vw);
  margin: 0;
}
/* The standfirst has no part in the overture — it arrives with the
   settled lockup, after the word. It is taken out of the centring grid so
   it cannot occupy a row and push the word off centre; the .is-js base
   rule above already has it at opacity 0, so absolute is enough to hide
   it and there is nothing to see at its static position.

   `display: none` is what this used to be, and it was the bug (2026-08-25):
   an element that goes from display:none to displayed has no previous
   computed style to interpolate from, so the opacity transition below
   never ran. The standfirst painted at full opacity on the very frame
   .is-settled landed — a second BEFORE the word it belongs to, which was
   still making its way back from the overture. Out of flow rather than
   out of the box keeps the element rendered, so the transition works. */
.is-js .map-hero--3d:not(.is-settled) .map-hero__cta,
.is-js .map-hero--3d:not(.is-settled) .map-hero__actions { position: absolute; }

/* Reveal choreography — JS only toggles the classes; the timing lives here.
   Map first, then the title, then the CTA. The hidden starting state is
   gated on .is-js so that with no JS at all the static hero is simply
   visible — a reveal that depends on script must never be able to hide
   content from users whose script never arrives.

   .is-js is on <html>, set by an inline script in the head of impact.html
   rather than by impact-hero.js. It has to land before the first paint:
   set from the deferred module at the end of <body> it arrives too late,
   and the flat SVG map flashes on screen before the reveal hides it.

   The hero's own grey field (--rhf-map-bg, on .map-hero--3d) is not part
   of the reveal — it paints immediately, so the hero starts as an empty
   grey stage and everything else arrives into it. */
.is-js .map-hero--3d .map-hero__canvas,
.is-js .map-hero--3d .map-hero__fallback,
.is-js .map-hero--3d .map-hero__title,
.is-js .map-hero--3d .map-hero__cta,
.is-js .map-hero--3d .map-hero__actions,
.is-js .map-hero--3d .map-hero__stats {
  opacity: 0;
  transition: opacity 0.6s ease;
}
.map-hero--3d.is-ready .map-hero__canvas,
.map-hero--3d.is-ready .map-hero__fallback { opacity: 1; }
/* This delay is the word coming BACK from .is-leaving, not the overture's
   entrance: once the title is split the container is held opaque by the
   .has-letters rule below and the letters are the entrance. Leave it at
   0.3s — pushing the overture later is the letters' animation-delay. */
.map-hero--3d.is-ready .map-hero__title { opacity: 1; transition-delay: 0.3s; }
/* The standfirst belongs to the settled lockup, not to the reveal, and it
   arrives BEHIND the word rather than beside it: the word takes its place
   in the corner first, then the sentence follows it in. The delay is the
   word's own return — 0.3s above + its 0.6s fade — plus a beat, so keep
   it longer than those two if either is retuned. */
.map-hero--3d.is-settled .map-hero__cta { opacity: 1; transition-delay: 1s; }

/* Then the figures' block, across the map in the other corner — and
   then the button, which closes the lockup off.

   These delays are measured from .is-settled and they are a SEQUENCE:
   the word arrives in the corner (0.3s + its own 0.6s fade), the
   sentence follows it (1s), the figures' block opens (0.9s) and the
   button lands last (1.35s). Retune one and the ones after it move with
   it, or the hero stops reading as one thing arriving and starts
   reading as four.

   What the block's own fade reveals is an EMPTY box while the rotator
   is driving: the figures inside it are held at opacity 0 by .is-live
   and the first one is brought up by hero-stats.js at START_MS. The two
   are separate on purpose — the block belongs to the hero's arrival,
   the figures in it belong to the rotation, and with no script the
   block's fade is the only one there is. */
.map-hero--3d.is-settled .map-hero__stats { opacity: 1; transition-delay: 0.9s; }
.map-hero--3d.is-settled .map-hero__actions { opacity: 1; transition-delay: 1.35s; }

/* Title: letters rise out of a mask, one after another.
   impact-hero.js splits the text into .line > .word > .letter (it cannot
   be done in markup — the headline is a plain-text WP field). Once split,
   the element itself no longer fades: the letters ARE the reveal, so the
   container must be opaque or it would double up with them.

   The map's own intro (regions extruding up out of the base, west →
   east) is the other half of this choreography and lives in
   impact-hero.js §1 — INTRO_MS, INTRO_STAGGER. The title uses the same
   shape of ease-out, so the two read as one movement.

   The 1.9s delay is deliberately a second later than the sweep it used to
   start on (2026-08-25): the leaf wave crosses the map between 0.7s and
   ~2.1s (LEAF_WAVE_* in impact-hero.js §1) and the word arriving on top
   of it cut the crossing in half. Now the map fills west → east on its
   own, and the word comes in over the tail of it. OVERTURE_MS moved by
   the same second — keep the two in step, or the word leaves before it
   has finished arriving. */
.is-js .map-hero--3d .map-hero__title.has-letters { opacity: 1; }
.map-hero__title .line {
  display: block;
  overflow: hidden;          /* the mask the letters rise out of */
  /* Without this the mask crops the descender of a letter like the "p"
     in Impact once it has come to rest. */
  padding-bottom: 0.16em;
  margin-bottom: -0.16em;
}
.map-hero__title .word { display: inline-block; white-space: nowrap; }
.map-hero__title .letter {
  display: inline-block;
  opacity: 0;
  transform: translateY(100%) rotate(8deg);
}
.map-hero--3d.is-ready .map-hero__title .letter {
  animation: mapHeroLetterRise 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  animation-delay: calc(1.9s + var(--i) * 0.06s);
}
@keyframes mapHeroLetterRise {
  to { opacity: 1; transform: translateY(0) rotate(0); }
}
/* Between the overture and the settled lockup: the word goes, and takes
   the whole 0.6s fade to do it. impact-hero.js swaps the position and
   size at the far end of this fade, while the word is invisible, then
   drops .is-leaving so the same transition carries it back in.
   .has-letters is repeated here for the same reason .is-detail repeats
   it below — it has to outrank the rule holding a split title opaque. */
.map-hero--3d.is-leaving .map-hero__title,
.map-hero--3d.is-leaving .map-hero__title.has-letters {
  opacity: 0;
  transition-delay: 0s;
}

/* A region is open: the title and CTA get out of the panel's way.
   .has-letters is repeated here so this outranks the rule that holds a
   split title opaque — without it the title stays put over the panel.
   The letters themselves have finished their animation by now, so the
   container's own fade is what carries the title out and back. */
.map-hero--3d.is-detail .map-hero__title,
.map-hero--3d.is-detail .map-hero__title.has-letters,
.map-hero--3d.is-detail .map-hero__cta,
.map-hero--3d.is-detail .map-hero__actions,
.map-hero--3d.is-detail .map-hero__stats { opacity: 0; transition-delay: 0s; }
/* The button is the one thing in the lockup that is FOCUSABLE, so
   fading it out is not enough: an invisible link is still a tab stop in
   the middle of an open panel, and following it would scroll the page
   out from under the reader. Visibility takes it out of the tab order,
   switched at the far end of the fade so the fade still plays. Coming
   back it is instant — the base transition names opacity only. */
.map-hero--3d.is-detail .map-hero__actions {
  visibility: hidden;
  transition: opacity 0.6s ease, visibility 0s linear 0.6s;
}

/* Non-pointer path to the same content. Always rendered — this
   is not a fallback that only appears when JS fails. */
.map-hero__regions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  list-style: none;
  margin: 0;
  padding: var(--sp-5) var(--gutter);
  border-top: 1px solid var(--rhf-border);
}
.map-hero__regions button {
  border: 1px solid var(--rhf-border-strong);
  background: #fff;
  border-radius: var(--radius-pill);
  padding: var(--sp-2) var(--sp-4);
  font: inherit;
  font-size: var(--fs-small);
  cursor: pointer;
}
.map-hero__regions button[aria-current="true"] {
  background: var(--rhf-ink);
  color: #fff;
  border-color: var(--rhf-ink);
}

/* Desktop: the region list stops being a strip of pills under the map and
   becomes one line of names along the bottom of the map viewport, where
   the CTA fine print used to sit.

   Taking it out of flow is what moves it INSIDE the stage without moving
   it in the DOM: its containing block is .map-hero--3d (position:
   relative), and with the list absolute the stage — flex: 1 in a
   min-height:100vh column — grows to the full hero, so bottom:0 lands it
   over the map's bottom edge. The markup stays exactly where it is, which
   matters because mobile still wants it in flow as a rail below the map.

   It sits above the stage's vignette (z-index 1) and text overlay (2).
   The names land on the vignette's white band, so muted grey reads
   cleanly against it rather than against the grey map field.

   Names wrap to a second line rather than overflowing on narrow desktops:
   13 province and territory names is ~158 characters, and there is no
   font size that fits them on one line at 768px and is still legible. */
@media (min-width: 768px) {
  .map-hero__regions {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 3;
    justify-content: center;
    align-items: baseline;
    gap: clamp(0.5rem, 1.15vw, 1.5rem);
    /* Inset by --sp-6 rather than the page --gutter: at 1200px+ the
       gutter is 5rem a side, and those 160px are the difference between
       one line and two. This bar spans the map, not the text column, so
       it does not owe the gutter an alignment. */
    padding: var(--sp-4) var(--sp-6) var(--sp-5);
    border-top: 0;
  }
  .map-hero__regions button {
    border: 0;
    background: none;
    border-radius: 0;
    padding: 0;
    font-size: clamp(0.6875rem, 0.95vw, 0.8125rem);
    color: var(--rhf-text-muted);
    white-space: nowrap;
    transition: color 0.2s ease;
    /* The hovered state goes to 700. Reserving the bold width up front
       means the name does not grow when it lights up and shove its
       thirteen neighbours sideways — a row that reflows under the
       pointer is unusable, and on a two-line wrap it would re-break the
       line. `font-synthesis: none` keeps the reservation honest: the
       measurement must be the real Nohemi 700, not a synthesised one. */
    font-synthesis: none;
  }
  .map-hero__regions button::before {
    content: attr(data-name);
    display: block;
    height: 0;
    overflow: hidden;
    visibility: hidden;
    font-weight: 700;
    text-decoration: underline;   /* the offset below can widen it too */
    text-underline-offset: 0.35em;
  }
  /* Three ways in, one look: the mouse on the name, the keyboard on the
     name, and .is-hovered — set by impact-hero.js when the pointer is on
     the REGION rather than the name, so the map and the list always agree
     on what is being pointed at. Bold AND underlined, not just darker:
     at 11–13px, muted grey going to ink is a difference you have to look
     for, and this row is the only thing telling you which of thirteen
     shapes is under the pointer. */
  .map-hero__regions button:hover,
  .map-hero__regions button:focus-visible,
  .map-hero__regions button.is-hovered {
    color: var(--rhf-ink);
    font-weight: 700;
    text-decoration: underline;
    text-underline-offset: 0.35em;
  }
  .map-hero__regions button[aria-current="true"] {
    background: none;
    color: var(--rhf-ink);
    text-decoration: underline;
    text-underline-offset: 0.35em;
  }
  /* With a region open the panel scrolls through this row's space, so the
     names need something to sit on — on the map alone they are legible
     against the vignette's white band, but not against a post title
     passing behind them. Only in the detail state: the row's whole point
     over the map is that it has no surface of its own. */
  .map-hero--3d.is-detail .map-hero__regions {
    background: var(--rhf-surface);
  }
}

/* Region detail — an overlay state of the hero, not a page section.
   Scrim is the surface token at 82%, so the zoomed map ghosts through;
   color-mix keeps the no-hardcoded-hex rule intact (fallback: solid). */
.region-panel--overlay {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: center;
  /* `safe` is what stops the centring from eating the top of a panel
     that outgrows the stage — plain `center` overflows equally in both
     directions, and the half above the box is unreachable by scrolling,
     so on a phone the region name and its count were simply gone. With
     `safe` the box falls back to start alignment the moment it cannot
     fit, and everything is reachable. Declared twice: browsers that do
     not know the keyword keep the first. */
  align-content: center;
  align-content: safe center;
  /* Top padding is the header's own height: the transparent header
     overlays this hero (pages.css), so once the panel is start-aligned
     its Back button lands under the logo without it. Exactly
     --header-h and no more — every pixel of padding here is a pixel
     closer to the panel needing a scrollbar it did not need before.
     Bottom padding is the ordinary one; from 768px up it becomes a
     reserve for the region-name row, below. */
  padding-block: var(--header-h) var(--sp-6);
  overflow-y: auto;               /* long feeds scroll rather than clip */
  background: var(--rhf-surface);
  background: color-mix(in srgb, var(--rhf-surface) 82%, transparent);
  z-index: 2;
  /* Fades in/out: JS unhides the panel, then adds .is-open a frame later
     so the transition actually runs ([hidden] is display:none, which
     would otherwise skip it). Close is the reverse, hidden after the fade. */
  opacity: 0;
  transition: opacity 0.4s ease;
}
/* From 768px up the panel reserves the region-name row's height below its
   content, so a panel taller than the stage can scroll its last post —
   the See more posts button — out from under the names rather than
   stopping half covered by them. Below 768px the row is in flow beneath
   the hero and overlaps nothing.

   --region-row-h is measured and written onto the hero by impact-hero.js
   (a ResizeObserver on the row), because the row's height is not a
   constant: it wraps to two lines somewhere around 1100px, wraps
   differently in French, and moves again when the webfont lands. The
   fallback covers the two-line case for the moment before the observer
   first fires, and for anyone with no script — who never opens this
   panel anyway.

   It has to sit AFTER the base rule above, not in the earlier
   region-list media block: media queries add no specificity, and
   `.region-panel--overlay`'s own `padding-block` would win the tie on
   source order. */
@media (min-width: 768px) {
  .region-panel--overlay {
    padding-bottom: calc(var(--region-row-h, 5.5rem) + var(--sp-2));
  }
}
.region-panel--overlay.is-open { opacity: 1; }
.region-panel[hidden] { display: none; }

/* Same measure as the rest of the site, so the panel's left edge lines
   up with page content rather than floating mid-canvas. */
.region-panel__inner {
  width: 100%;
  max-width: calc(var(--container) + var(--gutter) * 2);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}
.region-panel__back {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  border: 1px solid var(--rhf-border-strong);
  background: var(--rhf-surface);
  border-radius: var(--radius-pill);
  padding: var(--sp-2) var(--sp-5);
  font: inherit;
  font-size: var(--fs-small);
  cursor: pointer;
}
.region-panel__back-arrow { font-size: 1.1em; line-height: 1; }

/* Two columns. LEFT is the headline half — "Impact in <Region>", the
   count, a rule, and the featured post under it. RIGHT is the rest of
   the region's posts as rows, and the way through to the archive.

   The featured post moved out of the feed and under the headline on
   2026-08-25: with the card on top of five rows the right-hand column
   was a wall, and the headline sat beside it looking like its caption.
   Split this way each column carries one idea — this region, and what
   else is in it — and the left column reads top to bottom as a single
   editorial statement.

   The left column is the wider of the two: it holds the billboard type.
   1.15fr / 1fr is as far as that goes — the rows still have to hold two
   lines each without going narrow and ragged.

   Single column until there is room for two; the order in the markup is
   the order it stacks in, so the phone gets headline → feature → rows. */
.region-detail {
  display: grid;
  gap: var(--sp-7);
  align-items: start;
}
@media (min-width: 900px) {
  .region-detail {
    grid-template-columns: 1.15fr 1fr;
    gap: clamp(var(--sp-7), 5vw, var(--sp-9));
  }
}

/* The left column's two blocks, and the rule that separates them. The
   rule is on the featured card's side of the gap rather than the
   headline's, so a region whose content fails to load leaves no stray
   line under the number. */
.region-detail__lede {
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
  min-width: 0;
}
.region-detail__lede > [data-region-featured]:not(:empty) {
  border-top: 1px solid var(--rhf-border-strong);
  padding-top: var(--sp-6);
}
.region-detail__name {
  /* Billboard scale — the same token as "Featured Winners" further down
     the page, so the panel's headline sits at the page's own top heading
     size rather than at a size invented for it. It has to survive
     "Newfoundland and Labrador" (25 characters, longer again in French),
     which the token's vw term handles: the name grows into the space it
     has instead of wrapping to four lines on a laptop. */
  font-size: var(--fs-h1-xl);
  line-height: 0.95;
  margin: var(--sp-3) 0 0;
  /* No hyphenation (2026-08-25). At this size the column takes about ten
     characters a line, so `hyphens: auto` broke the two-word names —
     "British Co-/lumbia" — which reads as a typo on a billboard. Names
     break at the SPACE now: BRITISH / COLUMBIA.

     The word that cannot do that is "Newfoundland" (12 characters, and
     the region is "Newfoundland and Labrador"). `overflow-wrap` is the
     safety net under it — it only engages for a word that genuinely
     cannot fit its line, which no other region has, and it breaks
     without printing a hyphen. Better a rare hard break on the longest
     word in the country than a hyphen in the middle of a province. */
  hyphens: none;
  overflow-wrap: break-word;
}
.region-detail__count {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  margin: var(--sp-6) 0 0;
}
.region-detail__number {
  font-family: var(--rhf-font-display);   /* titling: the region panel's count */
  /* --fs-stat-xl, the Impact page's own numeral size, so the count in the
     panel reads at the same scale as the numerals in the band below the
     hero. Larger than the region name above it: the number is the single
     fact this panel exists to deliver. */
  font-size: var(--fs-stat-xl);
  line-height: 0.85;
  font-weight: 700;
  color: var(--rhf-red-deep);
}
.region-detail__count-label {
  font-size: var(--fs-lede);
  line-height: 1.2;
  color: var(--rhf-text-muted);
  max-width: 12ch;
}

/* The right column: the rows, then the way out. It is NOT given its own
   scroller — `.region-panel--overlay` already scrolls the whole panel,
   and a second scroll region nested inside the first is a trap with a
   pointer and worse with a thumb. */
.region-detail__feed {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  min-width: 0;
}

/* The lead post, under the headline in the left column. The one card in
   the panel, and it earns the exception the rows are denied: an image is
   what makes it read as the feature, and an image needs an edge to sit
   in. Image beside the text rather than above it — stacked, the card ran
   the whole depth of the panel on its own. */
.region-feature {
  display: grid;
  /* The image column scales with the card rather than being fixed: at
     12rem flat it took over half a 375px phone and squeezed the title
     into three lines. */
  grid-template-columns: minmax(0, clamp(7rem, 32%, 12rem)) minmax(0, 1fr);
  gap: var(--sp-5);
  align-items: center;
  text-decoration: none;
  color: inherit;
}
.region-feature__media {
  border-radius: var(--radius-card);
  aspect-ratio: 4 / 3;
  height: auto;
}
.region-feature__body { min-width: 0; }
.region-feature__meta { margin: 0 0 var(--sp-2); }
.region-feature__title {
  margin: 0;
  font-size: var(--fs-h3);
  line-height: var(--lh-tight);
}
.region-feature:hover .region-feature__title,
.region-feature:focus-visible .region-feature__title { text-decoration: underline; }

/* The way through to the region's archive. Sits at the end of the feed,
   aligned to its left edge rather than stretched across it — it is one
   more link out of the panel, not a form submit. */
.region-detail__more { align-self: flex-start; }
.region-detail__more[hidden] { display: none; }

/* The rows. Hairline-separated rather than cards: the panel already
   sits on a scrim over the map, and five cards on a scrim read as
   clutter next to the one card that is meant to lead. */
.region-posts {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--rhf-border);
}
.region-posts li { border-bottom: 1px solid var(--rhf-border); }
.region-posts a {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-4) 0;
  text-decoration: none;
  color: inherit;
}
.region-posts a:hover .region-posts__title { text-decoration: underline; }
.region-posts__meta {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--rhf-text-muted);
}
.region-posts__category { color: var(--rhf-red-deep); }
.region-posts__title {
  margin: 0;
  font-size: var(--fs-h3);
  /* Sampled at 400. Re-weighted to the heading rule's 700 — the
     largest of the four such changes, since these titles carry the
     region panel's whole post list. */
}

/* ---- Mobile: the hero splits AROUND the map ----
   User's layout, 2026-09-02. At 390px there is no quadrant to put
   anything in, so the four pieces are dealt above and below the land
   instead of around it: the word and its standfirst at the top of the
   stage, the map in the middle, the figures and then the button at the
   bottom. It is the same reading order as desktop, folded.

   `display: contents` on the lockup is what makes that possible without
   a second copy of the markup: the lockup's three children become items
   of the overlay's own column and can be split across the map, while on
   desktop the lockup stays a real box holding them together in the
   corner. Inherited properties (the centring, the colour) still reach
   them through it — that is the one thing `display: contents` keeps.

   `position: static` has to go with it. An absolutely positioned box is
   blockified, and a blockified `contents` is a plain `display: block` —
   the lockup would quietly become a box again and take its children
   back with it. The overture's own rule
   (.is-js …:not(.is-settled) .map-hero__lockup) outranks both of these,
   so the word still plays centre stage first and only lands up here
   once .is-settled arrives. */
@media (max-width: 767px) {
  .map-hero__overlay {
    display: flex;
    flex-direction: column;
    /* Clear of the transparent header floating over the same stage. */
    padding: calc(var(--header-h) + var(--sp-4)) var(--gutter) var(--sp-5);
  }
  .map-hero__lockup {
    display: contents;
    position: static;
  }
  .map-hero__title { order: 1; }
  .map-hero__cta { order: 2; }
  /* margin-top: auto is the split — everything from here down is packed
     against the bottom of the stage, everything above it against the
     top, and the map shows through the gap between them. */
  .map-hero__stats {
    order: 3;
    margin-top: auto;
  }
  .map-hero__actions { order: 4; }
  /* The lockup's max-width came off with its box; the two text blocks
     still need a measure of their own. */
  .map-hero__title,
  .map-hero__cta,
  .map-hero__stats .hero-stat {
    max-width: min(34rem, 100%);
    margin-inline: auto;
  }
}

/* Mobile: the region list becomes a horizontal snap RAIL pinned at the
   bottom of the hero. Swiping it drives the camera on a guided tour of
   the regions (impact-hero.js watches its scroll position); tapping a
   pill opens the region, exactly like desktop. Same DOM as the desktop
   pill list, so the accessibility contract is untouched. */
@media (max-width: 767px) {
  .map-hero__regions {
    flex-wrap: nowrap;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    border-top: 0;
    padding-block: var(--sp-4);
    /* Lets the first and last pills reach the viewport centre. */
    padding-inline: 50vw;
    scrollbar-width: none;
  }
  .map-hero__regions::-webkit-scrollbar { display: none; }
  .map-hero__regions li {
    flex: none;
    scroll-snap-align: center;
  }
  .map-hero__regions button { white-space: nowrap; }
  /* The pill currently under the camera's attention (set by JS as the
     rail scrolls). Visual only — aria-current still means "open". */
  .map-hero__regions button.is-centered {
    border-color: var(--rhf-ink);
  }
}

/* First prefers-reduced-motion handling in the repo — treat this block as
   the pattern: motion is layered on in the default styles and REMOVED
   here, never the other way around. The 3D scene reads the same media
   query in JS (impact-hero.js) and drops parallax/camera animation. */
@media (prefers-reduced-motion: reduce) {
  /* Selectors repeat the .is-js compound so they outrank the fade-in
     rules above regardless of order. */
  .is-js .map-hero--3d .map-hero__canvas,
  .is-js .map-hero--3d .map-hero__fallback,
  .is-js .map-hero--3d .map-hero__title,
  .is-js .map-hero--3d .map-hero__cta,
  .is-js .map-hero--3d .map-hero__actions,
  .is-js .map-hero--3d .map-hero__stats,
  .region-panel--overlay {
    transition: none;
  }
  /* The title's letters are rest state, not motion: no rise, no stagger.
     The mask stays (it costs nothing once nothing moves through it). */
  .is-js .map-hero--3d .map-hero__title .letter {
    opacity: 1;
    transform: none;
    animation: none;
  }
}



.rhf-home-map {width:100%;}
.rhf-home-map .map-hero--3d {font-family:'General Sans',sans-serif;color:#0a0b0b;min-height:800px;height:100svh;max-height:1100px;--header-h:32px;}
.rhf-home-map .map-hero__stage {min-height:600px;}
.rhf-home-map .map-hero__title {font-family:Nohemi,sans-serif;font-size:clamp(64px,8vw,120px);line-height:1;}
.rhf-home-map .map-hero__cta {font-size:20px;line-height:1.3;}
.rhf-home-map .btn {display:inline-flex;border:2px solid #af272e;border-radius:99px;padding:12px 24px;color:#af272e;font:700 14px/20px 'General Sans',sans-serif;text-decoration:none;}
.rhf-home-map .btn--solid {background:#d22630;color:white;}
.rhf-home-map .visually-hidden {position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;}
.rhf-home-map ul {list-style:none;margin:0;padding:0;}
.rhf-home-map button {font-family:'General Sans',sans-serif;}
.rhf-home-map [hidden] {display:none!important;}
.rhf-home-map .region-detail__name {font-size:clamp(40px,6vw,88px);}
.rhf-home-map .region-posts p {font-size:20px;margin-bottom:24px;}
.rhf-home-map .region-detail__count:has([data-region-count]:empty) {display:none;}
.rhf-home-map .map-hero__stats {display:none;}
.rhf-home-map a:focus-visible,.rhf-home-map button:focus-visible {outline:3px solid #af272e;outline-offset:4px;}
@media(max-width:767px){.rhf-home-map .map-hero--3d {min-height:700px;height:90svh;max-height:1000px;}.rhf-home-map .map-hero__stage{min-height:580px;}.rhf-home-map .map-hero__title{font-size:48px;}.rhf-home-map .map-hero__cta{font-size:16px;}.rhf-home-map .map-hero__lockup{bottom:24px;}.rhf-home-map .region-detail__name{font-size:40px;}}
body .elementor .rhf-home-map .map-hero__title {font-size:clamp(64px,8vw,120px);line-height:1;margin:0 0 24px;}
body .elementor .rhf-home-map .region-detail__name {font-size:clamp(40px,6vw,88px);line-height:1.05;}
body .elementor .rhf-home-map .map-hero__regions button {background:transparent;color:#545963;border:1px solid #a9b2bd;border-radius:99px;padding:10px 16px;font-size:14px;line-height:1.3;font-weight:400;}
body .elementor .rhf-home-map .map-hero__regions button:hover,body .elementor .rhf-home-map .map-hero__regions button.is-hovered,body .elementor .rhf-home-map .map-hero__regions button[aria-current=true] {color:#0a0b0b;font-weight:700;text-decoration:underline;}
body .elementor .rhf-home-map .region-panel__back {background:transparent;color:#af272e;border:2px solid #af272e;border-radius:99px;padding:12px 24px;}
@media(min-width:768px){body .elementor .rhf-home-map .map-hero__regions button {border:0;border-radius:0;padding:0;font-size:13px;}}
@media(max-width:767px){body .elementor .rhf-home-map .map-hero__title{font-size:48px;}body .elementor .rhf-home-map .region-detail__name{font-size:40px;}}
.rhf-home-remaining .rhf-home-map .map-hero--3d .map-hero__title {font-size:clamp(64px,8vw,120px)!important;line-height:1!important;}
.rhf-home-remaining .rhf-home-map .map-hero--3d .map-hero__stage {position:relative;display:block;flex:1;}
.rhf-home-remaining .rhf-home-map .map-hero--3d .map-hero__lockup {position:absolute;}
@media(max-width:767px){.rhf-home-remaining .rhf-home-map .map-hero--3d .map-hero__title{font-size:48px!important;}.rhf-home-remaining .rhf-home-map .map-hero--3d .map-hero__lockup{top:auto;bottom:24px;}}
.rhf-home-remaining .rhf-home-map .map-hero--3d .region-detail__name {font-size:clamp(40px,6vw,88px)!important;line-height:1.05!important;}
.impact-map-figure {position:absolute;right:6%;top:12%;width:26%;text-align:right;transition:opacity .4s;}
.impact-map-figure strong {font:700 clamp(36px,5vw,76px)/1 Nohemi,sans-serif;color:#d22630;}
.impact-map-figure p {font:16px/1.35 'General Sans',sans-serif;color:#111;margin-top:18px;}
.is-detail .impact-map-figure {opacity:0;}
@media(max-width:767px){.impact-map-figure{display:none;}}
