/* ============================================================
   scrollytelling.css — scroll-driven section layout
   ============================================================ */

/* The chapter wraps a sticky chart + scrolling steps. */
.chapter {
  position: relative;
  padding-block-start: var(--space-9);
  padding-block-end: var(--space-12);
}
.chapter + .chapter {
  border-block-start: 1px solid var(--rule);
}

/* Two-column scroller: text (left, 1fr) + sticky chart (right, 1.4fr) */
.scroller {
  display: grid;
  grid-template-columns: 1fr 1.4fr;
  gap: clamp(var(--space-6), 4vw, var(--space-9));
  align-items: start;
  max-inline-size: var(--w-wide);
  margin-inline: auto;
  padding-inline: var(--space-5);
}

/* Text column */
.scroller__text {
  display: flex; flex-direction: column; gap: 80dvh;  /* one step per viewport */
  /* [scroll-fix §3] padding-block-end was 20dvh — too little. scrollama fires at offset:0.55, so the
     LAST step's top must reach 55% of the viewport to activate, needing ~45dvh of trailing scroll.
     With only 20dvh the sticky unpinned first and the final step never fired (Services in chart 3, the
     heatmap's peak box). 52dvh gives every chapter's last step room to trigger AND dwell. */
  padding-block: 20dvh 52dvh;
}
.scroller__step {
  max-inline-size: 38ch;
  padding: var(--space-6) var(--space-6);
  border-radius: var(--radius-3);
  background: transparent;
  /* [R2 · 1e] Inactive steps stay FULL-CONTRAST (was opacity:0.3, which failed WCAG AA on
     ~173 nodes). Focus is carried by the active step's elevated card below, not by dimming
     upcoming steps into illegibility. On mobile these are invisible spacers (see responsive). */
  opacity: 1;
  transform: translateY(6px);
  transition:
    opacity var(--dur-4) var(--ease-out),
    transform var(--dur-4) var(--ease-out),
    background-color var(--dur-4) var(--ease-out);
}
.scroller__step.is-active {
  opacity: 1;
  transform: translateY(0);
  background: var(--bg-elev);
  box-shadow: var(--shadow-2);
}
.scroller__step h3,
.scroller__step h4 {
  font-family: var(--font-display);
  font-size: var(--fs-500);
  font-weight: 600;
  margin-block-end: var(--space-3);
  letter-spacing: -0.01em;
  line-height: var(--lh-snug);
}
.scroller__step .step-eyebrow {
  font-size: var(--fs-100);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent-text);   /* [R2·1e] AA-safe deep terracotta */
  margin-block-end: var(--space-2);
}
.scroller__step p {
  font-size: var(--fs-300);
  line-height: var(--lh-relaxed);
  color: var(--ink-soft);
}
.scroller__step strong { color: var(--ink); }

/* Sticky chart panel — Pudding/Reuters: no frame, just whitespace */
.scroller__chart {
  position: sticky;
  inset-block-start: var(--space-3);
  block-size: calc(100dvh - var(--space-4));
  display: grid;
  grid-template-rows: auto 1fr auto;
  /* [owner review A3 — TOP PRIORITY] kill the gaps: tight header→chart→source rhythm so the
     chart gets the maximum vertical space. (was gap space-3 + padding space-5.) */
  gap: var(--space-1);
  padding: var(--space-2) 0;
  background: transparent;
  border: 0;
  box-shadow: none;
  overflow: visible;
}
.scroller__chart header {
  display: flex; flex-direction: column; gap: 2px;
  max-inline-size: 60ch;
}
/* [owner review A1] chart titles: small, short, upright — a modest editorial headline, not a hero number. */
.scroller__chart h3 {
  font-family: var(--font-display);
  font-size: var(--fs-400);
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: -0.01em;
  color: var(--ink-strong);
  margin: 0;
}
/* [owner review A2] subtitles: short + small, one line of method/unit. */

