/* Base styles, mobile-first. This is scaffold structure, not final layout.
   Real section layouts get built from the Figma screens. */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--surface-light);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}

/* CHANGE-4: text-wrap: balance here covers every heading, card title, and
   panel title site-wide in one place — .page-title, .splash__title,
   .util__heading/.ctx__heading/.act__heading, .map-tip__title,
   .act-card__title, .about-heading, .src-type, .m-calc-heading, and the
   .util-card__toggle / .ctx-panel__toggle button labels nested inside an h3
   — because all of them are h1/h2/h3 elements, and text-wrap-style is an
   inherited property: setting it here reaches the button labels nested
   inside .util-card__head / .ctx-panel__head too, not just direct text.
   Progressive enhancement, no-op where unsupported — no @supports guard,
   no JS fallback. Pill labels aren't headings, so they get the same
   treatment separately at .pill (see CHANGE-4 there). */
h1, h2, h3 { line-height: var(--lh-tight); margin: 0 0 var(--space-2); text-wrap: balance; }

/* CHANGE-4: body copy — every paragraph on the site (panel copy, tooltip
   prose, card descriptions, page prose) is wrapped in <p>, so one rule on
   the tag covers all of it rather than enumerating each container class. */
p { text-wrap: pretty; }

a { color: var(--accent-on-light); }

.skip-link {
  position: absolute;
  left: -999px;
}
.skip-link:focus {
  left: var(--space-2);
  top: var(--space-2);
  background: #fff;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius);
  z-index: 10;
}

:focus-visible {
  outline: 2px solid var(--accent-on-light);
  outline-offset: 2px;
}

[x-cloak] { display: none !important; }

.container {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--space-2);
}

/* ===================== MASTHEAD ===================== */
/* position:absolute over whatever is at the top of the page. On index.html
   that's the dark splash hero — fine. On sources.html/methods.html/about.html
   the top of <main> is the light --surface-light page-title heading, so this
   off-white foreground will be very low-contrast there. No light-surface
   variant exists yet; flagged, not fixed, since it wasn't in scope here. */
.mast{
  position:absolute; top:0; left:0; right:0; z-index:20;
  padding:1.25rem 1.25rem;
}
@media (min-width:768px){
  .mast{ padding:1.75rem 2.5rem; }
}

.mast__bar{
  position:relative; z-index:2;
  display:flex; align-items:flex-start; justify-content:space-between; gap:1rem;
}

/* brand lockup */
.mast__brand{
  display:inline-flex; align-items:center; gap:.55rem;
  text-decoration:none; color:var(--color-fg, #EAE8E1);
}
.mast__logo{ height:2rem; width:auto; flex:none; display:block; }

/* desktop inline nav */
.mast__nav{ display:none; }
@media (min-width:768px){
  .mast__nav{ display:flex; gap:2rem; align-items:center; padding-top:.4rem; }
}
.mast__link{
  font-family:var(--font-body);
  font-size:.95rem; color:var(--color-fg, #EAE8E1);
}
/* underline = it links or expands. Every nav item is an anchor now that the
   Contact placeholder <span> is gone; the `a.` qualifier is kept so a future
   non-anchor item cannot silently pick up the link affordance. */
a.mast__link{
  text-decoration:underline; text-underline-offset:.35em; text-decoration-thickness:1px;
}
a.mast__link:hover{ opacity:.7; }

/* mobile menu / close toggle */
.mast__menu-btn{
  display:inline-flex; background:none; border:0; cursor:pointer;
  font-family:var(--font-body);
  font-size:.8rem; letter-spacing:.2em; text-transform:uppercase;
  color:var(--color-fg, #EAE8E1);
  text-decoration:underline; text-underline-offset:.35em; text-decoration-thickness:1px;
  padding:.4rem 0;
}
@media (min-width:768px){ .mast__menu-btn{ display:none; } }

/* full-screen overlay */
.mast__overlay{
  position:fixed; inset:0; z-index:1;
  background:rgba(6,16,11,.94);
  padding:5.5rem 1.25rem 1.25rem;
  display:flex; flex-direction:column;
}
.mast__overlay-nav{ display:flex; flex-direction:column; }
.mast__overlay-link{
  font-family:var(--font-serif);
  font-size:clamp(2rem, 9vw, 2.75rem); line-height:1.1;
  color:var(--color-fg, #EAE8E1); text-decoration:none;
  padding:.55em 0 .35em;
  border-bottom:1px solid rgba(234,232,225,.3);
}
.mast__overlay-link:active{ opacity:.7; }

/* Content-page top spacing.
   The masthead is position:absolute (an overlay), so <main> starts at the very
   top of the page under it. On the content pages we push main down so the green
   page title clears the header with the Figma gap. The splash (.page--splash) is
   left alone — the hero fills the top there.

   6rem/4rem (nudged down from an initial 7.5rem/5rem — sat too low) don't land
   on the --space-* scale (--space-4: 2rem jumps straight to --space-6: 3rem,
   nothing near either value), so swapping to the nearest token would still miss
   the Figma gap. Left as literal rem, matching the note to tune to Figma over
   forcing token reuse.

   The doubling risk here isn't the title's margin (h1/h2.page-title already
   zero their own margin-top) — it's .section's own padding-top: var(--space-4),
   which every page's first section carries. Zeroed below, per the note's
   "adjust padding-top to match" contingency. */
.page--content main{
  padding-top:6rem;   /* desktop: header band + gap before the page title. tune to Figma. */
}
.page--content main > :first-child{
  margin-top:0;         /* stop the title's own leading margin from doubling the gap */
  padding-top:0;        /* .section's own padding-top would otherwise stack on top of this */
}
@media (max-width:767px){
  .page--content main{ padding-top:4rem; }
}

/* ================= CONTENT PAGES — SHARED FOUNDATION ================= */
.page--content{ --gg-rule: rgba(18, 33, 26, .12); }  /* hairline; not a locked token, scoped local */

/* Header on the light surface: nav goes ink; current page green.
   The lockup no longer inherits colour — content pages point at logo-black.svg
   in markup, the splash and every footer at logo-white.svg. */
.page--content .mast__brand,
.page--content .mast__link,
.page--content .mast__menu-btn{ color: var(--ink); }
.page--content .mast__link[aria-current="page"]{ color: var(--accent-on-light); }
/* the mobile overlay is a dark scrim on every page, so its active link uses the dark-surface green */
.mast__overlay-link[aria-current="page"]{ color: var(--accent); }

/* Page title (Sources / Methods / About) */
.page--content .page-title{
  font-family: var(--font-serif);
  font-size: clamp(3.25rem, 8vw, 7rem);
  line-height: var(--lh-tight);
  font-weight: 600;
  color: var(--accent-on-light);
  margin: 0 0 var(--space-4);
}

/* Section heading + hairline rule (Federal Data / What is GridGap? / Calculations) */
.src-type,
.about-heading,
.m-calc-heading{
  font-family: var(--font-serif);
  font-size: clamp(2.25rem, 4vw, 3rem);
  line-height: var(--lh-tight);
  font-weight: 600;
  color: var(--ink);
  /* half the block gap: reads as a section opener, less air than between
     blocks but more than a block title gives its own rows */
  margin: var(--space-6) 0 calc(var(--m-block-gap) * .5);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--gg-rule);
}
.about-heading-lg{ font-size: clamp(2.25rem, 4vw, 3rem); }  /* FAQs; bump here if the Figma runs larger */

/* Sub-section heading (The PGE estimate) — no rule; the rows carry the dividers */
.m-block-title{
  font-family: var(--font-serif);
  font-size: var(--fs-h2);
  line-height: var(--lh-tight);
  font-weight: 600;
  color: var(--ink);
  /* Top margin lives on .m-block so one var owns the inter-block air (the two
     would collapse anyway). A quarter of the gap below keeps the title bound
     to its own rows rather than floating between blocks. */
  margin: 0 0 calc(var(--m-block-gap) * .25);
}

/* Source-card title */
.src-title{
  font-family: var(--font-serif);
  font-size: var(--fs-h3);
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 var(--space-1);
}

/* Body copy: source descriptions, About prose, FAQ answers */
.src-desc,
.about-copy{
  font-family: var(--font-serif);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink);
  margin: 0 0 var(--space-2);
}

/* Chips / pills. Green outline = short label + primary + applies; muted text = identifier */
.src-chip{ display:flex; flex-wrap:wrap; align-items:center; gap:var(--space-1); margin:0 0 var(--space-2); }
.src-chip-label,
.src-chip-primary,
.m-applies{
  display:inline-flex; align-items:center;
  font-family: var(--font-body);
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent-on-light);
  border: 1px solid var(--accent-on-light);
  border-radius: 999px;
  padding: .15rem .5rem;
  line-height: 1.2;
}
.src-chip-id{
  font-family: var(--font-body);
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* Calc-row label (SUBSTATION COST) */
.m-label{
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink);
}

/* Outbound links (Visit … ↗, source citations) */
.src-link{
  font-family: var(--font-body);
  font-size: var(--fs-small);
  color: var(--accent-on-light);
  text-decoration: underline;
  text-underline-offset: .2em;
}


/* Example section: operators, the one binding wired for the scaffold. */
.section { padding: var(--space-4) 0; }
.section > h2 {
  font-family: var(--font-head);
  font-size: var(--fs-h1);
  color: var(--accent-on-light);
}

.op-grid {
  display: grid;
  gap: var(--space-2);
  grid-template-columns: 1fr;
}
.op-card {
  background: #fff;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: var(--radius);
  padding: var(--space-3);
}
.op-card h3 {
  font-family: var(--font-head);
  font-size: var(--fs-h2);
  margin-bottom: var(--space-1);
}
.op-hook {
  font-family: var(--font-serif);
  margin: 0 0 var(--space-2);
}
.op-meta {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  color: var(--ink-muted);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  align-items: center;
}
.op-mw { color: var(--ink); font-weight: 600; }
.badge {
  font-family: var(--font-body);
  font-size: 0.75rem;
  padding: 0.1em 0.6em;
  border-radius: 999px;
  border: 1px solid var(--accent-on-light);
  color: var(--accent-on-light);
}

/* ==================== CONTENT-PAGE LAYOUT ==================== */
.page--content{
  --content-w: 40rem;   /* readable content column */
  --side-w: 9rem;       /* sidebar width */
  --side-gap: 4rem;     /* gap between sidebar and content */

  /* Air between Methods blocks. Local to this page: --space-6 (3rem) is the
     largest global step and the Figma wants more, so this is a page-scoped
     var rather than a distorted global token. The title->first-row and
     "Calculations"->first-block gaps are derived from it so the hierarchy
     scales together. Desktop value is set in the 60rem block below. */
  --m-block-gap: 4rem;
}
.page--content .container{ max-width: 66rem; }

/* ---------- mobile / narrow (default, < 60rem) ---------- */
.about-layout,
.src-layout,
.m-layout{ display: block; }

.about-nav{ display: none; }            /* About: no sidebar on mobile */

.src-nav,
.m-nav{                                  /* Sources + Methods: horizontal scrolling rail */
  display: flex;
  gap: var(--space-3);
  overflow-x: auto;
  white-space: nowrap;
  scrollbar-width: none;
  margin: 0 calc(-1 * var(--space-2)) var(--space-3);
  padding: 0 var(--space-2) var(--space-2);
  font-family: var(--font-body);
  font-size: var(--fs-small);
}
.src-nav::-webkit-scrollbar,
.m-nav::-webkit-scrollbar{ display: none; }

/* ---------- desktop (>= 60rem): content centered, sidebar in the left gutter ---------- */
@media (min-width: 60rem){
  .page--content{ --m-block-gap: 6rem; }

  .about-layout,
  .src-layout,
  .m-layout{
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--content-w) minmax(0, 1fr);
    align-items: start;
  }
  .about-content,
  .src-groups,
  .m-content{ grid-column: 2; }

  .about-nav,
  .src-nav,
  .m-nav{
    grid-column: 1;
    justify-self: end;
    width: var(--side-w);
    margin: var(--space-6) var(--side-gap) 0 0;
    padding: 0;
    position: sticky;
    top: var(--space-4);
    display: block;
    overflow: visible;
    white-space: normal;
  }
  .src-nav-link,
  .m-nav-link{ display: block; margin-bottom: var(--space-2); }

  /* Page title sits on the centered content column. #methods is NOT capped
     here any more: .m-layout is a three-column grid that needs the full
     container width so the subnav can sit in the left gutter, and .m-content
     is what carries the reading measure. */
  .page--content .page-title{
    max-width: var(--content-w);
    margin-left: auto;
    margin-right: auto;
  }
}