.scroller__chart .chart-body {
  min-block-size: 0;
  position: relative;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;   /* safety net — the mask on the choropleth SVG fades content well before this clip */
}
/* Choropleth: dissolve the rectangular frame so the map melts into the page.

   Geometry: the mask spans the SVG plus 160 px bleed on each side and 18 px top/bottom.
   In mask coordinates [0..100%] the SVG itself occupies the inner 160/(W+320) ≈ 18.7%
   to 81.3% horizontally and 18/(H+36) ≈ 4.5% to 95.5% vertically. So the gradient
   STOPS at those percentages keep the entire SVG (i.e. chart-body) FULLY OPAQUE —
   the map inside is never faded — while the bleed area outside chart-body smoothly
   fades from opaque at the SVG edge to fully transparent at the mask edge.

   The bleed area is where non-EU countries (and EU edges, after the projection zoom)
   render at progressively lower opacity, ghosting into the cream. There's no straight
   frame line anywhere because both the SVG edge transition AND the cream-on-page edge
   are continuous gradients, not hard cuts.

   `mask-clip: no-clip` is required so the gradient applies to overflow paint past the
   SVG's border-box. `chart-body { overflow: visible }` plus a horizontally-extended
   clip-path lets the bleed sideways; the clip-path's tight top/bottom (≈ 18 px) keeps
   the bleed from ever touching the header above or the timeline below. */
/* [CH1 layout · review] The MAP is the point — give it more of the panel's vertical budget.
   Review found it cramped behind chrome: the panel's space-5 padding (top/bottom dead space), a
   loose ~138px title/subtitle/hint header (big wrap-gaps), and the timeline scrubber. Trim them
   HERE (the other 11 charts keep their space-5 breathing room). The map is the 1fr grid row, so
   every reclaimed px grows it, and Choropleth.render re-fits the SVG after the controls settle so
   a taller row means a taller map, not a letterbox. Net: desktop map ~290px-effective(letterboxed)
   → 432px CLEAN. (A residual ~21px sits in the source's auto track — its min-content reserves ~2
   lines though it renders one; left as a small bottom margin rather than risk the map's clean fill.) */
.scroller__chart[data-chart-mount="choropleth"] {
  padding-block: 0;
  gap: 2px;
}
.scroller__chart[data-chart-mount="choropleth"] .chart-source { line-height: 1.15; margin: 0; }
.scroller__chart[data-chart-mount="choropleth"] .choro-header {
  gap: 1px;
  padding-block-end: 0;
}
.scroller__chart[data-chart-mount="choropleth"] .choro-header__top {
  row-gap: var(--space-1);     /* tighten the wrapped title / subtitle / hint lines (keep the horizontal gap) */
}
.scroller__chart[data-chart-mount="choropleth"] .map-timeline {
  block-size: 50px;            /* proper-height scrubber; year labels now sit BELOW as crisp HTML (owner review D1) */
}
/* Let the map SVG follow its grid row's height instead of hard-capping at its viewBox aspect.
   The shared `max-block-size:100%` made the SVG settle short — most visibly on MOBILE, where the
   map collapsed to a sliver. block-size:100% + min-block-size:0 let it fill its row at every
   viewport (mobile map ~54→99px), and Choropleth.render keeps the viewBox matched to the rendered
   height so there is no letterbox. */
.scroller__chart[data-chart-mount="choropleth"] .chart-body > svg.chart-svg {
  block-size: 100%;
  min-block-size: 0;
  max-block-size: none;
}
.scroller__chart[data-chart-mount="choropleth"] .chart-body {
  overflow: visible;
  clip-path: inset(-18px -160px -18px -160px);
  -webkit-clip-path: inset(-18px -160px -18px -160px);
}
.scroller__chart[data-chart-mount="choropleth"] .chart-body > svg.chart-svg {
  overflow: visible;
}
/* [debug 2026-07-06] Moved the vignette mask from the SVG ROOT to `.map-layer` (the country-shapes
   group, `this.gMap`) specifically. The comment below always said "text/annotations sit on a layer
   ABOVE, unaffected" — but a CSS mask on the SVG root fades EVERYTHING painted inside it, including
   every sibling drawn later (the year-kicker text in the top-left corner, farthest from the radial
   centre, took the worst of it — objectively low contrast, not just "quieter styling"). A mask
   cannot be undone by a descendant's own CSS once applied by an ancestor, so the fix is to scope it
   to the one layer it was actually meant for. `.year-kicker-g`/`.map-label-g`/etc. are siblings of
   `.map-layer` (not descendants), so they're now completely outside the masked subtree.
   VIGNETTE — the fade happens INSIDE chart-body's edges so the rectangular
     frame is dissolved into a smooth gradient with no abrupt opacity transition
     anywhere. At chart-body's edges the mask is partially transparent so
     country fills blend gradually into the cream, then continue at ever-lower
     opacity into the bleed area past chart-body.
     Horizontal opaque core: 30 %–70 % of mask (≈ inner 350 px of chart-body).
     Vertical opaque core: 22 %–78 % of mask (≈ inner 64 % of chart-body, more
     generous because EU fills the panel vertically).
   [owner review D1 stage 3] RADIAL opacity falloff — the map reads as a full-bleed background
     layer: 100% opaque across the central focus region, then dissolving smoothly OUTWARD into the
     paper as distance from the focus grows (a soft radial mask, no hard rectangular frame anywhere).
     Replaces the old linear edge-vignette. */