/* Sources. Grouped by Data Type; see $store.gg.sourcesByType(). Flat,
   divided list per the Figma screens, not the boxed op-card treatment. */
/* Every rail item is an anchor, so every rail item is underlined — the
   site-wide rule that an underline means it links or expands. The active
   item is marked by COLOUR alone (green, as in the masthead on light
   surfaces); the underline is no longer the active-state signal, because
   using it that way made the four inactive links read as non-clickable. */
.src-nav-link,
.m-nav-link {
  flex: 0 0 auto;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 0.25em;
}
.src-nav-link.is-active,
.m-nav-link.is-active {
  color: var(--accent-on-light);
}

.src-group { margin-bottom: var(--space-4); }
.src-card {
  padding: var(--space-3) 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
.src-card:first-child { padding-top: 0; }
.src-card:last-child { border-bottom: none; }
.src-meta {
  font-size: var(--fs-body);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  align-items: center;
}

/* Methods. Grouped by Block; see $store.gg.methodsByBlock(). "Calculations"
   is the single wrapping heading over the whole block list (see the
   markup note in index.html), not per-block. */
.m-block { margin-top: var(--m-block-gap); }
.m-block:first-of-type { margin-top: 0; }

/* Row: label above value on mobile, label column + value column from the
   tablet breakpoint. Attribution wraps to its own line under the value
   column (not the label) at that breakpoint. */
.m-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-2) 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
.m-row:last-child { border-bottom: none; }
.m-value {
  font-family: var(--font-serif);
  color: var(--ink);
}
.m-attribution {
  font-family: var(--font-body);
  font-size: var(--fs-small);
  color: var(--ink-muted);
  /* Block, NOT flex. As flex children each citation was its own unbreakable
     item and the separator could wrap to the head of a line; as inline text
     the line breaks between words and the "·" stays with the item it follows.
     Separation is the middot's own margins now, not a flex gap. */
  display: block;
}

@media (min-width: 40rem) {
  .m-row {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--space-1) var(--space-3);
  }
  .m-label {
    flex: 0 0 13rem;
    white-space: nowrap;
    padding-top: 0.2em;
  }
  .m-value { flex: 1 1 0; }
  .m-attribution {
    flex-basis: 100%;
    margin-left: calc(13rem + var(--space-3));
  }
}

/* Convention row 1 — Row Label "Intro": full-width prose above the rows.
   A <p>, not a .m-row, so it carries no label cell and no hairline. */
.m-intro {
  font-family: var(--font-serif);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink);
  margin: 0 0 var(--space-3);
}

/* Convention row 2 — Row Label ending "rationale" / "example": the visible
   label cell is emptied, but the text is only ever CLIP-hidden, never
   display:none, so it stays in the accessibility tree.
     < 40rem: rows stack, so the whole cell leaves the flow — an empty label
              box above the value would just read as a gap.
     >=40rem: the cell stays in flow and keeps its 13rem basis, which is what
              holds the value aligned with every other row's value. Only the
              text inside is hidden. The hairline is untouched at both. */