.scroller__chart[data-chart-mount="choropleth"] .chart-body > svg.chart-svg .map-layer {
  -webkit-mask-clip: no-clip;
  mask-clip: no-clip;
  -webkit-mask-origin: view-box;
  mask-origin: view-box;
  -webkit-mask-image: radial-gradient(ellipse 66% 74% at 50% 47%,
      #000 0%, #000 46%, rgba(0,0,0,0.62) 68%, rgba(0,0,0,0.18) 86%, transparent 100%);
  mask-image: radial-gradient(ellipse 66% 74% at 50% 47%,
      #000 0%, #000 46%, rgba(0,0,0,0.62) 68%, rgba(0,0,0,0.18) 86%, transparent 100%);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
/* [D75 · owner bug report + screen recording] The REAL "white vertical and horizontal lines" during a
   zoom were NOT the (claret, 0.05-opacity) graticule the earlier `.is-camming` pass hid — they were GPU
   COMPOSITOR TILE SEAMS. `.map-layer` (this masked `<g>`) is the very group d3 scales/pans on a camera
   flight, and it carries BOTH this `mask-image` vignette (with `mask-origin: view-box` — i.e. the mask
   is FIXED in the panel while the content transforms UNDER it) AND `will-change: transform`. Because
   the mask can't ride along with the transform, Chrome cannot pre-rasterise (content+mask) and move it
   as one texture — it must re-composite the fixed mask over freshly-transformed content every frame;
   `will-change` keeps that large layer permanently promoted and TILED; and the tile boundaries leak the
   paper background as a white horizontal/vertical grid. It's a live-compositor artifact, invisible to
   paint-based headless screenshots — which is exactly why the first `.is-camming` fix (verified only in
   Playwright) never caught it. Fix: drop the mask ONLY while the camera is flying. The map is moving
   fast then, so the momentary loss of the soft edge is imperceptible; the instant it settles,
   `.is-camming` clears and the full vignette snaps back. The RESTING map (every screenshot / sentinel
   state — essentially all viewing time) is byte-for-byte unchanged. Selector MUST match this block's own
   chain + `.is-camming` on the svg, or it loses the specificity contest to the rule above and no-ops. */
.scroller__chart[data-chart-mount="choropleth"] .chart-body > svg.chart-svg.is-camming .map-layer {
  -webkit-mask-image: none;
  mask-image: none;
}
.scroller__chart .chart-body svg.chart-svg { max-block-size: 100%; inline-size: 100%; }
.scroller__chart .chart-controls {
  display: flex; gap: var(--space-3); flex-wrap: wrap;
  align-items: center;
  font-size: var(--fs-100);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.scroller__chart .chart-controls input[type="range"] { accent-color: var(--accent); inline-size: 180px; }
.scroller__chart .chart-source {
  font-family: var(--font-ui);
  font-size: var(--fs-100);
  /* [R5 fix] WCAG-AA: --ink-soft gives 7.79:1 contrast on cream bg (AAA),
     where --ink-faint gave 3.20:1 — fails AA Normal for 12.75 px regular text.
     This is the per-chart attribution line; it must be readable. */
  color: var(--ink-soft);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* (The header progress-coin was removed in the round-5 debug pass — the header deliberately scrolls
   away, so a coin there can't function as a scroll compass. See LOG 2026-06-21 Session 19.) */

/* ============================================================
   LAYOUT DOCTRINE v2 (amendment §A) — scoped to .chapter--v2 so it rolls out
   chapter-by-chapter (F1 = CH1 reference; F2 = the rest). Centered full-width
   title, persistent left-column subtitle, step rail, breathing air.
   ============================================================ */

/* Left column becomes a grid: [rail] [flow(subtitle + steps)]. */
.chapter--v2 .scroller__text {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--space-4);
  gap: 0;
  padding-block: 0;
}
.chapter--v2 .scroller__flow { grid-column: 2; min-inline-size: 0; }

/* §A.2 · Subtitle — persistent, pinned at the top of the left column. */
.chapter--v2 .chapter-subtitle {
  position: sticky;
  inset-block-start: var(--space-5);
  z-index: 3;
  font-family: var(--font-ui);
  font-size: var(--fs-200);
  color: var(--ink-soft);
  line-height: var(--lh-snug);
  max-inline-size: 34ch;
  margin: 0;
  padding-block: var(--space-2) var(--space-3);
  background: var(--bg);
}

/* §A.3 · Steps — one at a time below the persistent subtitle (unchanged rhythm). */
.chapter--v2 .scroller__steps {
  display: flex;
  flex-direction: column;
  gap: 80dvh;
  padding-block: 6dvh 52dvh;
}

/* §A.4 · Step rail — thin vertical dot column, sticky, left of the steps. */
.step-rail { display: none; }               /* hidden until a chapter opts into v2 */
.chapter--v2 .step-rail {
  grid-column: 1;
  display: flex;
  flex-direction: column;
  gap: 10px;
  position: sticky;
  inset-block-start: 42vh;
  align-self: start;
  margin: 0;
  padding: 0;
}
.rail-dot {
  position: relative;
  inline-size: 22px;
  block-size: 22px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.rail-dot::before {
  content: "";
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  background: var(--ink-fainter);          /* ahead (default) */
  transition: background var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out);
}
.rail-dot.is-read::before { background: var(--ink); }
.rail-dot.is-active::before { background: var(--accent); transform: scale(1.6); }
.rail-dot:hover::before,
.rail-dot:focus-visible::before { transform: scale(1.6); }
.rail-dot:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; border-radius: 50%; }
.rail-microlabel {
  position: absolute;
  inset-inline-start: 20px;
  inset-block-start: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
  font-family: var(--font-ui);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  background: var(--bg);
  padding: 2px 7px;
  border-radius: var(--radius-1);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-2) var(--ease-out);
  box-shadow: var(--shadow-rail);
}
.rail-dot:hover .rail-microlabel,
.rail-dot:focus-visible .rail-microlabel { opacity: 1; }

/* §A.5 · Breathing — the chart must not touch the panel edges (no header in v2 figures).
   Target svgH/panelH 0.84–0.92 with ≥24px air above the chart body and ≥20px below. */
.chapter--v2 .scroller__chart { grid-template-rows: 1fr auto auto; }
.chapter--v2 .scroller__chart .chart-body { margin-block: 34px 26px; }

/* ============================================================
   AMENDMENT-2 §A — sticky-zone layout (scoped .chapter--a2; F1=CH1, F3=rest).
   Title + legend pinned INSIDE the panel; subtitle no-box own slot; rail −28px
   from the card column, vertically centered; footer strip source-left/controls-right.
   Placed after the .chapter--v2 block so it wins the equal-specificity overrides.
   ============================================================ */
/* [AMENDMENT-3 §4] "→12px→panel bottom" rolled to all 10 consumers (was 0). Consumer census
   (LAW §1.1): annotatedLine, smallMultiples, choropleth, heatmap, rateLevel, waffle, housing,
   race, divergingBar, boxplot — every `.chapter--a2` gets gated after this change.
   [ROUND-7 P1] scoreMap dropped from the census with its chapter. */
.chapter--a2 .scroller__chart { grid-template-rows: auto 1fr auto; gap: 0; padding-inline: 0; padding-block: 0 12px; }

/* §A.1 · panel head — title top-center, legend top-right, both pinned with the panel */
.chapter--a2 .panel-head { position: relative; text-align: center; min-block-size: 0; }
.chapter--a2 .panel-head .chart-title {
  font-family: var(--font-display); font-size: var(--fs-500); font-weight: 600;
  line-height: var(--lh-snug); letter-spacing: -0.01em; color: var(--ink-strong); margin: 0;
}
/* §A3 §3 · Legend reserved slot — drops below the fixed theme toggle (pins at the viewport
   top-right ~y54) so the two never overlap; 16px in from the panel edge, capped width so it
   never runs into the toggle or off the panel. Consumer census: `.chart-legend` markup exists
   in exactly ONE place in index.html (CH1) — this selector is textually shared-looking but has
   a single real consumer, confirmed by grep. */
.chapter--a2 .panel-head .chart-legend {
  position: absolute; inset-inline-end: 16px; inset-block-start: 44px; text-align: right;
  max-inline-size: 200px; display: flex; flex-direction: column; line-height: 1.2;
}
.chapter--a2 .chart-legend .legend-title { font-family: var(--font-ui); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-soft); }
.chapter--a2 .chart-legend .legend-sub { font-family: var(--font-ui); font-size: 9.5px; color: var(--ink-soft); }

/* §A.4 · breathing — 24px above the plot, 20px below (before the footer strip) */
.chapter--a2 .scroller__chart .chart-body { margin-block: 24px 0; }

/* §A.5 · footer strip — source LEFT, controls RIGHT, one row, no collision.
   [AMENDMENT-3 §4] "→16px→footer row" rolled to all 11 consumers (was 20px). */
.chapter--a2 .panel-foot { display: flex; justify-content: space-between; align-items: center; gap: var(--space-5); margin-block-start: 16px; }
.chapter--a2 .panel-foot .chart-source { margin: 0; text-align: left; flex: 0 1 auto; font-size: 10px; }
.chapter--a2 .panel-foot .chart-controls { margin: 0; justify-content: flex-end; flex-wrap: nowrap; flex: 0 0 auto; gap: var(--space-2); }

/* CH1 has the longest control cluster (compare picker + up to 4 chips + reset + zoom). Give
   the source a fixed slice so the controls get a predictable remaining width: one row at 0–3
   chips (foot stays compact → plot within the §A.4 breathing floor), wrapping to a 2nd row
   only at the 4-chip max. Scoped to CH1 so the source-only footers elsewhere are untouched. */
.chapter[data-chart="annotatedLine"] .panel-foot .chart-source { flex: 0 1 300px; }
/* row-gap tightened 9→5px (measured: the 4-chip case wraps to 2 rows; every px saved here
   feeds directly into the §2.4 fixed footer reservation instead of coming out of the plot). */
.chapter[data-chart="annotatedLine"] .panel-foot .chart-controls { flex: 1 1 auto; min-inline-size: 0; flex-wrap: wrap; row-gap: 5px; }
.chapter[data-chart="annotatedLine"] .ac-reset, .chapter[data-chart="annotatedLine"] .ac-zoom-btn { padding-block: 2px; }

/* [AMENDMENT-3 §4] Footer rhythm, CH1: x-axis → 12px → brush strip → 16px → footer row → 12px
   → panel bottom (measured; the old 74px SVG bottom-margin + 20px CSS margin left ~75px of dead
   space no rule ever explained — see AnnotatedLine.js margin.bottom 74→20 for the SVG-side half
   of this fix). 16px replaces the shared rule's 20px, CH1-scoped only (the shared `.chapter--a2
   .panel-foot` 20px still governs the other 10 a2 chapters — F3 rolls the rhythm to them with
   their own consumer-gated change, not silently here).
   [AMENDMENT-3 §2.4] min-block-size reserves the WORST-CASE footer height (measured: 4 chips
   wrap the controls to 2 rows, ~69px) so the "auto" grid row never grows/shrinks the plot's
   "1fr" row as chips are added/removed — the plot's svg height gate is ±2px @0 vs @4 chips. */
.chapter[data-chart="annotatedLine"] .panel-foot { margin-block-start: 14px; min-block-size: 64px; }
/* [AMENDMENT-3 §4] "→12px→panel bottom" — the panel had 0 bottom breathing before this. */
.chapter[data-chart="annotatedLine"] .scroller__chart { padding-block-end: 10px; }

/* [AMENDMENT-3 §2.4] race: measured — at its 2-chip max the shared rule's unconstrained
   chart-controls (flex:0 0 auto, single line, ~720px natural width) crowded chart-source
   (flex:0 1 auto) down to ~83px, wrapping its long source line to ~8 rows (110px) and shrinking
   the plot by 75px vs 0 chips. Same root cause as CH1 (source/controls fight for width with no
   agreed split), opposite symptom (source balloons instead of controls wrapping) — same fix:
   bound source, let controls wrap/shrink, then reserve the measured worst-case footer height. */
.chapter[data-chart="race"] .panel-foot .chart-source { flex: 0 1 300px; }
.chapter[data-chart="race"] .panel-foot .chart-controls { flex: 1 1 auto; min-inline-size: 0; flex-wrap: wrap; row-gap: 5px; }
.chapter[data-chart="race"] .ac-reset, .chapter[data-chart="race"] .ac-zoom-btn { padding-block: 2px; }
/* measured natural 2-chip footer = 63px; reserve 64px so 0-chip and 2-chip render identically
   (§2.4 gate: svg height ±2px @0 vs @2 chips). margin/padding trimmed same as CH1 (16→14,
   12→10) to recover the few px the 64px reservation costs the breathing floor. */
.chapter[data-chart="race"] .panel-foot { min-block-size: 64px; margin-block-start: 14px; }
.chapter[data-chart="race"] .scroller__chart { padding-block-end: 10px; }

/* §A.2 · subtitle — NO box, plain text, own slot; 3-line max; z BELOW cards */
.chapter--a2 .scroller__text { display: grid; grid-template-columns: 0 minmax(0, 1fr); column-gap: 0; padding-block: 0; }
.chapter--a2 .scroller__flow { grid-column: 2; min-inline-size: 0; }
.chapter--a2 .chapter-subtitle {
  position: sticky; inset-block-start: var(--space-4); z-index: 1;
  background: none; padding: 0; margin: 0;
  font-family: var(--font-ui); font-size: 13px; color: var(--ink-soft);
  line-height: 1.45; max-inline-size: 34ch; text-align: left;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.chapter--a2 .scroller__steps { margin-block-start: calc(3 * 1.45 * 13px + 24px); position: relative; z-index: 2; }
.chapter--a2 .scroller__step { position: relative; z-index: 2; }

/* §A.3 · step rail — dots −28px from the card column, vertically centered as a group */
.chapter--a2 .step-rail {
  grid-column: 1; position: sticky; inset-block-start: 50vh; align-self: start;
  transform: translateY(-50%); margin-inline-start: -28px; gap: 20px;
}

/* Choropleth keeps a JS-populated legend slot in its figure header → row order stays auto/1fr/auto,
   and the near-full-bleed vignette map wants only a small margin (not the shared 34/26). */
.chapter--v2 .scroller__chart[data-chart-mount="choropleth"] { grid-template-rows: auto 1fr auto; }
.chapter--v2 .scroller__chart[data-chart-mount="choropleth"] .chart-body { margin-block: 16px 22px; }