.m-row--nolabel .m-label {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
@media (min-width: 40rem) {
  .m-row--nolabel .m-label {
    position: static;
    width: auto; height: auto;
    overflow: visible;
    clip-path: none;
  }
  .m-row--nolabel .m-label__text {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* Footnotes: Display Value already carries the leading */** and full
   sentence (see content.json), so only it renders — no heading, no
   label column, no attribution/applies-to row. */
/* Footnotes sit closer than a normal block — deliberate, and scaled with the
   block gap so the relationship survives the larger spacing. */
.m-block[data-block="Footnotes"] { margin-top: calc(var(--m-block-gap) * .5); }
.m-block[data-block="Footnotes"] .m-block-title,
.m-block[data-block="Footnotes"] .m-label,
.m-block[data-block="Footnotes"] .m-attribution {
  display: none;
}
.m-block[data-block="Footnotes"] .m-row {
  padding: 0 0 var(--space-1);
  border-bottom: none;
}
.m-block[data-block="Footnotes"] .m-value {
  font-style: italic;
  font-size: var(--fs-small);
  color: var(--ink-muted);
}

/* Reversed August 24, 2026 (was: hide the whole attribution line, Aug 12).
   The source citation is back; the PGE / Pacific Power / Map chips stay hidden,
   so only the citation returns. Placement mirrors .src-meta under .src-desc on
   Sources: small, muted, under the value, clearly subordinate to it — the
   >=40rem rule above already indents it to the value column. */
.m-applies{ display: none; }

/* Citation colour for this surface. .cite-link deliberately carries no colour
   of its own (see its comment) so each surface sets one; Methods is a light
   surface, same as the tooltip. */
.m-attribution .cite-link{ color: var(--accent-on-light); }
.m-cite,
.m-cite-item{ display: inline; }

/* Site middot, matching how Sources chips compose "Short Label · Identifier".
   Its own element, never inside an anchor, so it is neither underlined nor
   part of the link text. The <wbr> after the glyph in the markup is the only
   break opportunity around it: a line can break AFTER the dot, never before,
   so a wrapped citation line cannot open with a stranded "·". */
.m-cite-sep{
  color: var(--ink-muted);
  text-decoration: none;
  margin: 0 .35em;
}

/* About. Flat, ordered block walk; see $store.gg.aboutOrdered() and the
   markup notes above the section for how Heading Style picks the mode. */
.about-block { margin-top: var(--space-4); }
.about-block:first-child { margin-top: 0; }

.about-copy:last-child { margin-bottom: 0; }
/* Closing contact line. Inherits .about-copy for the serif body voice; the
   extra top margin sets it off from the last content block. The link is a
   real link, so it keeps the site's underline affordance and the
   light-surface green from the global `a` rule. */
.about-contact { margin-top: var(--space-4); }
.about-contact-link { text-underline-offset: .2em; }

/* Inline label: no heading tag, {Name} as a bold lead-in on the first
   paragraph only (see the x-if pIdx === 0 in the markup). Set in italic
   to read as an aside from the surrounding H2 body copy. */
.about-inline .about-copy { font-style: italic; }
.about-inline strong { font-style: normal; }

/* FAQ accordion, inside the "Page H1" (FAQs) block. Each item owns its
   own local `open` state — multiple can be open at once. */
.faq-list {
  display: grid;
  gap: var(--space-2);
}
.faq-item {
  background: #fff;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: 12px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
  overflow: hidden;
}
.faq-question {
  display: flex;
  align-items: flex-start;
  /* Caret-to-question spacing. --space-2 was sized for the old glyph, which
     carried its own side bearings; the drawn caret is a bare .5em triangle
     and needs far less room beside it. */
  gap: var(--space-1);
  width: 100%;
  padding: var(--space-3);
  background: none;
  border: none;
  font: inherit;
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: var(--fs-h3);
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
/* .faq-question is a flex row with align-items: flex-start, so this caret is
   a flex item rather than inline — the baseline trick above does not apply
   and it is nudged down to meet the first line of a large serif question.
   Rotation, size, colour and the reduced-motion gate all come from .caret. */
.faq-caret {
  margin-top: 0.3em;
  color: var(--ink);
}
/* The only flex toggle on the site: .faq-question's `gap` already separates
   caret from label, so .caret's inline-context margin-right stacks on top of
   it and doubles the space. Doubled class rather than a bare .faq-caret
   override because .caret is declared LATER in this file and would otherwise
   win on source order at equal specificity. */
.caret.faq-caret { margin-right: 0; }
.faq-answer {
  padding: var(--space-3);
  border-top: 1px solid rgba(0, 0, 0, 0.12);
}
.faq-answer .about-copy:first-child { margin-top: 0; }

/* About's sidebar TOC is hidden below this breakpoint entirely rather than
   becoming a mobile rail like Sources, since the screens show no in-page nav
   below it. Layout/positioning for .about-layout/.about-nav now lives in the
   consolidated Sources+About rule above; only the link color rules stay here. */
@media (min-width: 60rem) {
  /* Same rule as the Sources and Methods rails: every item underlined
     because every item is a link, active marked by colour. Kept here rather
     than merged into that shared block because About's rail exists only at
     this breakpoint. */
  .about-nav-link {
    display: block;
    margin-bottom: var(--space-2);
    font-family: var(--font-body);
    font-size: var(--fs-small);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--ink);
    text-decoration: underline;
    text-underline-offset: 0.25em;
  }
  .about-nav-link.is-active {
    color: var(--accent-on-light);
  }
}

.site-footer {
  border-top: 1px solid rgba(0, 0, 0, 0.08);
  padding: var(--space-4) 0;
  color: var(--ink-muted);
  font-size: var(--fs-small);
}

/* ===================== FOOTER (.foot) ===================== */
.foot{
  --foot-fg: #f4f5f6;
  --foot-fg-muted: rgba(244, 245, 246, .6);
  background: var(--panel-blue);
  color: var(--foot-fg);
  font-family: var(--font-body);
  padding: var(--space-6) 1.25rem;   /* full-bleed: no margin, no radius. H-padding matches the masthead so the logos line up */
}
@media (min-width: 60rem){
  .foot{ padding: var(--space-6) 2.5rem; }
}

/* MOBILE FIRST is the stacked mock: one centred column. Desktop splits it
   into the two columns below. */
.foot__inner{
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Deliberately TIGHT. Five centred children on one even rhythm read as an
     unrelated list; the cluster break between the nav/icons pair and the
     identity block is added back as a margin on .foot__brand below, so the
     spacing carries the grouping instead of flattening it. */
  gap: var(--space-1);
  text-align: center;
}

/* The mobile mock's vertical order is nav, icons, logo, Terms, tagline —
   which interleaves the two desktop columns rather than stacking them, so
   `display: contents` flattens both wrappers and lets all five children be
   ordered against each other in one flex column. Same technique as the
   compact action cards; the DOM keeps the desktop-meaningful grouping. */
.foot__brand-col,
.foot__nav-col{ display: contents; }
/* Mobile order per the mobile mock: nav, icons, logo, Terms, tagline. The
   DESKTOP mock puts the tagline above Terms instead, so the desktop block
   resets these to DOM order rather than reusing this sequence. */
.foot__links  { order: 1; }
.foot__icons  { order: 2; }
.foot__brand  { order: 3; }
.foot__terms  { order: 4; }
.foot__tagline{ order: 5; }

.foot__brand{
  display: inline-flex;
  align-items: center;
  color: var(--foot-fg);
  text-decoration: none;
  /* Identity block starts here. The mock gives the nav, the icon row and the
     lockup a lot of air between them and closes tight on Terms/tagline, so
     the big gaps are margins on the cluster heads rather than one even gap. */
  margin-top: 4rem;
}
/* Matches .mast__logo exactly — the mock lines the footer lockup up with the
   masthead's rendered height, not with its own former 2.25rem. */
.foot__logo{ height: 2rem; width: auto; flex: none; display: block; }

.foot__tagline{
  margin: 0;
  color: var(--foot-fg);
  font-size: var(--fs-small);
  line-height: var(--lh-body);
}

.foot__links{
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  /* --space-4 between items pushed this row out to the full content width on
     a 390px screen, which is what made it read as scattered rather than as
     one row. Desktop restores the wider gap. */
  gap: var(--space-1) var(--space-2);
}

/* Terms sits with the nav links visually, so it takes the same treatment
   rather than inventing a second link style for one item. */
.foot__terms{ margin-top: var(--space-2); }

.foot__link,
.foot__terms{
  color: var(--foot-fg);
  font-size: var(--fs-small);
  text-decoration: underline;
  text-underline-offset: .3em;
}

.foot__icons{
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-top: 4rem;
}
/* The icons are links, but their affordance is the glyph itself — an
   underline under an SVG reads as a stray rule, so they opt out of the
   site's underline rule and keep only the shared hover. */
.foot__icon{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 44px touch target on a 20px glyph. The box is padded rather than the icon
     scaled up, so the mark stays the mock's size while the tap area is
     usable; the negative margin keeps that padding from widening the row.
     Desktop drops back to the bare glyph. */
  min-width: 2.75rem;
  min-height: 2.75rem;
  margin: calc(var(--space-1) * -1);
  color: var(--foot-fg);
  text-decoration: none;
}
/* Height only: each glyph has its own viewBox aspect (the envelope is 4:3,
   Instagram is square-ish), so a fixed width would squash one of them. */
.foot__icon svg{ height: 1.375rem; width: auto; display: block; }

/* One hover for every footer affordance. The icons inherit it through
   currentColor on their fill, so they dim with the links rather than
   needing a colour of their own. */
.foot__link:hover,
.foot__terms:hover,
.foot__icon:hover{ opacity: .75; }

/* DESKTOP: brand column left, nav + icons right-aligned under each other. */
@media (min-width: 48rem){
  .foot__inner{
    flex-direction: row;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--space-4);
    text-align: left;
  }
  /* Both wrappers become real boxes again. The mobile `order` values do NOT
     stop applying when that happens — they just re-sort within each column,
     which put Terms above the tagline. Desktop wants plain DOM order in both
     columns (logo, tagline, Terms / links, icons), so they are reset here. */
  .foot__brand-col,
  .foot__nav-col{
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
  }
  /* Tighter than the column default: the lockup, the tagline and Terms read
     as one identity block, so they sit close together rather than as three
     evenly-spaced rows. */
  /* gap: 0 — the desktop mock sets the lockup off from the tagline but closes
     tight on Terms, so the one gap that exists is a margin on the tagline
     rather than an even column gap that would separate both pairs equally. */
  .foot__brand-col{ align-items: flex-start; gap: 0; }
  .foot__tagline  { margin-top: var(--space-2); }
  .foot__nav-col  { align-items: flex-end; gap: var(--space-4); }
  .foot__links    { justify-content: flex-end; gap: var(--space-2) var(--space-4); }
  /* The mobile cluster break and touch padding are both column-stack
     concerns; side by side they would only knock the two columns out of
     alignment with each other. */
  .foot__brand    { margin-top: 0; }
  .foot__terms    { margin-top: 0; }
  .foot__icons    { margin-top: 0; gap: var(--space-4); }
  .foot__icon     { min-width: 0; min-height: 0; margin: 0; }
  /* Desktop mock order: logo, tagline, Terms — the reverse of mobile's
     closing pair, so DOM order is restored rather than the mobile sequence. */
  .foot__links, .foot__icons, .foot__brand,
  .foot__terms, .foot__tagline{ order: 0; }
}

/* Tablet and up: two-column operator grid. */
@media (min-width: 40rem) {
  .op-grid { grid-template-columns: 1fr 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* ===== SCAFFOLD ONLY — 2026-08-13. Replaced by the styling pass. ===== */
.splash { position: relative; overflow: hidden;
  min-height: 100vh; min-height: var(--deck-h); display: grid;
  grid-template-rows: 1fr auto; gap: var(--space-4);
  background: var(--panel-dark); color: #EAE8E1; padding: var(--space-6) 1.25rem;
  --deck-w: 40rem; }

/* Mobile crop is a genuinely different composition from the desktop one,
   not the same image at another size, so this is a source swap rather than
   a background-size change. The element is inset -4% on every side and
   starts at scale(1) so the drift can grow to 1.06 without ever exposing an
   edge. Sits at z-index 0 with the content above it at z-index 1. */
.splash__bg { position: absolute; inset: -4%; z-index: 0;
  background-image: url("../img/splash-mobile.jpg");
  background-size: cover; background-position: center;
  transform-origin: 70% 50%;
  animation: splash-drift 20s ease-in-out infinite alternate; }
@media (min-width: 48rem) {
  .splash__bg { background-image: url("../img/splash-desktop.jpg"); }
}

/* Drift scales toward 70% 50% — the bright teal side. The copy sits over
   the dark left region on both crops, so pushing the transform origin right
   moves the bright area further right as it grows, away from the text,
   instead of creeping under it and eating the contrast. Do not centre this
   origin without re-checking the copy's legibility. */
@keyframes splash-drift {
  from { transform: scale(1); }
  to   { transform: scale(1.12); }
}
/* Continuous background motion is a common vestibular trigger, and the map
   camera and accordion scrolls already respect this setting. */
@media (prefers-reduced-motion: reduce) {
  .splash__bg { animation: none; }
}

.splash__inner { position: relative; z-index: 1;
  display: grid; align-content: center; align-content: safe center;
  width: 100%; max-width: var(--deck-w); margin: 0 auto; }
.splash__title { font-family: var(--font-serif); font-weight: 400;
  font-size: clamp(2.5rem, 7vw, 5rem);
  line-height: var(--lh-tight); margin: 0 0 var(--space-4); }
.splash__copy { border-left: 2px solid rgba(255,255,255,.35); padding-left: var(--space-3);
  font-family: var(--font-serif); }

/* The splash flow pill is a sibling of .splash__inner, not nested inside
   it, so it needs its own stacking context above .splash__bg — scoped here
   rather than on the shared .pill class, which every other section's flow
   pill also uses without a background layer to clear. */
.splash > .pill { position: relative; z-index: 1; }

/* Flow pills — ONE shared class + two contrast modifiers, reused by the
   splash/map/utility/context pills that each link to the next deck section.
   The colour is contrast against the section's own background, not a
   per-button identity: light pill on dark sections, dark pill on light ones. */
/* CHANGE-3: the map pill is the only <button> among the four flow pills
   (splash/map/util/ctx — .act deliberately has none); the other three are
   <a>. Border-width was already 0 on both (not the cause) — the real split
   is that a native <button> keeps `appearance: auto` unless told otherwise,
   and a browser's native button widget uses its own line-height handling
   under auto appearance instead of the inherited var(--lh-body) an <a>
   picks up normally. That alone was worth ~4px: line-height: normal on a
   13px button vs the inherited ~20px line-height everywhere else.
   appearance: none removes the native widget; the explicit line-height
   means neither variant depends on inheritance behaving the same way
   across tag types to land on the same number.
   CHANGE-4: text-wrap: balance for pill labels — not a heading, so it isn't
   covered by the h1/h2/h3 rule; called out separately since .pill is a
   shared rule already, not one written just for this. ORDER MATTERS:
   text-wrap is shorthand for text-wrap-mode + text-wrap-style, and
   white-space is shorthand for white-space-collapse + that SAME
   text-wrap-mode — declared in either order, the later one wins the shared
   longhand. text-wrap: balance first, white-space: nowrap second, so nowrap
   is what actually governs wrapping; text-wrap-style: balance is left set
   but inert, since nowrap means no line-breaking opportunities exist for it
   to act on. Reversed, nowrap silently loses and pills go back to wrapping
   whenever their label doesn't fit — this exact reversal was caught during
   verification: the map pill still measured 1 line only because ITS
   current copy happens to fit unwrapped, not because nowrap was in force. */
.pill { justify-self: center; display: inline-block; appearance: none; border: 0; cursor: pointer;
  padding: .625rem 1.25rem; border-radius: 999px; text-decoration: none;
  font-family: var(--font-body); font-size: var(--fs-small); line-height: var(--lh-body);
  text-wrap: balance; white-space: nowrap; }
.pill--light { background: #d9d9d4; color: #12211a; }
.pill--dark { background: var(--panel-dark); color: #fff; }

/* Containing block for the mobile pill overlay below. The >=60rem block
   further down redeclares this redundantly — left as-is there, untouched. */
.map { position: relative; }

.map__canvas { position: relative; height: 100vh; height: var(--deck-h); background: var(--panel-dark);
  overflow: hidden; }

/* CHANGE-2: close the selected-state height gap. Mobile: at rest the canvas
   alone fills the section (its base height above), exact. Once a tooltip is
   open, canvas + panel need to sum to exactly one viewport too — but
   .map__panel's max-height (below) is a CAP, not a fixed size, so a short
   tooltip doesn't fill it and a fixed 45/55 split left a gap below both.
   .map is not otherwise a grid — this scopes display:grid to the selected
   state only, one row each: canvas gets the flexible 1fr track (floored so
   a very tall tooltip can't crush the map below something reasonable),
   panel keeps an auto track sized by its own actual content, still capped
   by its own max-height exactly as before. With only min-height set (not a
   fixed height) on the container, the 1fr track still correctly absorbs
   whatever the auto track doesn't use — the standard `1fr auto` + min-height
   sticky-footer pattern, not something specific to this file.
   Deliberately NOT animated, same reason as before: an instant change avoids
   waiting on transitionend before the resize()+camera nudge in the map
   section's x-data. Desktop is untouched — it pins the panel absolutely over
   a full-height canvas instead of sharing vertical space with it, so it
   never had this problem. */
@media (max-width: 59.99rem) {
  .map.is-selected {
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: minmax(calc(var(--deck-h) * 0.35), 1fr) auto;
    min-height: var(--deck-h);
  }
  .map.is-selected .map__canvas {
    grid-row: 1;
    grid-column: 1;
    height: auto;
  }
  .map.is-selected .map__panel {
    grid-row: 2;
    grid-column: 1;
  }
}
/* Map HUD: hint + persistent caption, stacked at the TOP of the canvas.
   Neither is interactive — a PILL means it navigates, an UNDERLINE means it
   links or expands, and this hint does neither, so it's plain italic text
   like the caption below it (reuses .map__caption directly rather than a
   parallel rule — see the markup). pointer-events are off so this stack can
   never eat a map drag, and re-enabled on the single link inside it. */
.map__hud { position: absolute; top: var(--space-3); left: 50%; transform: translateX(-50%);
  z-index: 3; display: flex; flex-direction: column; align-items: center; gap: .75rem;
  width: max-content; max-width: min(28rem, calc(100% - 2rem)); pointer-events: none; }
.map__circle {
  display: grid; place-items: center;
  background: none; border: none; padding: 0; cursor: pointer;
}
/* Focus ring routed to the dot, not the button — a square hit box would
   otherwise draw a square ring around a round mark. Same treatment as the
   file's global :focus-visible rule above (2px solid var(--accent-on-light),
   2px offset), just relocated. */
.map__circle:focus-visible { outline: none; }
.map__circle:focus-visible .map__circle-dot { outline: 2px solid var(--accent-on-light); outline-offset: 2px; }
.map__circle-dot { border-radius: 50%;
  border: 2px solid var(--accent-on-light); background: rgba(26,122,74,.18); }
.map__circle.is-active .map__circle-dot { background: rgba(26,122,74,.5); }
.map__circle-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.map__caption { margin: 0; color: var(--ink); font-family: var(--font-serif);
  font-style: italic; font-size: var(--fs-body); line-height: var(--lh-body);
  text-align: center; text-wrap: balance; }
.map__caption-link { display: block; margin-top: .35rem; pointer-events: auto;
  color: var(--ink); font-family: var(--font-body); font-style: normal;
  font-size: var(--fs-small); text-decoration: underline; text-underline-offset: 3px; }
/* CHANGE-5: the shadow lives here, not on .map-tip (the visible white card
   inside), because .map__panel has overflow-y: auto — an element's own
   overflow clips its DESCENDANTS' overflowing content, not its own
   box-shadow, but .map-tip is a descendant, so a shadow set there would get
   clipped on every side except whichever edge happens to align with
   .map__panel's own edge. .map__panel itself has no visible fill or radius
   of its own, so border-radius here is purely to make ITS shadow trace the
   same curve as .map-tip's corners, not to round any visible surface — it
   is otherwise transparent and exactly matches .map-tip's footprint (zero
   padding, same width). Same rule covers both breakpoints: the >=60rem
   absolute-positioned overlay further down (position:absolute; z-index:4)
   reads correctly on every side. Mobile needed a SEPARATE stacking fix
   before this shadow could read on its own top edge — .map__canvas is
   `position: relative`, and per CSS2.1 painting order a positioned element
   paints in a later step than in-flow static content regardless of DOM
   order, so canvas was painting over the panel's upward shadow spill even
   though panel comes later in the DOM. Confirmed with a controlled pixel
   diff before touching anything, not assumed. See the mobile media query's
   .map__panel rule (position: relative; z-index: 2) for the fix and the
   z-index reasoning — kept separate from this rule since one is a shadow
   value and the other is a stacking-context change. */
.map__panel {
  position: static;
  width: 100%;
  max-height: calc(var(--deck-h) * 0.55);
  overflow-y: auto;
  border-radius: var(--radius);
  box-shadow: var(--shadow-float);
}

/* Persistent scroll indicator for .map__panel. iOS overlay scrollbars only
   paint during an active scroll, so a reader who hasn't scrolled yet has no
   signal there's more below — and the panel can open already scrolled past
   its own title. Track is sticky so it stays pinned at the panel's visible
   top edge while the panel scrolls beneath it; thumb is positioned/sized by
   updateTipScroll() in the map's x-data, not CSS. Both pointer-events: none
   — this is informational only, never a draggable control. */
.map__scroll-track { position: sticky; top: 0; height: 0; z-index: 2;
  pointer-events: none; }
.map__scroll-thumb { position: absolute; top: 0; right: .25rem; width: 2px;
  border-radius: 999px; background: rgba(18,33,26,.35); display: none; }

.map-tip { position: relative; background: #fff; color: var(--ink); padding: var(--space-3);
  border-radius: var(--radius); }
/* CHANGE-5: no colour was ever set here, so the × inherited nothing useful
   and fell back to the UA default accent a <button> gets when unstyled —
   iOS blue. .map-tip has a white background, so per the two-token green
   rule this is --accent-on-light, not --accent (which fails contrast on a
   light surface). Set on the element itself, not relied on from an
   ancestor, since .map-tip's own color: var(--ink) would otherwise win by
   proximity and this needs to differ from it. */
.map-tip__close { position: absolute; top: .5rem; right: .5rem; border: 0; background: none;
  color: var(--accent-on-light); font-size: 1.25rem; cursor: pointer; }
.map-tip__eyebrow { margin: 0; font-family: var(--font-body); font-size: var(--fs-small);
  text-transform: uppercase; letter-spacing: .06em; color: var(--accent-on-light); }
.map-tip__title { font-family: var(--font-serif); font-weight: 400; font-size: 2.125rem;
  line-height: var(--lh-tight); margin: .35rem 0 .75rem; }
.map-tip__hook { font-family: var(--font-serif); font-size: var(--fs-body);
  line-height: var(--lh-body); margin: 0 0 var(--space-2); }
.map-tip__stats { display: flex; border: 1px solid var(--gg-rule, rgba(18,33,26,.12)); }
.map-tip__stat { flex: 1; padding: .5rem .75rem; }
.map-tip__stat + .map-tip__stat { border-left: 1px solid var(--gg-rule, rgba(18,33,26,.12)); }
.map-tip__stat-label { display: block; margin-bottom: .35rem; font-family: var(--font-body);
  font-size: var(--fs-small); text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink-muted); }
/* Mono per the Figma. Only the FIRST stat (homes equivalent) is green; the
   local comparison figure beside it is ink, so colour marks which number is
   GridGap's derived figure and which is the external reference point. */
.map-tip__stat-value { font-family: var(--font-mono); font-size: 1.25rem; color: var(--ink); }
.map-tip__stat:first-child .map-tip__stat-value { color: var(--accent-on-light); }
.map-tip__toggle, .util-card__toggle, .util-card__calc-toggle, .ctx-panel__toggle {
  display: block; width: 100%; text-align: left; border: 0; background: none; cursor: pointer;
  font-family: inherit; color: inherit; padding: .5rem 0; }
/* The underline sits on the label span, not the button — same reason as
   .util-card__toggle-label and .cite-link: a descendant cannot cancel an
   ancestor's text-decoration, so underlining the button would underline the
   caret too. */
.map-tip__toggle { padding: var(--space-2) 0 var(--space-2); font-family: var(--font-body);
  font-size: var(--fs-small); text-transform: uppercase; letter-spacing: .06em;
  color: var(--accent-on-light); text-decoration: none; }
.map-tip__toggle-label { text-decoration: underline; text-underline-offset: 3px; }

/* ---- Shared disclosure caret ----------------------------------------
   Every toggle on the site used a font glyph for this: U+25BC/U+25BA in the
   Alpine ternaries here, U+25B8 on the About/FAQ accordions. None of those
   are in Source Serif or Radio Canada, so each one rendered from whatever
   the OS fell back to — compressed on iOS in the utility cards, misaligned
   against the label on the accordions on both platforms. Drawing the
   triangle removes the font from the question entirely.

   clip-path over a border triangle: the box keeps real width and height, so
   it can be sized in em (tracking the toggle's own font-size, which is how
   the per-site .util-card__caret / .ctx-panel__caret modifiers still scale
   it) and rotated about its own centre. A border triangle is a zero-size box
   whose visual centre is not its transform origin, so it drifts as it turns.

   currentColor keeps the caret on the shared four-toggle rule's `color:
   inherit` behaviour — it follows whatever the surface gives the label,
   light-on-navy or accent-on-light, with no per-surface caret colour.

   The ONE state is the 90deg rotation, driven by the same open-state binding
   that already drives x-show, so a caret cannot disagree with its panel. */
.caret {
  display: inline-block;
  flex: none;
  width: .5em; height: .5em;
  margin-right: .5em;
  background: currentColor;
  /* Right-pointing triangle. The 15% left inset trims the flat back edge so
     the shape reads centred in its box once rotated. */
  clip-path: polygon(15% 0%, 100% 50%, 15% 100%);
  transform: rotate(0deg);
  transform-origin: 50% 50%;
}
/* An inline-block with no in-flow content baselines on its bottom margin
   edge, which puts a .5em box's centre at .5em/2 above the baseline — level
   with the centre of lowercase text. No vertical-align nudge needed. */
.caret.is-open { transform: rotate(90deg); }
@media (prefers-reduced-motion: no-preference) {
  .caret { transition: transform .15s ease; }
}

.map-tip__detail { border-top: 1px solid var(--gg-rule, rgba(18,33,26,.12));
  padding-top: var(--space-2); }
.map-tip__caveat { margin: 0 0 var(--space-2); font-family: var(--font-serif);
  font-style: italic; font-size: var(--fs-small); line-height: var(--lh-body);
  color: var(--ink-muted); }

.map-tip__rows dt { margin-bottom: .25rem; font-family: var(--font-body);
  font-size: var(--fs-small); color: var(--ink-muted); }
.map-tip__rows dd { margin: 0 0 var(--space-2); font-family: var(--font-body); font-size: .875rem;
  line-height: var(--lh-body); color: var(--ink); }
/* Source links are real links, so they carry the underline per the
   site-wide affordance rule. The arrow is generated here rather than
   written into the markup, so every source link gets one automatically
   instead of only whichever anchor someone remembered to type it into.
   \FE0E forces text presentation: U+2197 is text-default today, but
   U+25B6 taught us not to trust that on iOS. */
/* .cite-link is the shared half of this: underline + the generated arrow,
   with NO colour of its own. Both citation surfaces opt in through it, so
   the arrow glyph and the underline behaviour cannot drift apart between
   the tooltip and the utility cards — which is exactly what a second
   copy-pasted copy of this rule would eventually do. Colour deliberately
   stays OUT of the shared part: these two sit on opposite surfaces, white
   card vs dark navy card, and the site's two-token green rule means they
   cannot share one value (--accent fails contrast on light, and
   --accent-on-light is a light-surface token). Each surface sets its own
   colour below / at .util-card__sources a. */
.map-tip__rows dd a { color: var(--accent-on-light); }
/* The underline lives on an inner <span>, NOT on the <a> — do not "simplify"
   this back onto the anchor. Text decoration propagates from an ancestor to
   its inline descendants and a descendant cannot cancel it: `text-decoration:
   none` on the ::after is silently ignored, and the arrow gets underlined
   along with the label. Verified by rendering, not by reading the computed
   style, which reports `none` on the pseudo either way and is simply wrong
   about what paints.
   That is also why the arrow was previously `display: inline-block` — an
   atomic inline-level box is the one thing an ancestor's underline does not
   paint through. But an atomic box next to text creates a LINE-BREAK
   opportunity, so the arrow could wrap onto the next line by itself, leaving
   a stranded "↗" opening a line (measured: the anchor split into two client
   rects, the second only 12px wide). Undecorating the anchor and underlining
   the span instead lets the arrow go back to `display: inline`, which has no
   break opportunity before it, so label and arrow stay on one line together
   while the LABEL still wraps normally when it must — unlike white-space:
   nowrap, which made the whole anchor unbreakable and overflowed the tooltip
   at 360px and the page at 330px.
   Same technique, same reason, as .util-card__toggle-label. */
.cite-link { text-decoration: none; }
.cite-link > span {
  text-decoration: underline; text-underline-offset: 3px; }
.cite-link::after { content: "\2197\FE0E"; margin-left: .2em;
  display: inline; }
/* Hover inverts on the span, since that is what carries the underline now —
   on the anchor it would target a decoration that is already none. */
.cite-link:hover > span { text-decoration: none; }
/* A citation with no Sources record yet. Deliberately NOT a disabled
   link: no underline, no pointer, so it reads as an unlinked reference
   rather than something broken. Same treatment as .ctx-sub__link-pending,
   which the unsupplied PUC docket URLs already use. */
.map-tip__cite-pending,
.m-cite-pending { font-family: var(--font-serif); font-style: italic;
  color: var(--ink-muted); }
/* Spacing for consecutive citations. Must cover BOTH variants:
   x-for inserts siblings with no whitespace text nodes, so two
   pending spans in a row have nothing between them at all. */
.map-tip__rows dd a,
.map-tip__cite-pending { margin-right: .35rem; }
/* A qualifier under a row's value — Amazon's confidence line, Hillsboro's
   aggregate breakdown, Amazon's utility note. Body text, not muted: these
   qualify the figure rather than annotate it. */
.map-tip__row-note { margin: .35rem 0 0; font-family: var(--font-body);
  font-size: .875rem; line-height: var(--lh-body); color: var(--ink); }

/* Map flow pill, mobile: an overlay on the canvas, same idea as the
   >=60rem version below, instead of sitting in normal flow after a
   full-height canvas (which put it below the fold on every phone — the
   section ran a full viewport PLUS the pill). Anchored from the CANVAS
   bottom via `top: var(--deck-h)` rather than `bottom`, because anchoring
   from the section bottom would sit it on top of the tooltip panel once
   an operator is selected; this way the pill's own height never has to be
   known or hardcoded. Guarded with an explicit max-width (rather than
   left unguarded like most mobile-first rules here) so `top` can never
   leak into the >=60rem block below, which sets `bottom` but not `top` —
   both set at once with height:auto would stretch the button vertically
   between them. z-index clears Mapbox's canvas and controls; no
   pointer-events override needed, since unlike .map__hud this element
   IS the interactive target, not a decorative wrapper around one. */
@media (max-width: 59.99rem) {
  /* width: max-content — see the desktop .map__pill rule below for why this
     is needed on both breakpoints, not just here. */
  .map__pill {
    position: absolute;
    left: 50%;
    top: var(--deck-h);
    transform: translateX(-50%) translateY(calc(-100% - var(--space-4)));
    margin: 0;
    width: max-content;
    z-index: 3;
  }
  /* CHANGE-2 follow-on: the canvas's selected-state height is no longer a
     fixed 45% (see .map.is-selected above) — it varies with the panel's
     actual content, so a hardcoded `top: 45%` anchor would drift off the
     real canvas/panel boundary as soon as a tooltip's content differs from
     whatever the 45% split assumed. .map.is-selected is a grid now; placing
     the pill in the CANVAS's row (grid-row: 1) and self-aligning it to that
     row's end edge pins it to the actual boundary the grid resolves to,
     whatever that is, with no percentage to keep in sync. That replaces the
     left/top/transform overlay technique for this state specifically — the
     margin-bottom is what leaves the same gap above the boundary that the
     old translateY(-100% - var(--space-4)) did. */
  .map.is-selected .map__pill {
    position: static;
    grid-row: 1;
    grid-column: 1;
    align-self: end;
    justify-self: center;
    margin: 0 0 var(--space-4);
    transform: none;
  }

  /* Stacking fix for the shadow's upward spill (see the long comment at
     .map__panel's box-shadow, further down): .map__canvas is `position:
     relative`, so per CSS2.1 painting order it paints in a later step than
     .map__panel's previous `position: static` — canvas on top regardless of
     DOM order, which ate the panel's own upward shadow. `position: relative`
     here moves the panel into that same "positioned" step, where DOM order
     (panel after canvas) puts it back on top, matching what desktop already
     does with its own absolute+z-index.
     z-index chosen from what's actually on the page, not reached for: canvas
     itself has none (auto — confirmed via computed style, nothing to clear
     numerically). .map__pill and .map__hud are both z-index: 3. The pill
     anchors to the canvas's bottom edge — the same line as the panel's top
     edge — so pill and panel can overlap right there, and the pill (the
     live nav control) has to stay on top of the panel (which the pill
     itself sits visually in front of, per the design). 2 clears the canvas
     while staying under both: incidentally the same number Mapbox's own
     controls use, not chosen to match them — those are clipped inside
     .map__canvas's own overflow:hidden box regardless of z-index, so they
     were never actually in competition with the panel. */
  .map__panel {
    position: relative;
    z-index: 2;
  }
}

/* Restoring position:absolute here, scoped to desktop only, is a deliberate
   reversal of the Aug 14 fix that removed it. That removal was needed because
   the panel could escape the map's box and cover the section below it. Pinned
   inside a 100svh map (.map gets its own position:relative just for this,
   scoped to this query — .map__canvas keeps its own separate one), the panel
   can never reach past the map's own bottom edge, so that bug cannot return.
   Do not "fix" this back to static/in-flow. */
@media (min-width: 60rem){
  .map{ position: relative; }
  .map.is-selected .map__canvas{ height: 100vh; height: var(--deck-h); }
  .map__panel{
    position: absolute;
    top: var(--space-4);
    right: var(--space-4);
    z-index: 4;
    width: min(24rem, 34vw);
    max-height: calc(var(--deck-h) - var(--space-4) * 2);
    overflow-y: auto;
  }
  /* Absolutely positioned over the full-bleed canvas, bottom centre. This
     was bottom: 5.5rem to clear .map__caption; the caption moved to the top
     of the canvas, so the offset is back to normal spacing.

     width: max-content — abs-pos + left:50% + width:auto blockifies the
     button and shrink-to-fits it against an "available width" of only
     (containing-block width − left offset), i.e. half the map's width,
     BEFORE the translateX(-50%) recentring runs (transforms are a paint-time
     step the width algorithm has no knowledge of). That silently caps the
     longest pill's text to half-width and wraps it, on any viewport wide
     enough for the map to be its containing block. width: max-content sizes
     the box to its actual content instead, matching how .map__hud handles
     the same left:50%+translateX(-50%) pattern elsewhere in this file. */
  .map__pill{
    position: absolute;
    left: 50%;
    bottom: var(--space-4);
    transform: translateX(-50%);
    margin: 0;
    width: max-content;
    z-index: 3;
  }
}

.util {
  min-height: 100vh;
  min-height: var(--deck-h);
  display: grid;
  grid-template-rows: 1fr auto;
  gap: var(--space-4);
  background: var(--panel-blue); color: #f4f5f6; padding: var(--space-6) 1.25rem;
  /* Deck content column. The Figma card is 640px in a 1440 frame = 40rem,
     matching --content-w on the content pages, not --maxw (46rem) which
     everything here previously used. Scoped to .util for now; .ctx and .act
     should adopt the same variable once their screens are reviewed. */
  --deck-w: 40rem;
}
.util__body {
  display: grid;
  align-content: center;
  align-content: safe center;
}
.util__heading, .ctx__heading, .act__heading { font-family: var(--font-serif);
  font-size: clamp(2rem, 5vw, 3.5rem); max-width: var(--maxw); margin: 0 auto var(--space-4); }
/* Utility-only override. It is an <h2>, so it was rendering bold against a
   Figma that shows regular weight at display size. */
.util__heading { font-weight: 400; font-size: clamp(3rem, 6vw, 3.75rem);
  line-height: var(--lh-tight); width: 100%; max-width: var(--deck-w); margin-bottom: var(--space-6); }

.util-card { width: 100%; max-width: var(--deck-w); margin: 0 auto var(--space-3); scroll-margin-top: var(--space-4);
  border: 1px solid rgba(255,255,255,.18); border-radius: var(--radius); padding: var(--space-4); }
.util-card__territory { font-family: var(--font-serif); font-size: 1.25rem;
  line-height: var(--lh-tight); font-weight: 400; margin: 0; }
.util-card__head { margin: 0; }
/* font-weight is explicit because the button sits inside an <h3> and would
   otherwise inherit bold. The underline moved to the inner label span, since
   text-decoration on the button also underlines the caret glyph and a
   descendant cannot remove an ancestor's underline. */
.util-card__toggle { font-family: var(--font-body); font-size: var(--fs-small);
  font-weight: 500; text-transform: uppercase; letter-spacing: .06em;
  text-decoration: none; padding: .5rem 0 0; }
.util-card__toggle-label { text-decoration: underline; text-underline-offset: 3px; }
/* Scales the shared .caret via its em sizing; spacing comes from .caret. */
.util-card__caret { font-size: .85em; }

/* The three toggles with text-transform: uppercase want the caret a shade
   higher. An inline-block .5em box centres about .25em above the baseline,
   level with lowercase x-height — but capitals centre nearer .35em, so
   against an all-caps label the caret reads low. margin-bottom lifts the box
   off the baseline without touching `transform`, which the open state owns. */
.util-card__caret,
.map-tip__toggle .caret { margin-bottom: .1em; }

.util-card__body { border-top: 1px solid rgba(255,255,255,.18);
  margin-top: var(--space-2); padding-top: var(--space-3); }
.util-card__lead { border-left: 2px solid rgba(255,255,255,.5); margin: 0 0 var(--space-3);
  padding-left: var(--space-3); font-family: var(--font-serif); font-size: 1.25rem;
  line-height: 1.45; }
/* This had NO rule at all, so the main story paragraph was rendering in the
   body font while the Figma has it in the serif. */
.util-card__story { font-family: var(--font-serif); font-size: var(--fs-body);
  line-height: var(--lh-body); }
.util-card__story p { margin: 0 0 var(--space-2); }
.util-card__equity { font-family: var(--font-serif); font-style: italic;
  font-size: var(--fs-small); line-height: var(--lh-body);
  color: rgba(244,245,246,.7); margin: var(--space-3) 0 0; }
.util-card__calc-toggle { font-family: var(--font-body); font-size: var(--fs-small);
  font-weight: 500; text-transform: uppercase; letter-spacing: .06em;
  text-decoration: none; padding: var(--space-3) 0 0; }

.util-card__detail { border-top: 1px solid rgba(255,255,255,.18);
  margin-top: var(--space-2); padding-top: var(--space-3); }
.util-calc__row { display: flex; gap: var(--space-3); padding: .35rem 0; }
.util-calc__label { flex: 0 0 11rem; font-family: var(--font-body); font-size: var(--fs-small);
  color: rgba(244,245,246,.7); }
.util-calc__value { font-family: var(--font-body); font-size: var(--fs-small);
  line-height: var(--lh-body); }
.util-calc__value a, .util-card__sources a { color: inherit; text-underline-offset: 3px; }
.util-calc__note { font-family: var(--font-body); font-size: var(--fs-small);
  color: rgba(244,245,246,.7); margin: var(--space-2) 0 0; }
.util-card__sources { font-family: var(--font-body); font-size: var(--fs-small);
  color: rgba(244,245,246,.7); margin: var(--space-3) 0 0; }
/* Citation list, one .util-card__cite per linked Sources record. The
   separator is a real element rather than a text node or a ::before,
   because x-for clones its template per item and inserts the roots with
   NO whitespace text nodes between them — the same thing that silently
   ran the tooltip's pending spans together (see .map-tip__cite-pending's
   margin-right note). Margins on the separator itself mean the gap never
   depends on collapsed whitespace surviving the clone.
   The anchors take their colour from the .util-card__sources a rule
   further up (color: inherit), matching .util__fallback-link and
   .ctx-link — every link inside the utility section inherits the card's
   own ink rather than using the light-surface green, which would fail
   contrast on this navy panel. */
.util-card__cite { display: inline; }
.util-card__cite-sep { margin: 0 .4em; }
/* Mobile stacks label above value per the Figma; the two-column split needs
   more width than a phone has. 767px matches the file's existing breakpoint. */
@media (max-width: 767px){
  .util-calc__row { display: block; }
  .util-calc__label { display: block; margin-bottom: .15rem; }
}

/* CHANGE-2: serif italic on all three lines, matching the map section's
   hint/caption stack (.map__caption — same var(--font-serif) token, not a
   hardcoded family name). Set once here at the parent rather than on each
   child, since none of the three lines override font-family/font-style of
   their own — the label's own font-style: italic below is gone now that
   the parent covers it. */
.util__fallback { width: 100%; max-width: var(--deck-w); margin: var(--space-6) auto 0; text-align: center;
  font-family: var(--font-serif); font-style: italic;
  font-size: var(--fs-small); line-height: var(--lh-body); }
/* "Not sure?" is a label for the sentence beneath it, not a link or a
   toggle — it navigates nowhere and expands nothing, so per the site's
   pill-means-navigate / underline-means-links-or-expands rule it gets
   neither, just italic (now inherited from .util__fallback above; see
   CHANGE-2). Sentence case is already what's in the markup; this only
   drops the uppercase transform that was overriding it. */
.util__fallback-label { display: block; margin-bottom: .5rem; }
.util__fallback-link { display: block; margin-top: .15rem; color: inherit;
  text-underline-offset: 3px; }

.ctx {
  position: relative;
  overflow: hidden;
  min-height: 100vh;
  min-height: var(--deck-h);
  display: grid;
  grid-template-rows: 1fr auto;
  gap: var(--space-4);
  /* Only visible for the instant before the poster paints. Was #4bbd7e,
     which flashed bright green against a now blue-toned video. */
  background: #1b4a68; color: var(--ink); padding: var(--space-6) 1.25rem;
  /* Same 40rem column as the utility section. Duplicated rather than
     promoted to a token because .act has not been reviewed yet; once it
     has, all three should move to one --deck-w in tokens.css. */
  --deck-w: 40rem;
}
.ctx__body {
  display: grid;
  align-content: center;
  align-content: safe center;
}

/* Video background. Sits behind .ctx__body, which already has its own
   stacking context. The section grows past 100svh when panels expand, so
   this is pinned to the section box and the video covers it at whatever
   height it ends up. */
.ctx__bg { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
.ctx__video { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center; }

/* 70deg carries green (120) past cyan into the blue range rather than
   stopping at teal. brightness lifts it out of the murk; saturate is back
   above 1 because the hue is now one worth intensifying. TUNE hue-rotate
   FIRST, it does the most work — 55deg is more teal, 85deg more indigo. */
.ctx__video { filter: hue-rotate(70deg) saturate(1.15) brightness(1.12) contrast(1.02); }

.ctx__wash { position: absolute; inset: 0;
  background: rgba(38,110,158,.22); }
.ctx__bg::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(rgba(10,30,48,.22), rgba(10,30,48,.42)); }

/* .ctx__body and the flow pill must clear the video. The pill is a SIBLING
   of .ctx__body in the section's second grid row, not a child of it — same
   structure as the splash, where it needed its own rule. */
.ctx__body { position: relative; z-index: 1; }
.ctx > .pill { position: relative; z-index: 1; }

/* Reduced motion: the JS pauses playback, this makes the still the intended
   state rather than a frozen accident. */
@media (prefers-reduced-motion: reduce) {
  .ctx__video { display: none; }
  .ctx__bg { background-image: url("../img/ctx-salmon-poster.jpg");
    background-size: cover; background-position: center; }
}
/* CONTRAST NOTE, deliberate and accepted: this heading sits directly on
   the video with no plate behind it and does NOT meet WCAG AA. Measured
   2.23:1 worst case across 9 samples (3 points x 3 playback moments).
   The compliant version — a translucent backdrop plate matching the
   panels — was built, measured at 8.16:1, and REJECTED on 2026-08-18:
   it read as a lozenge, and width: max-content overflowed 390px viewports
   by 103px, clipped invisibly by .ctx's overflow: hidden. Mel accepted the
   failure on the grounds that this is a decorative section heading whose
   text is also the section's accessible name via aria-labelledby, so it is
   available to assistive tech regardless of visual contrast. Do not
   "fix" this back to a plate without re-reading that history.
   The body copy inside the panels measures 8.4-8.8:1 and is unaffected.
   Selector note: written as `.ctx > .ctx__heading` in the original spec,
   but the heading is nested inside .ctx__body, not a direct child of
   .ctx — that selector never matches. Using `.ctx__body > .ctx__heading`
   instead, same two-class specificity, actually matches the DOM. */
.ctx__body > .ctx__heading { font-family: var(--font-serif); font-weight: 400;
  font-size: clamp(3rem, 6vw, 3.75rem); line-height: var(--lh-tight);
  color: #EAE8E1;
  text-shadow: 0 1px 24px rgba(6,18,26,.55), 0 1px 3px rgba(6,18,26,.4);
  width: 100%; max-width: var(--deck-w);
  margin: 0 auto var(--space-4);
  padding: 0; background: none;
  backdrop-filter: none; -webkit-backdrop-filter: none; }

/* width: 100% alongside max-width because margin: 0 auto on a grid item
   suppresses the stretch and makes the box shrink-to-fit. Same fix as the
   utility cards. */
/* CHANGE-1 (revised from the c01b10c pass): the near-black translucent panel
   clashed with the solid black pill sitting under it on the teal video —
   the COLOUR was the right fix, but going opaque was a mistake, not a
   necessary consequence of it. Panels are meant to stay semi-transparent
   over the fish-ladder video. Alpha (.62), blur (14px) and the @supports
   fallback are recovered unchanged from the pre-c01b10c rule — only the
   base colour changed, from the old near-black rgb(10,14,17) to the same
   blue as .util, rgb(26,37,48) / var(--panel-blue). */
.ctx-panel { width: 100%; max-width: var(--deck-w); margin: 0 auto var(--space-3);
  background: rgba(26,37,48,.62);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: #EAE8E1; border-radius: var(--radius);
  padding: var(--space-3) var(--space-4); }

/* Fallback: without backdrop-filter the same alpha is not enough to hold
   the copy, so go opaque instead. */
@supports not (backdrop-filter: blur(8px)) {
  .ctx-panel { background: var(--panel-blue); }
}
.ctx-panel__head { margin: 0; font-family: var(--font-serif);
  font-size: var(--fs-h2); font-weight: 400; line-height: var(--lh-tight); }
/* font-size: inherit is load-bearing. The shared toggle rule above sets
   font-family and color to inherit but not font-size, and a <button> does
   not inherit font-size by default — without this the panel title renders
   at the UA default ~13.3px inside a 24px <h3>. */
.ctx-panel__toggle { font-size: inherit; line-height: inherit; padding: .25rem 0; }
/* .7em against the panel title's --fs-h2, so the caret stays subordinate to
   a large serif heading rather than matching its cap height. */
.ctx-panel__caret { font-size: .7em; }
.ctx-panel__body { border-top: 1px solid rgba(255,255,255,.2);
  margin-top: var(--space-2); padding-top: var(--space-3); }
.ctx-panel__copy { font-family: var(--font-serif); font-size: var(--fs-body);
  line-height: var(--lh-body); }
.ctx-panel__copy p { margin: 0 0 var(--space-2); }
.ctx-panel__copy p:last-child { margin-bottom: 0; }

/* renderInline() wraps CTX_LINKS phrase matches in .ctx-link, but NO rule
   has ever targeted it — they were falling through to the global
   a { color: var(--accent-on-light) }, which is the light-surface green on
   a dark panel, the exact pairing the two-token rule forbids. The Figma
   shows them as body-coloured with a dotted underline, which also reads as
   annotation rather than navigation. */
.ctx-link { color: inherit; text-decoration: underline;
  text-decoration-style: dotted; text-underline-offset: 4px;
  text-decoration-thickness: 1px; }
.ctx-link:hover { text-decoration-style: solid; }

.ctx-sub { display: grid; gap: var(--space-2); margin-top: var(--space-3); }
.ctx-sub__card { border: 1px solid rgba(255,255,255,.2); border-radius: var(--radius);
  padding: var(--space-3); }
.ctx-sub__label { display: flex; align-items: center; gap: .5rem; margin: 0 0 var(--space-2);
  font-family: var(--font-body); font-size: var(--fs-small); }
.ctx-sub__dot { width: .5rem; height: .5rem; border-radius: 50%; display: inline-block;
  flex: none; }
.ctx-sub__dot--pge { background: #4ed28c; }
.ctx-sub__dot--pp { background: #e79a3c; }
/* Sub-cards are a deliberately different register from the serif narrative
   above them: body font, smaller, so they read as per-utility detail rather
   than continuous prose. */
.ctx-sub__copy { font-family: var(--font-body); font-size: var(--fs-small);
  line-height: var(--lh-body); color: rgba(234,232,225,.85); margin: 0; }
.ctx-sub__link { margin: var(--space-2) 0 0; font-family: var(--font-body);
  font-size: var(--fs-small); }
/* The anchor opts into .cite-link for the underline-on-inner-span and the
   generated text-presentation arrow, so this contributes only the colour that
   .cite-link deliberately withholds — here `inherit`, which picks up the
   sub-card's light-on-navy text. Third surface to share that rule, after the
   tooltip rows and the utility-card sources. */
.ctx-sub__link a { color: inherit; }
/* Pending state: a Sub Link URL field that is empty renders as a plain span
   rather than an anchor. Styled to read as a label, NOT as a broken or
   disabled link — no underline, no pointer, muted. Both URLs are populated
   today, so this is the fallback path, not the usual one. */
.ctx-sub__link-pending { color: rgba(234,232,225,.5); font-style: italic; }

.act {
  min-height: 100vh;
  min-height: var(--deck-h);
  display: grid;
  /* The other three narrative sections use `grid-template-rows: 1fr auto`
     to pin a flow pill in row 2. This section is the last one before the
     footer and deliberately has NO pill, so it keeps a single implicit
     track per child. Do not "make it consistent" by adding a fixed
     template — the rows here are content-sized on purpose (heading, cards,
     video band) and a `1fr` would stretch one of them.

     NO LONGER VERTICALLY CENTRED. It used to `align-content: safe center`
     on the assumption the deck always fit one viewport; with five cards
     and the video band it never does, so `safe center` was silently
     degrading to flush-top and the heading sat at bare padding-top (48px)
     while .util sat at 110 and .ctx at 152. Now explicitly top-anchored
     with a padding-top in that same range, and content running below the
     fold is expected rather than something to design away. */
  align-content: start;
  /* Same navy as .util — see --panel-blue in tokens.css. --panel-dark is a
     green-black and reads as a third green section here; this reuses the
     existing value rather than inventing a fourth dark, and matches the
     blue cast of the night-sky video band at the foot of the section. */
  background: var(--panel-blue); color: #f4f5f6;
  /* Kept as a variable so the full-bleed video band can cancel exactly this
     inset without the two values drifting apart. */
  --act-pad-x: 1.25rem;
  /* Tracks viewport height so tall screens breathe more, bounded so short
     ones do not lose the heading. Lands ~112px at 800px tall, between
     .util (110) and .ctx (152). */
  padding: clamp(var(--space-6), 14vh, 8rem) var(--act-pad-x) 0;
  --deck-w: 40rem;
}

/* Selector deliberately matches `.section > h2` specificity — that generic
   rule sets font-family, font-size and color, and this heading IS a direct
   child of the section (unlike .util__heading and .ctx__heading, which sit
   inside a wrapper div and never match it). Do not simplify this back to a
   bare .act__heading; it silently stops applying. */
.act > .act__heading { font-family: var(--font-serif); font-weight: 400;
  font-size: clamp(3rem, 6vw, 3.75rem); line-height: var(--lh-tight);
  color: #f4f5f6; width: 100%; max-width: var(--deck-w);
  margin: 0 auto var(--space-4); }

/* width: 100% alongside max-width: margin: 0 auto on a grid item suppresses
   the stretch and makes the box shrink-to-fit. Third time this has bitten,
   after .util-card and .ctx-panel. */
.act__cards { width: 100%; max-width: var(--deck-w); margin: 0 auto; }

/* ---- Night-sky band -------------------------------------------------
   Full-bleed: the section pads its content by --act-pad-x, and this cancels
   exactly that inset off both sides rather than hardcoding 1.25rem twice —
   change the padding and the band follows.

   Height is the mock's bottom-third proportion of the panel, floored so it
   does not collapse on short viewports. The section's padding-bottom is 0,
   so the band sits flush to the bottom edge with no navy strip beneath it.

   The margin-top is what lets the cards "clear" the band on desktop while
   still allowing the slight overlap the mock permits at the card
   container's bottom edge. */
.act__band {
  width: calc(100% + var(--act-pad-x) * 2);
  margin-inline: calc(var(--act-pad-x) * -1);
  margin-top: var(--space-6);
  /* The fade needs vertical room to BE a fade: at the mock's ~third-of-panel
     height the ramp had to compress into the top of the band and read as an
     edge. Taller band, longer dissolve, and more of the star field survives
     the mask. */
  height: clamp(13rem, 40vh, 24rem);
  /* Establishes the box the video covers; without it the video's own
     intrinsic 16:9 would drive the height instead. */
  overflow: hidden;
}

/* THE BLEND: the mask is on the video itself, not a gradient overlay sitting
   on top of it. An overlay would have to be painted in --panel-blue and
   colour-matched to the section, and any drift between the two shows as a
   seam; masking makes the frame's own top genuinely transparent so the
   section background IS the fade target and there is nothing to match.

   The stop is deliberately late and the ramp long — the fade should read as
   the sky emerging out of the navy, not as a wipe. Both the standard and
   -webkit- properties are set: Safari still needs the prefix. */
.act__video {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  /* Bottom-anchored: the Mt Hood silhouette and the Milky Way core sit in
     the lower half of the frame, and a wide short band cropped from the
     centre would show only empty mid-sky. */
  object-position: center bottom;
  /* The fade line is a shallow WAVE, not a straight edge: two wide, offset
     ellipses whose alpha adds (the default mask-composite for multiple
     layers), so the boundary rises left of centre and settles to the right.
     Both are far wider than they are tall and centred below the box, which
     keeps the ramp long — the curve changes WHERE the dissolve happens, not
     how abrupt it is.

     Deliberately not an SVG wave path: a filled path has a crisp top edge,
     which puts back exactly the visible seam this mask exists to avoid.

     The stops stay early (30% / 73%) so the ramp finishes INSIDE the band:
     pushing them out to reveal more sky moved the whole dissolve above the
     box, and the band then met the navy as a hard cut with no gradient left
     to see. More sky is bought with BAND HEIGHT instead — a taller box shows
     a taller slice of the frame and leaves the fade proportions alone.

     The linear layer is a floor, not part of the shape — percentages resize
     with the box, so at extreme aspect ratios (very wide desktop, 390px
     phone) it guarantees the bottom of the band stays fully opaque and no
     navy shows through beneath the mountain. */
  --sky-fade:
    radial-gradient(ellipse 70% 145% at 24% 118%, #000 30%, transparent 73%),
    radial-gradient(ellipse 74% 145% at 75% 130%, #000 30%, transparent 73%),
    linear-gradient(to bottom, transparent 82%, #000 100%);
  mask-image: var(--sky-fade);
  -webkit-mask-image: var(--sky-fade);
}

.act-card--featured { border: 1px solid rgba(255,255,255,.2); border-radius: var(--radius);
  background: rgba(255,255,255,.04);
  padding: var(--space-4); margin-bottom: var(--space-4); }
.act-card--compact { border-top: 1px solid rgba(255,255,255,.15);
  padding: var(--space-4) 0 var(--space-3); }
/* The featured card always renders first, so the first COMPACT card is
   never :first-child. Adjacent-sibling instead. The featured card already
   carries margin-bottom, so the rule that follows it needs no top rule. */
.act-card--featured + .act-card--compact { border-top: 0; padding-top: 0; }

/* Desktop puts the compact card's link on the title's baseline, right
   aligned. Mobile drops it below the body copy — handled by the source
   order plus the media query at the bottom of this block. */
.act-card__head { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: baseline;
  justify-content: space-between; margin-bottom: var(--space-2); }
/* Explicit weight because it is an <h3>. Featured is the larger tier; the
   compact cards step down, which is what separates the one action being
   asked for from the four supporting ones. */
.act-card__title { font-family: var(--font-serif); font-weight: 400;
  line-height: var(--lh-tight); margin: 0; }
.act-card--featured .act-card__title { font-size: var(--fs-h2); }
.act-card--compact .act-card__title { font-size: 1.25rem; }

.act-card__status { display: flex; align-items: center; gap: .5rem; margin: 0;
  font-family: var(--font-body); font-size: var(--fs-small);
  color: rgba(244,245,246,.85); }
.act-card__status-dot { width: .5rem; height: .5rem; border-radius: 50%;
  background: #4ed28c; flex: none; }

/* This had NO rule at all, so every card's body copy was rendering in the
   body font while the Figma has it in the serif. Same miss as
   .util-card__story. */
.act-card__body { font-family: var(--font-serif); font-size: var(--fs-body);
  line-height: var(--lh-body); }
.act-card__body p { margin: 0 0 var(--space-2); }
.act-card__body p:last-child { margin-bottom: 0; }

.act-card__note { font-family: var(--font-serif); font-style: italic;
  font-size: var(--fs-small); line-height: var(--lh-body);
  color: rgba(244,245,246,.7); margin: var(--space-2) 0 0; }

/* Same visual object as .pill--light, but it is a real CTA inside a card
   rather than a flow pill, so it keeps its own class and the card radius
   instead of the pill's 999px. */
.act-card__cta { display: inline-block; margin-top: var(--space-3); padding: .625rem 1.25rem;
  background: #d9d9d4; color: #12211a; text-decoration: none; border-radius: var(--radius);
  font-family: var(--font-body); font-size: var(--fs-small); }
.act-card__cta:hover { background: #eceae5; }

/* --accent is correct here: this is a dark surface. The Figma underlines
   these; the previous rule had no underline at all. */
.act-card__link { color: var(--accent); font-family: var(--font-body);
  font-size: var(--fs-small); text-decoration: underline;
  text-underline-offset: 3px; white-space: nowrap; }

@media (max-width: 767px) {
  .act-card__head { flex-direction: column; align-items: flex-start; gap: .5rem; }

  /* Mobile puts the compact card's link AFTER the body copy. The previous
     rule only ordered the link within .act-card__head, so it landed under the
     TITLE — the body is a sibling of __head, not inside it, and `order` can
     only sort items that share a flex container.
     display:contents on __head removes its box from the layout so the title
     and the link become flex items of .act-card itself, alongside the body;
     the three can then be ordered against each other. The DOM is untouched,
     so reading order and the desktop rule are both unaffected — none of this
     exists above 767px, where the link stays right-aligned on the title
     baseline in __head's own flex row.
     __head's box going away also takes its margin-bottom and gap with it,
     so the spacing they provided is restored on the items directly. */
  .act-card--compact { display: flex; flex-direction: column; }
  .act-card--compact .act-card__head { display: contents; }
  .act-card--compact .act-card__title { order: 1; margin-bottom: var(--space-2); }
  .act-card--compact .act-card__body { order: 2; }
  .act-card--compact .act-card__link {
    order: 3; align-self: flex-start; margin-top: var(--space-2); }
}

@media (min-width: 48rem) {
  .ctx-sub { grid-template-columns: 1fr 1fr; }
}
/* ===== END SCAFFOLD ONLY ===== */

/* ============================================================
   SCROLL SNAP — ON TRIAL. Mel wants to see how this feels; kept
   isolated on purpose so it's a one-block delete if it doesn't work
   out (plus the .snap-suspended add/remove calls in the utility
   toggle() and map toBill() methods in index.html, which can stay —
   they're harmless no-ops once this block and the class they toggle
   are gone).

   scroll-snap-type lives on <html>, confirmed via
   document.scrollingElement in the browser (see the verification
   report) — NOT assumed. PROXIMITY, not mandatory: mandatory would
   trap the reader inside the context section once its panels are
   expanded past one viewport tall.
   ============================================================ */
html { scroll-snap-type: y proximity; }
/* CHANGE-3: pill clicks jump instantly without this — same <html> scroller
   as scroll-snap-type above, confirmed the same way (document.scrollingElement).
   Gated on no-preference rather than applied unconditionally, and rather than
   guarded with the file's other `@media (prefers-reduced-motion: reduce)`
   block (which zeroes animations/transitions): scroll-behavior has no
   "reduce" value to fall back to, so it has to be opted into for motion-ok
   users specifically, not opted out of for reduced-motion ones. Every
   scrollIntoView() call site already passes behavior: 'auto' for a
   reduced-motion user independently of this, so this rule only governs
   smoothing what those calls don't already control (and any future
   scrollIntoView() with no explicit behavior). */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}
html.snap-suspended { scroll-snap-type: none; }

/* .act is deliberately NOT in this list. A snap point at the start of a
   section that is TALLER than the viewport fights the reader on the way
   past it: proximity snapping keeps pulling back to the section top, and
   the action deck plus the video band plus the footer all sit below that
   point, so the bottom of the page becomes a fight to reach. The other four
   sections are viewport-height, where a start-aligned snap is doing what it
   was added to do. */
.splash,
.map,
.util,
.ctx {
  scroll-snap-align: start;
}
