/* Data stories scrolly (#132 / #93 Tier B) */

.stories-hub {
  max-width: 720px;
  margin: 0 auto;
  padding: 2rem 1.5rem 4rem;
}

.stories-header {
  text-align: center;
  margin-bottom: 2.5rem;
}

.stories-eyebrow {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #8b0000;
  margin: 0 0 0.5rem;
}

.stories-eyebrow a {
  color: inherit;
  text-decoration: none;
}

.stories-eyebrow a:hover,
.stories-eyebrow a:focus-visible {
  text-decoration: underline;
}

.stories-header h1,
.story-hero h1 {
  font-family: 'Source Serif Pro', serif;
  font-size: 2rem;
  margin: 0 0 0.75rem;
}

.stories-subtitle,
.story-dek {
  color: #333;
  max-width: 36rem;
  margin: 0 auto;
  line-height: 1.55;
}

.stories-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 1.5rem;
}

/* Hub page (src/stories/index.html) never had a multi-column breakpoint
   -- .story-read-next-list (below) added one for itself but the base
   class stayed single-column everywhere. 2 columns keeps card text at a
   comfortable reading width instead of stretching full-width. */
@media (min-width: 700px) {
  .stories-list {
    grid-template-columns: repeat(2, 1fr);
  }
}

.story-card {
  border: 1px solid #e0d8d0;
  border-radius: 8px;
  padding: 1.25rem 1.5rem;
  background: #fffaf5;
}

.story-card h2,
.story-card h3 {
  font-size: 1.25rem;
  margin: 0 0 0.5rem;
}

.story-card h2 a,
.story-card h3 a {
  color: #1a1a1a;
  text-decoration: none;
}

.story-card h2 a:hover,
.story-card h2 a:focus-visible,
.story-card h3 a:hover,
.story-card h3 a:focus-visible {
  color: #8b0000;
  text-decoration: underline;
}

.story-card p {
  margin: 0 0 1rem;
  color: #444;
  line-height: 1.5;
}

.story-card-link {
  font-weight: 600;
  font-size: 0.9rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #8b0000;
}

/* Scrolly story page */
.scrolly-story {
  /* Widened from 960px (#350) -- the figure column was capped well below
     its actual available grid space, leaving figures small and surrounded
     by unused white space on any viewport wider than ~1000px. */
  max-width: 1160px;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 4rem;
}

.story-hero {
  text-align: center;
  margin-bottom: 2rem;
  max-width: 40rem;
  margin-left: auto;
  margin-right: auto;
}

/* Sticky story header (#358) -- pins the story title below the (already
   globally-sticky) site nav as the reader scrolls through steps, so the
   story's identity stays visible without needing to scroll back up.
   Full-width + solid background (not just the 40rem text column) so any
   figure card scrolling up underneath is fully masked, not peeking out on
   either side of a narrower floating box. --story-nav-height mirrors the
   real #nav-placeholder height (measured 81px on desktop); a few px of
   drift on extreme viewports is a cosmetic risk, not a functional one, same
   tradeoff docs/DESIGN_SYSTEM.md's presidents rail already accepts. */
:root {
  --story-nav-height: 81px;
}

.scrolly-story .story-hero {
  position: sticky;
  top: var(--story-nav-height);
  z-index: 40;
  max-width: none;
  background: #fff;
  padding: 0.65rem 1.25rem;
  margin: 0 -1.25rem 2rem;
  border-bottom: 1px solid #e0d8d0;
}

/* Widen the dek specifically for the sticky-hero pinned-fade stories --
   .story-dek's shared 36rem cap (used by the plain stories-hub cards too,
   which stay narrower on purpose) forces THIS story's longer one-sentence
   dek to wrap 3 lines even though .scrolly-story .story-hero above is
   already full-width and has the room to spare. Requested live: "widen
   the section for the story title and subtitle... so we can get more room
   for the story and actually achieve the buffer that I want" -- every line
   the dek doesn't need to wrap is scroll-offset height (--story-slide-top
   in brh-fx.js) the pinned card doesn't have to clear either. */
.scrolly-story .story-dek {
  max-width: 52rem;
}

/* Matching trims to the sticky hero's own vertical rhythm -- smaller than
   the plain (non-sticky) spacing above since this version is visible for
   the whole scroll, not just once at the top; every rem here is offset the
   pinned card has to clear on every subsequent page view of this story. */
.scrolly-story .story-hero-back,
.scrolly-story .stories-eyebrow {
  margin-bottom: 0.35rem;
}

.scrolly-story .story-hero h1 {
  margin-bottom: 0.4rem;
}

/* Top-left "back to all stories" link (#358 follow-up fix) -- reported
   live: "'All stories' should be in the header, top left." Previously this
   link only existed down in the last step's CTA extras, so it scrolled out
   of reach with everything else and duplicated the centered .stories-eyebrow
   link below it to the same destination. Living in the (sticky) .story-hero
   means it's visible for the entire scroll, not just while step 4 is
   active -- and text-align: left overrides the hero's own centered text,
   scoped to just this one line. */
.story-hero-back {
  text-align: left;
  margin: 0 0 0.75rem;
  font-size: 0.85rem;
}

.story-hero-back a {
  color: #8b0000;
  text-decoration: none;
}

.story-hero-back a:hover,
.story-hero-back a:focus-visible {
  text-decoration: underline;
}

.story-status,
.story-loading,
.story-error {
  text-align: center;
  color: #555;
}

.story-error {
  color: #8b0000;
}

/* #376: publish/unpublish config -- shared card treatment for both the
   hub's empty state (zero published stories) and a single story's
   direct-URL guard, so they read as one consistent "not ready yet"
   pattern rather than two different-looking placeholders. */
.stories-coming-soon,
.story-status.story-unpublished {
  max-width: 36rem;
  margin: 3rem auto;
  padding: 2rem 1.75rem;
  background: #fffaf5;
  border: 1px solid #e0d8d0;
  border-radius: 8px;
  text-align: center;
}

.story-status.story-unpublished {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  color: #444;
}

.stories-coming-soon h2,
.story-unpublished-title {
  margin: 0;
  font-family: 'Source Serif Pro', serif;
  font-size: 1.3rem;
  color: #1a1a1a;
}

.story-unpublished-body {
  line-height: 1.6;
}

.story-unpublished-link {
  margin-top: 0.25rem;
  color: #8b0000;
  font-weight: 600;
  text-decoration: none;
}

.story-unpublished-link:hover,
.story-unpublished-link:focus-visible {
  text-decoration: underline;
}

.scrolly-layout {
  display: grid;
  /* #370: without an explicit track, a CSS Grid container below the
     desktop media query falls back to an implicit "auto" column, which
     sizes itself to the MAX-CONTENT width of its widest descendant --
     not the viewport. A horizontal scroll-snap strip (.story-headline-
     gallery, .story-lang-headline-strip, etc.) is exactly that: its
     max-content width is the full unwrapped row (e.g. 3468px for 15
     cards), so the whole .scrolly-layout--pinned-fade column, and every
     block-level ancestor up to <body>, silently grew to match on mobile
     -- invisibly, because body/html clip the resulting overflow instead
     of showing a scrollbar. minmax(0, 1fr) forces the track to respect
     the actual available width regardless of what a descendant wants;
     it's the mobile-first default here on purpose so every layout
     variant (not just --pinned-fade) is safe by default, even though
     the desktop media query below re-specifies its own columns anyway. */
  grid-template-columns: minmax(0, 1fr);
  gap: 2rem;
  margin: 2rem 0;
}

@media (min-width: 900px) {
  .scrolly-layout {
    /* Wider minimums (#350) so the figure column actually uses the room
       .scrolly-story's wider max-width now provides, instead of the figure
       SVGs (each with their own max-width cap, also raised in this change)
       staying small inside a column that could fit them 40-50% larger.
       .scrolly-figure-host is the FIRST grid child in every story's HTML
       (see src/stories/*.html) -- the larger share goes to column 1, not 2,
       so the figure (the actual point of a "data story") gets the room, not
       the narrative text column (which only needs ~34rem per .story-step-text
       anyway). */
    grid-template-columns: minmax(400px, 1.15fr) minmax(320px, 1fr);
    align-items: start;
    gap: 3rem;
  }

  .scrolly-figure-host {
    position: sticky;
    top: 5rem;
    align-self: start;
  }

  /* #355/#356 follow-up fix: .scrolly-layout--inline-figures has only ONE
     grid child (#story-steps) -- the two-column grid-template-columns above
     squeezed it into column 1 alone (e.g. 573px of an 1120px container),
     leaving the second column empty. Reported live as "not centered, still
     not wide enough". Force a single full-width column for this modifier
     so #story-steps (and everything inline inside it) uses the whole
     .scrolly-story width. Same fix, same reason, for .scrolly-layout--
     pinned-fade (#358) -- its only child is .scrolly-pin-track, and missed
     this override the first time around (confirmed live: "stuck in a two
     column grid", card using only ~half the width). */
  .scrolly-layout--inline-figures,
  .scrolly-layout--pinned-fade {
    grid-template-columns: 1fr;
  }

}

/* Full-width inline figures (#355, #356) -- opt-in per story via
   `<div id="scrolly-layout" class="scrolly-layout scrolly-layout--inline-figures">`
   with each story's wiring rendering one figure snapshot inline at the top
   of its own <section class="scrolly-step">, instead of one shared sticky
   figure swapped in a side column.

   History: the first attempt (#355) kept a single shared STICKY figure but
   widened it to full width. That broke as soon as figure and text shared
   one column -- a pinned figure and text scrolling past it in the SAME
   column visually cross paths (confirmed via real browser use, not just
   measurement: "the story blurb overlaps the visual"). The 2-column layout
   never had this problem because figure and text were spatially separate
   columns; going full-width removed that separation, and sticky positioning
   alone doesn't restore it.

   This is the fix: no pinned element, no shared scroll region between a
   fixed figure and moving text -- each figure is a normal-flow sibling of
   its own step's text, so nothing can visually overlap by construction.
   Standard "illustrated narrative" pattern (figure, caption, repeat), not
   classic sticky scrollytelling. Keeps vertical scroll + the existing
   scrolly-engine IntersectionObserver step detection (still used for
   .is-active / aria-current marking) -- this is a layout change, not an
   interaction-model change. */
.scrolly-layout--inline-figures .scrolly-step {
  min-height: auto;
  padding: 2.5rem 0;
  display: block;
  border-top: 1px solid #e0d8d0;
  /* Fade + rise reveal (#358) -- safe to animate here specifically because
     nothing is pinned/sticky in this mode: figure+text fade together as ONE
     unit (the .scrolly-step itself), driven by the existing .is-active
     class from scrolly-engine's IntersectionObserver. No scroll-position
     math, no risk of the sticky-slide clipping bug found and reverted
     earlier -- purely a paint-layer treatment on normal-flow content. */
  opacity: 0.3;
  transform: translateY(20px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.scrolly-layout--inline-figures .scrolly-step.is-active {
  opacity: 1;
  transform: translateY(0);
}

/* First step has no step above it to divide from -- the hero/dek already
   provides that separation, a top border here would just float. */
.scrolly-layout--inline-figures .scrolly-step:first-child {
  padding-top: 0;
  border-top: none;
}

.scrolly-layout--inline-figures .scrolly-step:last-child {
  padding-bottom: 0;
}

.scrolly-layout--inline-figures .story-step-text {
  max-width: 42rem;
}

/* Pinned in-place crossfade (#358) -- scroll stays the interaction model
   (per explicit follow-up direction: "i don't want a click through. i want
   to scroll and animate the in place fade out fade in"), but unlike the
   #358 "sticky slide" attempt that got reverted, the sticky viewport here
   spans ALL steps at once via one shared tall track, so it never releases
   mid-narrative -- there is no per-step pin/release boundary for the active
   (opaque) content to get clipped against. This mirrors the original
   pre-#355 2-column layout (sticky figure-host sibling to a tall steps
   column) -- same mechanism, just full-width and single-column, with
   figure+text crossfading together as one .scrolly-step instead of only
   the figure swapping.
   Structure: .scrolly-pin-viewport (sticky) and .scrolly-markers (tall,
   invisible spacer stack -- one .scrolly-step-marker per step, purely an
   IntersectionObserver target) are SIBLINGS inside .scrolly-pin-track;
   .scrolly-steps (the visible, grid-stacked slides) live inside the
   viewport. scrolly-engine's initScrolly observes the markers
   (observeSelector) but marks the visible .scrolly-step slides active
   (activeSelector) -- see src/js/stories/brh-fx.js.
   Desktop-only (matches the pre-existing >=900px gate on the classic
   2-column sticky figure-host below): a full-viewport pin section is a
   known mobile jank risk (browser chrome/address-bar resize mid-scroll),
   so mobile and reduced-motion both get the SAME plain fallback -- every
   step visible at once, stacked, dividers between them, no pin/fade at
   all. That fallback is also the browser's default state before JS runs,
   so it's what non-JS/slow-network readers see too. */
.scrolly-layout--pinned-fade .scrolly-steps {
  display: block;
}

.scrolly-layout--pinned-fade .scrolly-step {
  /* Without this override, the un-scoped base .scrolly-step rule (further
     down this file, used by the classic 2-column stories) leaks its
     `display: flex; align-items: center` in here, laying the figure and
     text out SIDE BY SIDE instead of stacked -- confirmed live ("now it
     looks terrible"). Same leak risk for min-height: 70vh. */
  display: block;
  min-height: auto;
  opacity: 1;
  padding: 2rem 0;
  border-top: 1px solid #e0d8d0;
}

.scrolly-layout--pinned-fade .scrolly-step:first-child {
  padding-top: 0;
  border-top: none;
}

.scrolly-layout--pinned-fade .scrolly-markers {
  display: none;
}

/* Mobile-first base: hidden until the >=900px pinned-fade rule turns it on
   below -- the mobile fallback has no pin/crossfade to hint at. */
.scrolly-scroll-hint {
  display: none;
}

@media (min-width: 900px) {
  .scrolly-layout--pinned-fade {
    /* Kill the un-scoped base .scrolly-layout's `margin: 2rem 0` -- that
       32px+ top margin was extra distance the card had to travel (in
       normal flow, visibly moving with the page) before it reached its
       own sticky point and locked in place. Reported live: "the visual
       moves while i do the initial scroll until it hits the header. it
       shouldn't move at all." Zero top margin puts the card's natural
       position (almost) exactly at its sticky target from scroll 0, so
       there's nothing left to travel. */
    margin-top: 0;
  }

  /* Adjoining margins collapse to the larger of the two (CSS spec), so
     .story-hero's own margin-bottom: 2rem re-creates the exact same 32px
     gap even with the override above -- confirmed by measurement (viewport
     top 338px vs sticky target 282px, a 56px gap, before this fix; the
     32px came from whichever margin won the collapse). Zero it too, only
     when this story uses pinned-fade (mobile/other layouts still want the
     normal hero spacing). */
  .scrolly-story:has(.scrolly-layout--pinned-fade) .story-hero {
    margin-bottom: 0;
  }

  /* .scrolly-story's own 1.5rem (24px) top padding is a THIRD source of
     the same "extra distance to travel before locking in" problem the two
     fixes above already target -- it sits ABOVE .story-hero, so even with
     both margins zeroed, the hero's natural (pre-scroll) position is still
     24px below its sticky target (top: var(--story-nav-height)) until the
     reader scrolls that first 24px away. Reported live: "the initial
     scroll down... moves the page a few pixels before locking into place.
     it should be in that locked position from the start." Zeroing it here
     (not on the shared base .scrolly-story rule, which other layouts still
     want padded) puts the hero at its final stuck position from the very
     first frame, matching the card's own already-fixed behavior above. */
  .scrolly-story:has(.scrolly-layout--pinned-fade) {
    padding-top: 0;
  }

  /* .scrolly-pin-viewport and .scrolly-markers are grid-stacked in the SAME
     cell (not normal-flow siblings) so the track's height is the MAX of
     the two, not their SUM. As normal-flow siblings, the viewport's own
     natural height (based on the tallest of the 5 stacked steps, since
     CSS grid auto-sizing for .scrolly-steps ignores opacity) was being
     ADDED on top of the markers' combined height -- confirmed live: the
     sticky viewport started releasing (scrolling away, part of it going
     behind the sticky header) hundreds of px before the last marker had
     even finished, clipping content mid-narrative again despite the
     single-continuous-viewport design that was supposed to prevent
     exactly this. */
  .scrolly-layout--pinned-fade .scrolly-pin-track {
    display: grid;
  }

  .scrolly-layout--pinned-fade .scrolly-pin-viewport {
    grid-column: 1;
    grid-row: 1;
    /* align-self: start (not the grid default of stretch) keeps the
       viewport at its own natural content height instead of stretching to
       fill the whole (much taller, marker-dominated) row -- a stretched
       sticky element has nowhere left to travel/stick meaningfully. */
    align-self: start;
    position: sticky;
    /* Set from JS (measured nav + sticky-hero height, src/js/stories/brh-fx.js)
       so the viewport starts right below both sticky bars. Fallback covers
       nav (~81px) + a typical hero (~200px). */
    top: var(--story-slide-top, 290px);
  }

  /* Sizes the card to whichever step is ACTUALLY showing, not the tallest
     of all 5 -- see syncActiveStepHeight() in src/js/stories/brh-fx.js for
     why grid-stacked steps otherwise force a constant, tallest-step height
     regardless of which one is opaque. Reported live, repeatedly, even
     after earlier trailer/footer-gap fixes: "too much space still". A
     transition on height (matching the existing 0.6s opacity/transform
     crossfade timing closely enough) means the card visibly resizes along
     with the fade instead of snapping. overflow: hidden clips whatever
     inactive (opacity: 0, already invisible) steps are taller than the
     current target height -- no visual effect since they're unseen anyway.
     align-items: start is required for this to work at all: the default
     `stretch` would otherwise force EVERY step (including short ones) to
     render at the row's own auto-computed height, which grid still bases
     on the TALLEST co-located item regardless of align-items -- so without
     this override, measuring any individual step's height in JS would
     just read back that same shared tallest-of-5 height, defeating the
     whole point. `start` decouples "how tall the row track is" from "how
     tall each item actually renders", so each step keeps its own natural
     height (measurable and independently settable as the container's
     explicit height above) even while sharing a cell with taller siblings. */
  .scrolly-layout--pinned-fade .scrolly-steps {
    display: grid;
    align-items: start;
    height: var(--story-active-step-height, auto);
    overflow: hidden;
    transition: height 0.5s ease;
  }

  .scrolly-layout--pinned-fade .scrolly-step {
    display: block;
    min-height: auto;
    grid-column: 1;
    grid-row: 1;
    padding: 0;
    border-top: none;
    opacity: 0;
    /* Subtle scale-in (matches the click-through slideshow's polish, which
       the pinned crossfade otherwise replicates exactly minus the click). */
    transform: scale(0.98);
    transition: opacity 0.6s ease, transform 0.6s ease;
    pointer-events: none;
  }

  .scrolly-layout--pinned-fade .scrolly-step.is-active {
    opacity: 1;
    transform: scale(1);
    pointer-events: auto;
  }

  .scrolly-layout--pinned-fade .scrolly-markers {
    display: block;
    grid-column: 1;
    grid-row: 1;
  }

  /* 12.5vh -- with the default rootMargin (-45% bottom, shrinking the
     observer root to 55% of viewport height), a marker SHORTER than the
     root can be FULLY contained in it over a real scroll range, reaching
     intersection ratio 1.0, not just some fragile ceiling -- e.g. the
     original 100vh markers had a ceiling of EXACTLY 0.55 (root 55% /
     marker 100%), landing right on the default 0.55 threshold with no
     margin, so it almost never reliably fired. 45vh (roughly one
     viewport-height per step, in line with typical scrollytelling
     convention of ~0.75-1.0vh/step -- Perplexity research, #358) fixed
     reliability but read as "too much scroll depth between steps" live;
     25vh was snappier but STILL too much on further feedback ("reduce it
     by 50%"). 12.5vh keeps the same headroom under the 55vh root (ratio
     can still reach 1.0) with half the scroll distance per step again. */
  .scrolly-layout--pinned-fade .scrolly-step-marker {
    height: 12.5vh;
  }

  /* Height set dynamically from the pinned card's actual rendered size --
     see syncPinDimensions() in src/js/stories/brh-fx.js. A fixed 100vh
     guess here left a large, content-independent dead gap before the
     footer (measured live: 321px) -- reported as "i feel like this should
     be dynamic." 100vh fallback only covers the brief window before JS
     measures and sets the real value (and no-JS/slow-network readers, who
     never get past this fallback -- acceptable since they also never see
     the sticky pin behavior at all without JS). */
  .scrolly-layout--pinned-fade .scrolly-pin-trailer {
    height: var(--story-trailer-height, 100vh);
  }

  /* Scroll hint (#358) -- research (Perplexity) on scrollytelling
     affordances: minimal is best (small chevron + short label beats a
     bouncing-mouse icon or persistent motion), and it should disappear on
     the reader's first scroll rather than lingering. Rendered as a child
     of step 0 ONLY (see renderScrollHint() in src/js/stories/brh-fx.js),
     so it gets the existing fade-to-0 for free once the reader advances
     past the intro -- no separate "step changed" JS needed for that case.
     A one-time scroll listener additionally dismisses it (`.is-dismissed`)
     on the reader's first scroll attempt, even before that transition
     completes, matching "disappear on first meaningful scroll" rather
     than waiting for the full step change.
     Sized up (#358 follow-up fix) -- "minimal" doesn't mean "illegible":
     the original 0.75rem text / 1.35rem chevron read as too small next to
     the ~250px+ tall figure card above it, reported live as "too small
     relative to the content and easy to miss". Text roughly doubled and
     the chevron more than doubled (it's the primary visual anchor, a
     bouncing arrow reads at a glance even before the label registers),
     plus a darker color for more contrast against the white background
     -- still monochrome/understated, just no longer easy to overlook. */
  .scrolly-layout--pinned-fade .scrolly-scroll-hint {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    margin-top: 2rem;
    color: #555;
    font-family: 'DM Sans', sans-serif;
    font-size: 0.95rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    opacity: 1;
    transition: opacity 0.4s ease;
  }

  .scrolly-layout--pinned-fade .scrolly-scroll-hint.is-dismissed {
    opacity: 0;
  }

  .scrolly-layout--pinned-fade .scrolly-scroll-hint-chevron {
    font-size: 2.5rem;
    line-height: 1;
    animation: scrolly-hint-bounce 1.8s ease-in-out infinite;
  }

  @keyframes scrolly-hint-bounce {
    0%, 100% {
      transform: translateY(0);
    }

    50% {
      transform: translateY(10px);
    }
  }
}

/* Reduced motion (any width): the plain-list fallback doesn't need a
   "scroll to advance" hint at all -- there's no pin/crossfade to
   discover, just an ordinary page to read. Also stops the bounce
   animation outright for anyone who somehow still sees the hint. */
.scrolly-reduced-motion .scrolly-scroll-hint {
  display: none;
}

.scrolly-reduced-motion .scrolly-scroll-hint-chevron {
  animation: none;
}

/* Accessible fallback (mobile OR reduced motion, at any width): abandon the
   pin/crossfade entirely rather than try to reproduce it without motion --
   plain stacked list, matching .scrolly-layout--inline-figures' reduced-
   motion treatment. Specificity (2 classes) beats the >=900px rule above
   (1 class) regardless of media-query/source order. */
.scrolly-reduced-motion.scrolly-layout--pinned-fade .scrolly-pin-viewport {
  position: static;
}

.scrolly-reduced-motion.scrolly-layout--pinned-fade .scrolly-steps {
  display: block;
}

.scrolly-reduced-motion.scrolly-layout--pinned-fade .scrolly-step {
  display: block;
  min-height: auto;
  grid-column: unset;
  grid-row: unset;
  padding: 2rem 0;
  border-top: 1px solid #e0d8d0;
  opacity: 1;
  transform: none;
  transition: none;
  pointer-events: auto;
}

.scrolly-reduced-motion.scrolly-layout--pinned-fade .scrolly-step:first-child {
  padding-top: 0;
  border-top: none;
}

.scrolly-reduced-motion.scrolly-layout--pinned-fade .scrolly-markers {
  display: none;
}

.scrolly-layout--pinned-fade .story-step-text {
  /* No max-width -- match the full-width figure card above it, not a
     narrower 42rem reading column (that mismatch is what looked wrong
     live: text visibly narrower than the card it belongs to). */
  max-width: none;
}

/* Each figure gets its own visible card (border + tint), matching the old
   .scrolly-figure frame -- without this, figure and text had identical
   "no background" treatment and the eye couldn't tell where one step's
   figure/text pair ended and the next began (reported live: "hard to
   differentiate one block from another"). */
.scrolly-step-figure {
  margin-bottom: 1.5rem;
  padding: 1.25rem;
  background: #fffaf5;
  border: 1px solid #e0d8d0;
  border-radius: 8px;
}

.scrolly-reduced-motion .scrolly-figure-host {
  position: static;
}

/* Responsive SVG sizing (#350) -- these 10 story figure SVGs had NO width
   CSS at all, so they rendered at their literal HTML width/height
   attributes (e.g. 340x360) regardless of how much space their wrapper
   figure actually had -- the real root cause of figures looking "scrunched"
   with unused white space around them, not just the wrapper max-width caps
   (which were also too small and got raised separately in this change).
   viewBox on each keeps the internal coordinate system and aspect ratio
   intact; this just lets the rendered box scale up to fill its container. */
.story-brh-fx-ribbon-svg,
.story-debt-era-svg,
.story-debt-estimate-sparkline,
.story-debt-ribbon-svg,
.story-inflation-ribbon-svg,
.story-lang-ribbons-svg,
.story-market-ribbon-svg,
.story-occupation-ribbon-svg,
.story-women-ribbons-svg,
.viz-external-sparkline {
  display: block;
  width: 100%;
  height: auto;
}

.scrolly-figure {
  background: #fffaf5;
  border: 1px solid #e0d8d0;
  border-radius: 8px;
  padding: 1rem;
  min-height: 220px;
}

.scrolly-steps {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.scrolly-step {
  min-height: 70vh;
  display: flex;
  align-items: center;
  padding: 2rem 0;
  opacity: 0.45;
  transition: opacity 0.25s ease;
}

.scrolly-reduced-motion .scrolly-step {
  min-height: auto;
  opacity: 1;
  transform: none;
  padding: 1rem 0;
}

.scrolly-step.is-active {
  opacity: 1;
}

.story-step-text {
  font-family: 'Source Serif Pro', serif;
  font-size: 1.2rem;
  line-height: 1.65;
  margin: 0;
  max-width: 34rem;
}

/* Quarter chart */
.story-quarter-chart {
  width: 100%;
  height: auto;
  display: block;
}

.story-quarter-label,
.story-quarter-value {
  font-family: 'DM Sans', sans-serif;
  font-size: 12px;
  fill: #333;
}

.story-quarter-track {
  fill: #f0e8e0;
}

.story-quarter-fill {
  fill: #c41e3a;
  transition: width 0.3s ease;
}

.story-quarter-bar:not(.is-active) .story-quarter-fill {
  fill: #e8a0a8;
}

.scrolly-figure[data-step="3"] .story-quarter-bar .story-quarter-fill,
.scrolly-figure[data-step="4"] .story-quarter-bar .story-quarter-fill {
  fill: #c41e3a;
}

.story-attention-chart,
.story-decade-chart {
  width: 100%;
  max-width: 960px; /* widened again (#371) -- was 520px (#350), from before the pinned-fade
    full-width redesign. A pinned card now runs ~1120px wide; 520px left an obvious dead-space gap
    beside the card's cream background/border (the enclosing box spans the full card, the content
    inside it didn't) -- reported live as content "stuck in the left side" of the card. */
  height: auto;
}

.story-attention-label,
.story-decade-label {
  font-size: 0.75rem;
  fill: #333;
}

.story-attention-track,
.story-decade-track {
  fill: #f0e8e0;
}

.story-attention-fill,
.story-decade-fill {
  fill: #c41e3a;
  transition: width 0.3s ease;
}

.story-attention-bar:not(.is-active) .story-attention-fill,
.story-decade-bar:not(.is-active) .story-decade-fill {
  fill: #e8a0a8;
}

.story-attention-value,
.story-decade-value {
  font-size: 0.75rem;
  fill: #444;
}

/* Languages of power (#209, area ribbon rebuild #274) */
/* #374: some steps stack this ribbon with a headline-collage strip or
   typographic specimens, pushing total step height to 758-881px against
   a ~643px budget. 750px trims the ribbon's own height a bit further
   than the #373 text-scaling pass alone did. */
.story-lang-ribbons {
  max-width: 750px;
}

.story-lang-ribbons--dimmed .story-lang-ribbon {
  opacity: 0.45;
}

.story-lang-ribbons-heading {
  margin: 0 0 0.35rem;
  font-size: 0.95rem;
  font-weight: 700;
  font-family: 'DM Sans', sans-serif;
}

.story-lang-ribbons-caption,
.story-lang-disclaimer {
  margin: 0 0 0.75rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: #555;
}

.story-lang-ribbon-area {
  opacity: 0.35;
}

.story-lang-ribbon.is-active .story-lang-ribbon-area,
.story-lang-ribbon.is-zoomed .story-lang-ribbon-area {
  opacity: 0.55;
}

.story-lang-ribbon.is-dimmed .story-lang-ribbon-area {
  opacity: 0.2;
}

.story-lang-ribbon-line {
  stroke-width: 1.5;
}

/* #373: these are SVG <text> elements inside an inner <svg viewBox="0 0 280
   ...">; the CSS font-size below is set in this SVG's own *local* user-unit
   space, then multiplied by however much bigger the SVG renders than its
   280-unit viewBox. Before #371 widened .story-lang-ribbons from 520px to
   960px, that scale factor was ~1.86x (520/280); it's now ~3.43x (960/280)
   -- a ~1.85x jump nobody compensated for, so this text rendered at nearly
   double its previous (already full) size: measured live at ~46px/50px
   effective height next to a 13.6px figcaption right beside it. Sizes
   below are chosen so the RENDERED result (local size * ~3.43 at the
   current 960px width) lands close to the surrounding UI text, not so the
   *local* number looks reasonable on its own -- if .story-lang-ribbons'
   max-width ever changes again, these need to move with it. */
/* #374: recomputed again for the bespoke 750px cap above (scale ~2.68x,
   not the ~3.43x this was originally tuned for at 960px). */
.story-lang-ribbon-label,
.story-lang-ribbon-share,
.story-lang-ribbon-decade {
  fill: #444;
  font-size: 0.233rem;
  font-family: 'DM Sans', sans-serif;
}

.story-lang-ribbon-label,
.story-lang-ribbon-share {
  font-size: 0.287rem;
}

.story-lang-ribbon-label {
  font-weight: 600;
}

.story-lang-ribbon-decade.is-active {
  font-weight: 700;
  fill: #8b0000;
}

/* #370: flex-column instead of grid for pure vertical stacking -- a CSS
   Grid container with no explicit grid-template-columns creates an
   implicit "auto" track that GROWS to fit a descendant's max-content
   width (e.g. a horizontal scroll-snap strip's full unwrapped row),
   ignoring the viewport entirely. A column-direction flex container
   doesn't have that failure mode: its cross-axis (width) sizing behaves
   like a normal block box, so oversized descendants scroll/overflow
   *inside* it instead of blowing up its own width. Same visual result
   (vertical stack with a gap) for every one of these figure/specimen/
   spotlight wrappers -- see the "Step height budget" section of
   adversarial-uat-otdihh/SKILL.md for the investigation. */
.story-lang-figure-split,
.story-lang-figure-context {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 960px; /* widened again (#371) -- was 520px (#350), from before the pinned-fade
    full-width redesign. A pinned card now runs ~1120px wide; 520px left an obvious dead-space gap
    beside the card's cream background/border (the enclosing box spans the full card, the content
    inside it didn't) -- reported live as content "stuck in the left side" of the card. */
}

.story-lang-specimens {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.story-lang-specimen {
  margin: 0;
  border-left: 3px solid #002a9f;
  padding: 0.5rem 0 0.5rem 0.75rem;
  background: #f5f8ff;
}

.story-lang-specimen-type {
  margin: 0 0 0.25rem;
  font-size: 0.65rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #666;
}

.story-lang-specimen-title {
  margin: 0;
  font-family: 'Source Serif Pro', serif;
  font-size: 0.95rem;
  line-height: 1.4;
}

.story-lang-specimen-meta {
  margin: 0.35rem 0 0;
  font-size: 0.75rem;
  color: #666;
}

.story-lang-specimen-empty {
  margin: 0;
  font-size: 0.85rem;
  color: #666;
}

.story-lang-headline-collage {
  margin-top: 0.75rem;
}

.story-lang-headline-strip {
  display: flex;
  gap: 0.75rem;
  overflow-x: auto;
  padding-bottom: 0.5rem;
  scroll-snap-type: x mandatory;
}

.story-lang-headline-card {
  flex: 0 0 200px;
  scroll-snap-align: start;
  padding: 0.75rem;
  background: #fffaf5;
  border: 1px solid #e8dfd4;
  border-radius: 4px;
}

.story-lang-headline-year {
  margin: 0 0 0.35rem;
  font-size: 0.7rem;
  font-weight: 600;
  color: #8b0000;
}

.story-lang-headline-title {
  margin: 0;
  font-family: 'Source Serif Pro', serif;
  font-size: 0.85rem;
  line-height: 1.35;
}

.story-ipc-lens {
  margin: 0;
}

.story-ipc-title {
  font-size: 1rem;
  font-weight: 600;
  margin: 0 0 0.35rem;
}

.story-ipc-caption,
.story-ipc-range,
.story-ipc-source {
  font-size: 0.85rem;
  color: #555;
  line-height: 1.45;
  margin: 0 0 0.5rem;
}

.story-ipc-sparkline {
  display: block;
  max-width: 100%;
  margin: 0.5rem 0 0.75rem;
}

.story-ipc-line {
  stroke: #8b0000;
  stroke-width: 2;
}

.story-ipc-lens--brh .story-ipc-line {
  stroke: #1a4d6d;
}

.story-ipc-latest {
  fill: currentColor;
}

.story-fx-lens {
  margin: 0;
}

.story-fx-title {
  font-size: 1rem;
  font-weight: 600;
  margin: 0 0 0.35rem;
}

.story-fx-caption,
.story-fx-range,
.story-fx-source {
  font-size: 0.85rem;
  color: #555;
  line-height: 1.45;
  margin: 0 0 0.5rem;
}

.story-fx-sparkline {
  display: block;
  max-width: 100%;
  margin: 0.5rem 0 0.75rem;
}

.story-fx-line {
  stroke: #1a4d6d;
  stroke-width: 2;
}

.story-fx-lens--depreciation .story-fx-line {
  stroke: #8b0000;
}

.story-fx-latest {
  fill: currentColor;
}

.story-footer {
  margin-top: 3rem;
  padding-top: 1.5rem;
  border-top: 1px solid #e0d8d0;
}

.story-source {
  font-size: 0.85rem;
  color: #555;
  line-height: 1.5;
  margin: 0 0 1rem;
}

.story-table-details {
  margin-bottom: 1.5rem;
}

.story-table-details summary {
  cursor: pointer;
  font-weight: 600;
  margin-bottom: 0.75rem;
}

.story-back {
  font-size: 0.9rem;
}

.story-back a {
  color: #8b0000;
}

.labs-stories-link {
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid #e0d8d0;
}

.labs-stories-link h2 {
  font-size: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 0 0 0.5rem;
}

.labs-stories-link p {
  margin: 0 0 0.75rem;
  color: #444;
}

/* Shared "read next" conclusion module (#358 follow-up) -- rendered by
   src/js/stories/story-read-next.js into #story-read-next-placeholder on
   EVERY /stories/*.html page via load-shared.js, replacing what used to
   be dead/blank space after a story's own citation + data table. A
   generous top margin and rule visually mark this as "page furniture"
   (the reader's own next step) distinct from the story's authored
   content above it -- same separation Perplexity's scrollytelling-ending
   research (#358) found across NYT/Pudding/Reuters/WaPo/538/Bloomberg:
   resolve the narrative first, THEN offer a relevant next path. */
.story-read-next {
  margin-top: 3rem;
  padding-top: 2rem;
  border-top: 1px solid #e0d8d0;
}

.story-read-next h2 {
  font-family: 'DM Sans', sans-serif;
  font-size: 0.85rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: #8b0000;
  margin: 0 0 1.25rem;
}

.story-read-next-list {
  margin-bottom: 1.5rem;
}

@media (min-width: 700px) {
  .story-read-next-list {
    grid-template-columns: repeat(3, 1fr);
  }
}

.story-read-next-all {
  margin: 0;
  font-size: 0.9rem;
  text-align: center;
}

.story-read-next-all a {
  color: #8b0000;
}

/* Macro context tri-track ribbon (#268) */
.story-macro-ribbon {
  margin: 0;
}

.story-macro-ribbon--dimmed .story-macro-lane.is-active {
  opacity: 0.65;
}

.story-macro-ribbon-heading {
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.95rem;
  font-weight: 600;
  margin: 0 0 0.35rem;
}

.story-macro-ribbon-caption,
.story-macro-disclaimer {
  font-size: 0.85rem;
  line-height: 1.45;
  margin: 0 0 0.5rem;
  color: var(--text-muted, #555);
}

.story-macro-disclaimer {
  margin-top: 0.75rem;
  font-style: italic;
}

.story-macro-ribbon-svg {
  display: block;
  max-width: 100%;
  height: auto;
  margin: 0.5rem 0;
}

/* #374: 0.7rem was correctly sized for the OLD fixed, non-responsive
   380px SVG (scale exactly 1:1 -- see .story-macro-terrain-svg above).
   Now that it scales up to a 700px cap (scale ~1.84x), recompute to land
   near the same ~13px rendered size instead of the ~20px it'd be at the
   old local value. */
.story-macro-axis-label,
.story-macro-lane-label,
.story-macro-lane-scale,
.story-macro-wb-label,
.story-macro-highlight-label {
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.7rem;
  fill: var(--text-muted, #666);
}

.story-macro-lane-label {
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.story-macro-lane-track {
  stroke: #d8d0c8;
  stroke-width: 1;
}

.story-macro-lane.is-pending .story-macro-lane-track {
  stroke-dasharray: 4 4;
  opacity: 0.5;
}

.story-macro-lane.is-active .story-macro-lane-track {
  stroke: #b8a898;
}

.story-macro-wb-line,
.story-macro-highlight-line {
  stroke: var(--haiti-blue, #002a9f);
  stroke-width: 1;
  stroke-dasharray: 4 4;
  opacity: 0.75;
}

.story-macro-archive-area {
  fill: rgba(139, 0, 0, 0.12);
}

.story-macro-archive-line {
  stroke: #8b0000;
  stroke-width: 2;
}

.story-macro-population-line {
  stroke: #1a5f7a;
  stroke-width: 2;
}

.story-macro-gdp-line {
  stroke: #5a4a6a;
  stroke-width: 2;
}

.story-macro-figure-full {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

/* Layered-terrain figure (#300) -- three translucent overlapping silhouette
   layers on one shared baseline, replacing the parallel-lane ribbon so this
   story reads as visually distinct from inflation-lenses/brh-fx/
   marketplace-inflation's shared ribbon scaffold. */
/* #374: this SVG was missed by the #371/#373 sweep entirely -- it renders
   at a raw, non-responsive `width="${width}"` SVG attribute (380px) with
   no `width: 100%` CSS override at all, unlike every other -ribbon-svg
   class. At desktop this just meant it never grew to use the pinned
   card's full width (an #371-style dead-space case nobody caught
   because it doesn't share the "-ribbon(s)-svg" naming this sweep
   originally grepped for); at a 320px mobile viewport the fixed 380px
   width overflowed its container outright (#370). */
.story-macro-terrain-svg {
  display: block;
  width: 100%;
  max-width: 440px;
  height: auto;
  overflow: visible;
}

.story-macro-terrain-baseline {
  stroke: #d8d0c8;
  stroke-width: 1;
}

.story-macro-terrain-population {
  fill: rgba(26, 95, 122, 0.35);
  stroke: #1a5f7a;
  stroke-width: 1.5;
}

.story-macro-terrain-gdp {
  fill: rgba(90, 74, 106, 0.35);
  stroke: #5a4a6a;
  stroke-width: 1.5;
}

.story-macro-terrain-archive {
  fill: rgba(139, 0, 0, 0.45);
  stroke: #8b0000;
  stroke-width: 2;
}

.story-macro-terrain-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0 0 0.5rem;
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.7rem;
  color: #444;
}

.story-macro-terrain-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.story-macro-terrain-legend-swatch {
  display: inline-block;
  width: 0.65rem;
  height: 0.65rem;
  border-radius: 2px;
}

.story-macro-legend--archive .story-macro-terrain-legend-swatch {
  background: #8b0000;
}

.story-macro-legend--population .story-macro-terrain-legend-swatch {
  background: #1a5f7a;
}

.story-macro-legend--gdp .story-macro-terrain-legend-swatch {
  background: #5a4a6a;
}

/* Archive silences story (#214) */
.story-silences-heatmap .story-chart-title,
/* #374: .story-dept-svg (width:100%) never had a width cap on its
   containing figure -- pre-existing, unrelated to #371. A geographic map
   (viewBox 400x300, Haiti's rough bounding box) rendered at the full
   ~941px card width, alone, at ~706px tall. No embedded <text> in this
   one (just <path> department shapes), so no font-size to recompute --
   simpler than every other fix in this pass. */
.story-silences-dept {
  max-width: 550px;
}

.story-silences-dept .story-chart-title {
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.95rem;
  font-weight: 600;
  margin: 0 0 0.35rem;
}

.story-silences-heatmap .story-chart-desc,
.story-silences-dept .story-chart-desc {
  font-size: 0.85rem;
  color: var(--text-muted, #555);
  margin: 0 0 0.75rem;
}

/* #369: the shared 12-month calendar heatmap (days-of-joy, archive-silences)
   was one row per month -- 12 rows stacked, ~720px tall alone -- which
   pushed whole pinned-fade steps past the card's viewport budget
   (days-of-joy measured ~3200px in a ~1264px viewport before this fix).
   Running 3 months per row cuts the grid to 4 rows and uses the horizontal
   space a full-width pinned card already has, instead of stacking
   vertically forever. Also: story pages load stories.css only, not
   viz.css, so .viz-heat-grid's `display: grid` never actually applied
   here -- set it explicitly or grid-template-columns is inert and the
   browser silently falls back to block (single-column) stacking. */
.story-heat-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr)); /* minmax(0, ..): see #370 */
  column-gap: 1.5rem;
  max-width: 100%;
  overflow-x: auto;
}

@media (max-width: 899px) {
  .story-heat-grid {
    grid-template-columns: 1fr;
  }
}

.story-heat-cell {
  width: 8px;
  height: 8px;
  min-width: 8px;
  padding: 0;
  border: none;
  display: inline-block;
}

/* #369: story pages load stories.css only, not viz.css -- these cells
   and legend swatches carry .viz-heat-* / .viz-legend-item classes from
   the shared calendar-heatmap builder (story-days-of-joy.js,
   story-archive-silences.js) but had no color rule anywhere in this
   sheet, so every calendar cell and legend swatch rendered invisible
   (transparent) on every story page. Mirrors src/css/viz.css. */
.viz-heat-has-highlight::before,
.story-heat-cell.viz-heat-has-highlight {
  background: var(--haiti-red, #d21034);
}

.viz-heat-has-events::before,
.story-heat-cell.viz-heat-has-events {
  background: #f4c4ce;
}

.viz-heat-events-low::before,
.story-heat-cell.viz-heat-events-low {
  background: #f4c4ce;
}

.viz-heat-events-mid::before,
.story-heat-cell.viz-heat-events-mid {
  background: #e86882;
}

.viz-heat-events-high::before,
.story-heat-cell.viz-heat-events-high {
  background: var(--haiti-red, #d21034);
}

.viz-heat-empty::before,
.story-heat-cell.viz-heat-empty {
  background: #e8e8e8;
}

.viz-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.viz-legend-item::before {
  content: '';
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 2px;
  border: 1px solid rgba(0, 0, 0, 0.15);
}

.story-heat-cell-dim {
  opacity: 0.35;
}

.story-heat-month-label {
  font-size: 0.65rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.story-heat-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  margin-bottom: 0.75rem;
  font-size: 0.75rem;
}

.story-dept-svg {
  width: 100%;
  max-width: 960px; /* widened again (#371) -- was 520px (#350), from before the pinned-fade
    full-width redesign. A pinned card now runs ~1120px wide; 520px left an obvious dead-space gap
    beside the card's cream background/border (the enclosing box spans the full card, the content
    inside it didn't) -- reported live as content "stuck in the left side" of the card. */
  height: auto;
}

.story-dept-path.story-dept-silent {
  stroke-dasharray: 4 2;
}

.story-indep-figure {
  display: flex; /* was grid -- see #370 note near .story-lang-figure-context */
  flex-direction: column;
  gap: 1rem;
  max-width: 960px; /* widened again (#371) -- was 540px (#350), from before the pinned-fade
    full-width redesign. A pinned card now runs ~1120px wide; 540px left an obvious dead-space gap
    beside the card's cream background/border (the enclosing box spans the full card, the content
    inside it didn't) -- reported live as content "stuck in the left side" of the card. */
}

.story-indep-figure--full {
  gap: 1.25rem;
}

/* #374: this step also stacks the timeline ribbon with a specimens row
   and a headline gallery strip -- at the full 960px width the ribbon
   alone (374-436px) combined with the rest pushed steps to 810-1095px
   against a ~643px pinned-card budget. 750px trims the ribbon's own
   height while staying well above the old 540px dead-space-y cap. */
.story-indep-ribbon {
  max-width: 750px;
}

.story-indep-ribbon--dimmed .story-indep-era-band,
.story-indep-ribbon--dimmed .story-indep-pin {
  opacity: 0.55;
}

.story-indep-ribbon-heading,
.story-indep-gallery-heading,
.story-indep-specimens-heading {
  margin: 0 0 0.35rem;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #555;
}

.story-indep-ribbon-caption {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: #444;
}

.story-indep-ribbon-svg {
  width: 100%;
  height: auto;
}

.story-indep-era-band {
  fill: #f3ebe3;
  opacity: 0.45;
}

.story-indep-era-band.is-active {
  fill: #f8e8e0;
  opacity: 0.95;
  stroke: #8b0000;
  stroke-width: 1;
}

.story-indep-era-band.is-muted {
  opacity: 0.25;
}

.story-indep-era-label {
  fill: #666;
  /* #374: recomputed again for the bespoke 750px cap (scale ~1.97x, not
     ~2.53x) -- see the .story-lang-ribbon-decade note for the full
     explanation of this coupling. */
  font-size: 0.4rem;
  font-family: 'DM Sans', sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.story-indep-axis {
  stroke: #c8bfb4;
  stroke-width: 2;
}

.story-indep-axis-label,
.story-indep-milestone-label,
.story-indep-pin-year {
  fill: #555;
  font-size: 0.43rem; /* #374: recomputed for the 750px cap, see .story-indep-era-label note */
  font-family: 'DM Sans', sans-serif;
}

.story-indep-milestone-line {
  stroke: #d4c4b0;
  stroke-width: 1;
  stroke-dasharray: 3 3;
}

.story-indep-milestone.is-active .story-indep-milestone-line {
  stroke: #8b0000;
  stroke-width: 2;
}

.story-indep-pin circle {
  fill: #8b0000;
}

.story-indep-pin.is-active circle {
  fill: #1a1a1a;
  stroke: #8b0000;
  stroke-width: 2;
}

.story-indep-pin-plate {
  fill: #fffaf5;
  stroke: #e0d8d0;
}

.story-indep-specimens {
  max-width: 960px; /* widened again (#371) -- was 540px (#350), from before the pinned-fade
    full-width redesign. A pinned card now runs ~1120px wide; 540px left an obvious dead-space gap
    beside the card's cream background/border (the enclosing box spans the full card, the content
    inside it didn't) -- reported live as content "stuck in the left side" of the card. */
}

.story-indep-specimen-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.story-indep-specimen {
  display: inline-block;
  padding: 0.35rem 0.65rem;
  border: 1px solid #e0d8d0;
  border-radius: 4px;
  background: #fff;
  font-family: 'Source Serif Pro', serif;
  font-size: 0.8rem;
  color: #444;
}

.story-indep-specimen.is-active {
  border-color: #8b0000;
  background: #fffaf5;
  color: #1a1a1a;
  font-weight: 600;
}

.story-indep-specimen.is-muted {
  opacity: 0.45;
}

.story-indep-disclaimer {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.45;
  color: #555;
}

.story-indep-timeline {
  width: 100%;
  max-width: 960px; /* widened again (#371) -- was 520px (#350), from before the pinned-fade
    full-width redesign. A pinned card now runs ~1120px wide; 520px left an obvious dead-space gap
    beside the card's cream background/border (the enclosing box spans the full card, the content
    inside it didn't) -- reported live as content "stuck in the left side" of the card. */
  height: auto;
}

.story-indep-tick {
  fill: #8b0000;
}

.story-indep-tick.is-active {
  fill: #1a1a1a;
  stroke: #8b0000;
  stroke-width: 2;
}

.story-indep-milestone {
  stroke: #d4c4b0;
  stroke-width: 1;
  stroke-dasharray: 3 3;
}

.story-joy-figure {
  display: flex; /* was grid -- see #370 note near .story-lang-figure-context */
  flex-direction: column;
  gap: 1rem;
  max-width: 960px; /* widened again (#371) -- was 540px (#350), from before the pinned-fade
    full-width redesign. A pinned card now runs ~1120px wide; 540px left an obvious dead-space gap
    beside the card's cream background/border (the enclosing box spans the full card, the content
    inside it didn't) -- reported live as content "stuck in the left side" of the card. */
}

.story-joy-figure--full {
  gap: 1.25rem;
}

/* #374: this story stacks FOUR figure sections every step (ribbon,
   heatmap, specimens, headline gallery) -- unlike most other stories'
   1-2, so even reasonably-sized individual pieces add up past budget
   (1031-1268px measured). Trimmed further than the #371/#373 pass alone
   did; the heatmap's 3-column grid (#369) isn't width-driven the same
   way so isn't re-touched here. */
.story-joy-ribbon {
  max-width: 650px;
}

.story-joy-ribbon--dimmed .story-joy-month-fill {
  opacity: 0.55;
}

.story-joy-ribbon-heading,
.story-joy-gallery-heading,
.story-joy-heatmap-heading,
.story-joy-specimens-heading {
  margin: 0 0 0.35rem;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #555;
}

.story-joy-ribbon-caption,
.story-joy-heatmap-caption {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: #444;
}

.story-joy-ribbon-svg {
  width: 100%;
  height: auto;
}

.story-joy-month-fill {
  fill: #c45c26;
  opacity: 0.75;
}

/* Magnitude tier (same vocabulary as the calendar heatmap below) -- color
   intensity, not tile height, encodes signal count (#298). */
.story-joy-month-fill.viz-heat-events-low {
  fill: #f4c4ce;
}

.story-joy-month-fill.viz-heat-events-mid {
  fill: #e86882;
}

.story-joy-month-fill.viz-heat-events-high {
  fill: var(--haiti-red, #d21034);
}

.story-joy-month-fill.viz-heat-empty {
  fill: #e8e8e8;
}

.story-joy-month-col.is-active .story-joy-month-fill {
  fill: #8b0000;
  opacity: 1;
}

.story-joy-month-col.is-muted .story-joy-month-fill {
  opacity: 0.3;
}

.story-joy-month-col.is-dimmed .story-joy-month-fill {
  opacity: 0.35;
}

.story-joy-month-label {
  fill: #555;
  /* #374: recomputed again for the bespoke 650px cap above (scale
     ~1.71x, not ~2.53x) -- see .story-indep-era-label for the full
     explanation of this coupling. */
  font-size: 0.457rem;
  font-family: 'DM Sans', sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.story-joy-heatmap {
  max-width: 960px; /* widened (#369) -- pinned-fade cards run ~1120px wide now; a
    single-column 12-month stack no longer needs to fit a narrow sidebar */
}

.story-joy-heat-grid--dimmed .story-joy-heat-cell:not(.story-heat-cell-dim) {
  opacity: 0.65;
}

.story-joy-heat-legend {
  margin-bottom: 0.75rem;
}

.story-joy-specimens {
  max-width: 960px; /* widened again (#371) -- was 540px (#350), from before the pinned-fade
    full-width redesign. A pinned card now runs ~1120px wide; 540px left an obvious dead-space gap
    beside the card's cream background/border (the enclosing box spans the full card, the content
    inside it didn't) -- reported live as content "stuck in the left side" of the card. */
}

.story-joy-specimen-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.story-joy-specimen {
  display: inline-block;
  padding: 0.35rem 0.65rem;
  border: 1px solid #e0d8d0;
  border-radius: 4px;
  background: #fff;
  font-family: 'Source Serif Pro', serif;
  font-size: 0.8rem;
  color: #444;
}

.story-joy-specimen.is-active {
  border-color: #8b0000;
  background: #fffaf5;
  color: #1a1a1a;
  font-weight: 600;
}

.story-joy-specimen.is-muted {
  opacity: 0.45;
}

.story-joy-specimen--festival.is-active {
  border-color: #c45c26;
}

.story-joy-specimen--music.is-active {
  border-color: #2a6f97;
}

.story-joy-specimen--sport.is-active {
  border-color: #3d7c47;
}

.story-joy-disclaimer {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.45;
  color: #555;
}

/* Horizontal scroll-snap strip (#369) -- was a single-column vertical
   grid that stacked every spotlight/headline unfiltered, which grows
   every year (independence-day, days-of-joy) and blew past the pinned
   card's viewport budget (days-of-joy hit ~3200px in a ~1264px
   viewport). Same strip pattern already proven by
   .story-lang-headline-strip / .story-women-headline-strip: bounded
   height regardless of card count, no data loss (scroll for the rest),
   and puts the "overview" gallery's horizontal breadth to use instead
   of unbounded vertical stacking. */
.story-headline-gallery {
  display: flex;
  gap: 0.75rem;
  overflow-x: auto;
  padding-bottom: 0.5rem;
  scroll-snap-type: x mandatory;
  max-width: 100%;
}

.story-headline-card {
  flex: 0 0 220px;
  scroll-snap-align: start;
  border: 1px solid #e0d8d0;
  border-radius: 6px;
  padding: 0.75rem 1rem;
  background: #fffaf5;
}

.story-headline-card.is-active {
  border-color: #8b0000;
  box-shadow: 0 0 0 1px rgba(139, 0, 0, 0.15);
}

.story-headline-card.is-muted {
  opacity: 0.5;
}

.story-headline-year {
  margin: 0 0 0.25rem;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #8b0000;
}

.story-headline-title {
  margin: 0 0 0.35rem;
  font-size: 0.95rem;
  line-height: 1.4;
}

.story-headline-meta {
  margin: 0;
  font-size: 0.75rem;
  color: #666;
}

.story-headline-empty {
  font-size: 0.9rem;
  color: #555;
}

/* #374: bespoke, deliberately narrow -- .story-debt-ribbon's own viewBox
   (340x362) is TALLER than wide by design (a *vertical* timeline, unlike
   the horizontal area-ribbons every other -ribbons class widened by #371
   is). Widening a chart that's already taller-than-wide just makes it
   proportionally MORE tall: at 960px this rendered ~1022px tall (960 *
   362/340), pushing steps that stack it with marginalia/disclaimer text
   past 1200-1800px. A vertical timeline doesn't need much width to begin
   with -- 330px keeps it near its native ~1:1 scale (351px tall) instead
   of stretching it to fill space that a tall-and-narrow chart was never
   designed to use. */
.story-debt-ribbon {
  max-width: 330px;
}

.story-debt-ribbon--dimmed .story-debt-ribbon-band {
  opacity: 0.45;
}

.story-debt-ribbon-heading,
.story-debt-era-heading,
.story-debt-panel-title,
.story-debt-marginalia-heading {
  margin: 0 0 0.35rem;
  font-size: 0.95rem;
  font-weight: 700;
  font-family: 'DM Sans', sans-serif;
}

.story-debt-ribbon-caption,
.story-debt-era-caption,
.story-debt-panel-caption,
.story-debt-disclaimer {
  margin: 0 0 0.75rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: #555;
}

.story-debt-ribbon-track {
  fill: #f3ebe3;
  stroke: #d4c4b0;
  stroke-width: 1;
}

.story-debt-ribbon-segment {
  fill: #8b0000;
  opacity: 0.85;
}

.story-debt-ribbon-band.is-dimmed .story-debt-ribbon-segment,
.story-debt-ribbon-band:not(.is-active) .story-debt-ribbon-segment {
  opacity: 0.35;
}

.story-debt-ribbon-band.is-highlight .story-debt-ribbon-segment,
.story-debt-ribbon-band.is-active .story-debt-ribbon-segment {
  opacity: 1;
}

.story-debt-ribbon-connector {
  stroke: #c8bfb4;
  stroke-width: 1;
}

/* #373: .story-debt-ribbon (this text's container) was widened 520px ->
   960px by #371 -- same scale-mismatch fix as .story-lang-ribbon-decade.
   .story-debt-era-label/-total live in a *different* figure
   (.story-debt-era-sparklines, its own <figure>, not inside
   .story-debt-ribbon) that #371 never touched -- left alone here
   pending its own check, not assumed fine or assumed broken. */
/* #374: .story-debt-ribbon's bespoke 330px cap (above) lands its scale
   factor near 1:1 (330/340), so these can just be normal-looking rem
   values again instead of the tiny #373-style local sizes every wider
   ribbon needs. */
/* #374: keep local font-size close to the ORIGINAL 0.65rem/0.7rem, not
   scaled up much -- a bigger local size makes the label text occupy more
   of the ribbon's fixed internal label column (in viewBox units), which
   clips long labels ("Haitian independence proclaimed...") against the
   viewBox edge. Confirmed live: 250px + a proportionally larger local
   font-size clipped every label; 330px + a modest bump does not. */
.story-debt-ribbon-year,
.story-debt-ribbon-label,
.story-debt-ribbon-ledger,
.story-debt-ribbon-axis {
  fill: #444;
  font-size: 0.8rem;
  font-family: 'DM Sans', sans-serif;
}

/* #373: .story-debt-era-svg has always been width:100% with no max-width
   cap on its containing <figure> (.story-debt-era-sparklines) -- unlike
   the other fixes above, this one predates today's #371 change (measured
   live: ~3.3x scale, effectively full card width already). Same fix
   pattern regardless: target a rendered size close to the surrounding
   caption text (13.6px), not the local number alone. */
.story-debt-era-label,
.story-debt-era-total {
  fill: #444;
  font-size: 0.5rem; /* recomputed for the 480px cap above (scale ~1.5x, not ~3.3x) */
  font-family: 'DM Sans', sans-serif;
}

.story-debt-ribbon-label {
  font-weight: 600;
  font-size: 0.85rem;
}

/* #374: never had a max-width cap at all (unlike every other -svg class),
   so .story-debt-estimate-sparkline's width:100% scaled to the full
   ~1120px card. This chart's viewBox (340x140) is much wider than tall,
   so widening it doesn't explode its height the way the vertical ribbon
   above does -- but this step stacks it with the ribbon, and both
   together still need a budget. */
.story-debt-historiography-panel {
  max-width: 480px;
}

/* #374: same story as .story-debt-historiography-panel above --
   .story-debt-era-svg (width:100%) had no cap on its containing figure,
   scaling to the full card width and, with it, this chart's height
   (viewBox 320x232-ish depending on era count). */
.story-debt-era-sparklines {
  max-width: 480px;
}

.story-debt-figure-split,
.story-debt-split-panel,
.story-debt-figure-disclaimer {
  display: flex; /* was grid -- see #370 note near .story-lang-figure-context */
  flex-direction: column;
  gap: 1rem;
  max-width: 960px; /* widened again (#371) -- was 520px (#350), from before the pinned-fade
    full-width redesign. A pinned card now runs ~1120px wide; 520px left an obvious dead-space gap
    beside the card's cream background/border (the enclosing box spans the full card, the content
    inside it didn't) -- reported live as content "stuck in the left side" of the card. */
}

.story-debt-source-divider {
  margin: 0.5rem 0;
  padding: 0.35rem 0;
  border-top: 2px solid #8b0000;
  border-bottom: 1px solid #e0d8d0;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #8b0000;
}

.story-debt-estimate-line,
.story-debt-era-line {
  stroke: #1a4d6d;
  stroke-width: 2;
  fill: none;
}

.story-debt-estimate-point {
  fill: #1a4d6d;
}

.story-debt-panel-source {
  margin: 0.5rem 0 0;
  font-size: 0.75rem;
  color: #666;
}

.story-debt-marginalia-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex; /* was grid -- see #370 note near .story-lang-figure-context */
  flex-direction: column;
  gap: 0.5rem;
}

.story-debt-marginalia-item {
  border-left: 3px solid #d4c4b0;
  padding: 0.35rem 0 0.35rem 0.65rem;
  font-size: 0.8rem;
  line-height: 1.35;
}

.story-debt-marginalia-item.is-active {
  border-left-color: #8b0000;
}

.story-debt-marginalia-year {
  display: block;
  font-weight: 700;
  font-size: 0.7rem;
  color: #8b0000;
}

.story-debt-marginalia-title {
  display: block;
}

.story-debt-marginalia-themes {
  display: block;
  font-size: 0.7rem;
  color: #777;
}

.story-debt-marginalia-empty {
  font-size: 0.85rem;
  color: #555;
}

.story-debt-era-spark.is-active .story-debt-era-line {
  stroke: #8b0000;
}

/* Women in the pages (#215) — role facet area ribbons */
/* #374: bespoke, deliberately NOT 960px like every other -ribbons class.
   This story stacks 4 role ribbons (subject/author/advertiser/organizer),
   not 2 like languages-of-power -- viewHeight = roles.length*62+8 = 256,
   nearly SQUARE against the 280-wide viewBox. Since the SVG is width:100%;
   height:auto (aspect-ratio preserved), widening it the same way as every
   other ribbon exploded its rendered height too: 960px wide -> ~878-921px
   TALL, pushing steps to 1327-1570px in a ~1264px viewport (#369 all over
   again, this time caused by the #371 fix itself). 700px keeps the SVG
   under ~640px tall while still using much more width than the old 520px
   cap -- a real width/height trade-off other -ribbons stories with fewer
   stacked rows don't have to make. */
.story-women-ribbons {
  max-width: 700px;
}

/* #375: the "all 4 roles shown" variant (steps 0 and 4) used to stack
   all 4 ribbons in one column (viewBox 280x256, nearly square), which
   forced a width/height trade-off: widen it to match the card (#371)
   and it got too tall (#369); narrow it to fix the height (#374) and it
   left dead space next to a full-width card (#371 all over again). A
   2x2 grid (see buildRoleFacetRibbonsSvg) makes the same 4 ribbons
   560x132 -- wide-and-short instead of narrow-and-tall -- so it can
   share the base class's 700px cap with no trade-off either way. */
.story-women-ribbons--full {
  max-width: 700px;
}

/* Scale at 700px / 560 viewBox units = 1.25x -- much gentler than the
   base (zoomed) variant's 2.1875x, so these render a bit larger per
   local unit for the same on-screen size. */
.story-women-ribbons--full .story-women-ribbon-decade {
  font-size: 0.44rem;
}

.story-women-ribbons--full .story-women-ribbon-label,
.story-women-ribbons--full .story-women-ribbon-share {
  font-size: 0.54rem;
}

.story-women-ribbons--dimmed .story-women-ribbon {
  opacity: 0.45;
}

.story-women-ribbons-heading,
.story-women-enrollment-heading {
  margin: 0 0 0.35rem;
  font-size: 0.95rem;
  font-weight: 700;
  font-family: 'DM Sans', sans-serif;
}

.story-women-ribbons-caption,
.story-women-enrollment-caption,
.story-women-disclaimer {
  margin: 0 0 0.75rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: #555;
}

.story-women-ribbon-area {
  opacity: 0.35;
}

.story-women-ribbon.is-active .story-women-ribbon-area,
.story-women-ribbon.is-zoomed .story-women-ribbon-area {
  opacity: 0.55;
}

.story-women-ribbon.is-dimmed .story-women-ribbon-area {
  opacity: 0.2;
}

.story-women-ribbon-line {
  stroke-width: 1.5;
}

/* #373: same fix, same reason, as .story-lang-ribbon-decade -- .story-
   women-ribbons uses the identical 280-unit viewBox and was widened by
   the same #371 change (520px -> 960px), so it has the exact same scale
   mismatch. See .story-lang-ribbon-decade's comment for the full
   explanation. */
/* #374: local sizes recomputed for the bespoke 700px cap above (scale
   ~2.5x, not the ~3.43x every other -ribbons class uses at 960px) -- see
   the .story-lang-ribbon-decade note for why this coupling exists. */
.story-women-ribbon-label,
.story-women-ribbon-share,
.story-women-ribbon-decade {
  fill: #444;
  font-size: 0.25rem;
  font-family: 'DM Sans', sans-serif;
}

.story-women-ribbon-label,
.story-women-ribbon-share {
  font-size: 0.31rem;
}

.story-women-ribbon-label {
  font-weight: 600;
}

.story-women-figure-split,
.story-women-figure-context {
  display: flex; /* was grid -- see #370 note near .story-lang-figure-context */
  flex-direction: column;
  gap: 1rem;
  max-width: 960px; /* widened again (#371) -- was 520px (#350), from before the pinned-fade
    full-width redesign. A pinned card now runs ~1120px wide; 520px left an obvious dead-space gap
    beside the card's cream background/border (the enclosing box spans the full card, the content
    inside it didn't) -- reported live as content "stuck in the left side" of the card. */
}

.story-women-specimens {
  display: flex; /* was grid -- see #370 note near .story-lang-figure-context */
  flex-direction: column;
  gap: 0.75rem;
}

.story-women-specimen {
  margin: 0;
  border-left: 3px solid #8b0000;
  padding: 0.5rem 0 0.5rem 0.75rem;
  background: #fffaf5;
}

.story-women-specimen--uncertain {
  border-left-color: #b7791f;
}

.story-women-specimen-type {
  margin: 0 0 0.25rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #8b0000;
}

.story-women-specimen-title {
  margin: 0 0 0.35rem;
  font-family: 'Source Serif Pro', serif;
  font-size: 0.95rem;
  line-height: 1.4;
}

.story-women-specimen-meta {
  margin: 0;
  font-size: 0.75rem;
  color: #666;
}

/* Larger typographic specimens on mobile (#260 review P2 -- small serif quotes were
   hard to read at phone width even though the grid itself already stacks to one column). */
@media (max-width: 600px) {
  .story-women-specimens {
    gap: 1rem;
  }

  .story-women-specimen-title {
    font-size: 1.1rem;
    line-height: 1.45;
  }

  .story-women-specimen-meta {
    font-size: 0.82rem;
  }
}

.story-women-ocr-badge {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0.1rem 0.35rem;
  border-radius: 3px;
  background: #fef3c7;
  color: #92400e;
  font-size: 0.65rem;
  font-weight: 600;
  text-transform: uppercase;
}

.story-women-headline-collage {
  margin-top: 1rem;
  overflow-x: auto;
}

.story-women-headline-strip {
  display: flex;
  gap: 0.75rem;
  padding-bottom: 0.5rem;
  min-width: min-content;
}

.story-women-headline-card {
  flex: 0 0 180px;
  border: 1px solid #e0d8d0;
  border-radius: 6px;
  padding: 0.75rem;
  background: #fffaf5;
}

.story-women-headline-year {
  margin: 0 0 0.25rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #8b0000;
}

.story-women-headline-title {
  margin: 0;
  font-family: 'Source Serif Pro', serif;
  font-size: 0.85rem;
  line-height: 1.35;
}

.story-women-spotlights {
  display: flex; /* was grid -- see #370 note near .story-lang-figure-context */
  flex-direction: column;
  gap: 0.65rem;
}

.story-women-spotlight-card {
  border: 1px solid #e0d8d0;
  border-radius: 6px;
  padding: 0.65rem 0.85rem;
  background: #fff;
}

.story-women-spotlight-title {
  margin: 0 0 0.25rem;
  font-size: 0.9rem;
  line-height: 1.35;
}

.story-women-spotlight-meta {
  margin: 0;
  font-size: 0.75rem;
  color: #666;
}

.story-women-source-divider {
  margin: 0.75rem 0;
  font-size: 0.75rem;
  color: #888;
  text-align: center;
}

.story-women-enrollment-footnote {
  border: 1px dashed #d4c4b0;
  border-radius: 6px;
  padding: 0.75rem 1rem;
  background: #faf8f5;
}

.story-women-enrollment-stat {
  margin: 0 0 0.5rem;
  font-size: 0.9rem;
}

.story-women-enrollment-value {
  margin-left: 0.35rem;
  font-weight: 700;
  color: #8b0000;
}

.story-women-enrollment-period {
  margin-left: 0.25rem;
  font-size: 0.8rem;
  color: #666;
}

.story-women-enrollment-source {
  margin: 0;
  font-size: 0.75rem;
  color: #666;
}

@media (prefers-reduced-motion: reduce) {
  .scrolly-step {
    transition: none;
  }
}

/* U.S. occupation lane ribbon (#214) */
/* #374: .story-occupation-ribbon-svg (width:100%) never had a max-width
   cap at all -- unlike #371/#373's stories, this predates today's session
   entirely. Rendered at the full ~1077px card width, its 360x208 viewBox
   (a legitimately wide 4-lane ribbon, not a vertical timeline) still
   produced a ~622px-tall SVG alone, before any accompanying spotlight
   cards/specimens stack on top. */
.story-occupation-ribbon {
  margin: 0;
  max-width: 700px;
}

.story-occupation-ribbon--dimmed .story-occupation-lane.is-active {
  opacity: 0.65;
}

.story-occupation-ribbon-heading,
.story-occupation-spotlights-heading {
  margin: 0 0 0.35rem;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #1a1a1a;
}

.story-occupation-ribbon-caption {
  margin: 0 0 0.75rem;
  font-size: 0.8rem;
  line-height: 1.4;
  color: #555;
}

/* #374: recomputed for the 700px cap above (scale ~1.94x, was ~2.99x at
   full width) -- see .story-lang-ribbon-decade for why this coupling
   exists. */
.story-occupation-axis-label,
.story-occupation-lane-label,
.story-occupation-milestone-label {
  font-family: 'DM Sans', sans-serif;
  font-size: 0.42rem;
  fill: #555;
}

.story-occupation-lane-label {
  font-weight: 600;
}

.story-occupation-lane-track {
  stroke: #d4c4b0;
  stroke-width: 2;
}

.story-occupation-lane.is-active .story-occupation-lane-track {
  stroke: #1a4d6d;
  stroke-width: 3;
}

.story-occupation-lane.is-dimmed .story-occupation-lane-track,
.story-occupation-lane.is-muted .story-occupation-lane-track {
  stroke: #e8e0d8;
}

.story-occupation-tick {
  stroke: #1a4d6d;
  stroke-width: 2;
}

.story-occupation-tick.is-active {
  stroke: #8b0000;
  stroke-width: 3;
}

.story-occupation-tick.is-dimmed {
  stroke: #c8bfb4;
  stroke-width: 1;
}

.story-occupation-tick.is-highlight {
  stroke: #8b0000;
  stroke-width: 4;
}

.story-occupation-milestone line {
  stroke: #999;
  stroke-dasharray: 3 3;
  stroke-width: 1;
}

.story-occupation-milestone.is-withdrawal line {
  stroke: #8b0000;
  stroke-dasharray: none;
  stroke-width: 2;
}

.story-occupation-figure-split {
  display: flex; /* was grid -- see #370 note near .story-lang-figure-context */
  flex-direction: column;
  gap: 1rem;
}

.story-occupation-spotlight-grid {
  display: flex; /* was grid -- see #370 note near .story-lang-figure-context */
  flex-direction: column;
  gap: 0.75rem;
}

.story-occupation-spotlight-card {
  border-left: 4px solid #8b0000;
  padding: 0.35rem 0 0.35rem 0.75rem;
}

.story-occupation-spotlight-year {
  margin: 0;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #8b0000;
}

.story-occupation-spotlight-quote {
  margin: 0.25rem 0;
  font-family: 'Source Serif Pro', serif;
  font-size: 1rem;
  line-height: 1.35;
  color: #1a1a1a;
}

.story-occupation-spotlight-meta {
  margin: 0;
  font-size: 0.7rem;
  color: #777;
}

.story-occupation-spotlight-empty,
.story-occupation-late-note {
  margin: 0.75rem 0 0;
  font-size: 0.8rem;
  line-height: 1.4;
  color: #555;
}

.story-occupation-late-note {
  border-top: 1px solid #e0d8d0;
  padding-top: 0.75rem;
  font-style: italic;
}

/* Haiti and the world attention flow ribbon (#269) */
.story-world-ribbon {
  margin: 0;
}

.story-world-ribbon--dimmed .story-world-flow.is-active {
  opacity: 0.55;
}

.story-world-ribbon-heading {
  margin: 0 0 0.35rem;
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.story-world-ribbon-caption,
.story-world-disclaimer {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  color: #5c534c;
  line-height: 1.45;
}

.story-world-disclaimer {
  border-top: 1px solid #e0d8d0;
  padding-top: 0.75rem;
  font-style: italic;
}

/* #374: viewBox (400x216) is wide-not-tall, but this step is the ONLY
   content on the step (no accompanying text/list), so the map alone at
   960px (518px tall) plus figcaption/heading was still enough to exceed
   budget across all 5 steps. 650px trims it further than the #371/#373
   pass alone did. */
.story-world-ribbon-svg {
  display: block;
  width: 100%;
  max-width: 650px;
  height: auto;
}

.story-world-map-bg {
  fill: #e8eef5;
}

/* Real country silhouettes (#299) -- replaces the flat rect that used to be
   the entire "map." Choropleth-filled by mention count via choroplethFill(),
   same color scale as the archive department map. */
.story-world-country {
  stroke: #ffffff;
  stroke-width: 0.6;
  transition: fill 0.2s ease;
}

.story-world-country--haiti {
  stroke: #ffffff;
  stroke-width: 1;
}

.story-world-flow-ribbon {
  fill: none;
  stroke: #3d5a80;
  stroke-linecap: round;
  opacity: 0.85;
}

.story-world-flow.is-active .story-world-flow-ribbon {
  stroke: #1b3a57;
  opacity: 1;
}

.story-world-flow.is-dimmed .story-world-flow-ribbon {
  stroke: #9aa8b8;
  opacity: 0.35;
}

.story-world-flow.is-muted .story-world-flow-ribbon {
  opacity: 0.5;
}

.story-world-place-dot,
.story-world-origin-dot {
  fill: #c45c3e;
}

.story-world-origin-dot {
  fill: #1b3a57;
}

.story-world-place-label,
.story-world-origin-label {
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  /* #374: recomputed again for the 650px cap above (scale ~1.63x) --
     see the .story-lang-ribbon-decade note for the full explanation. */
  font-size: 7.75px;
  fill: #3d342c;
}

.story-world-origin-label {
  font-weight: 600;
}

.story-world-decade-axis {
  stroke: #c8bfb4;
  stroke-width: 1;
}

.story-world-decade-tick circle {
  fill: #3d5a80;
  opacity: 0.7;
}

.story-world-decade-tick.is-active circle {
  fill: #c45c3e;
  opacity: 1;
}

.story-world-decade-tick.is-dimmed circle {
  fill: #b8afa4;
  opacity: 0.4;
}

.story-world-decade-tick text {
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 8px;
  fill: #5c534c;
}

.story-world-figure-full {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

@media (prefers-reduced-motion: reduce) {
  .story-world-flow-ribbon {
    transition: none;
  }
}

/* Marketplace inflation dual-track ribbon (#254) */
/* #374: .story-market-ribbon-svg (width:100%) never had a max-width cap
   at all -- pre-existing, unrelated to #371. Rendered at the full
   ~1077px card width, viewBox 380x296 (nearly square) alone hit ~839px
   tall, before any accompanying decade-density list. */
.story-market-ribbon {
  margin: 0;
  max-width: 450px;
}

.story-market-ribbon--dimmed .story-market-lane.is-active {
  opacity: 0.55;
}

.story-market-ribbon-heading,
.story-market-decade-heading {
  margin: 0 0 0.35rem;
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.95rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.story-market-ribbon-caption,
.story-market-ribbon-gap-note,
.story-market-disclaimer {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: #444;
}

.story-market-ribbon-gap-note {
  font-style: italic;
  color: #666;
}

.story-market-axis-label,
.story-market-lane-label,
.story-market-lane-scale,
.story-market-gap-label,
.story-market-series-note {
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.68rem;
  fill: #555;
}

.story-market-lane-label {
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.story-market-lane-track {
  stroke: #d8cfc5;
  stroke-width: 2;
}

.story-market-lane.is-pending .story-market-lane-track {
  stroke-dasharray: 4 4;
  opacity: 0.55;
}

.story-market-lane.is-active .story-market-lane-track {
  stroke: #8a6f58;
}

.story-market-gap-line {
  stroke: #b8a99a;
  stroke-width: 1;
}

.story-market-ad-marker {
  fill: #c45c26;
  stroke: #fff;
  stroke-width: 1;
}

.story-market-ad-marker--a {
  fill: #a83b12;
}

.story-market-ad-marker--b {
  fill: #c45c26;
  opacity: 0.85;
}

.story-market-ad-marker--c {
  fill: #d9a066;
  opacity: 0.75;
}

.story-market-ipc-line {
  stroke: #2f5d50;
  stroke-width: 2.5;
}

.story-market-fx-line {
  stroke: #3d4f7a;
  stroke-width: 2.5;
}

.story-market-decade-density {
  margin-top: 1rem;
  padding-top: 0.75rem;
  border-top: 1px solid #e0d8d0;
}

.story-market-decade-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.story-market-decade-row {
  display: grid;
  grid-template-columns: 3.5rem 1fr 2rem;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.35rem;
  font-size: 0.8rem;
}

/* Discrete 3-dot tier, not a proportional bar (#298, applied to the origin story per #301). */
.story-market-decade-dots {
  display: flex;
  gap: 0.3rem;
}

.story-market-decade-dot {
  display: block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: #ecd9c8;
}

.story-market-decade-dot.is-filled {
  background: #c45c26;
}

.story-market-figure-full {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* Inflation lenses lens filmstrip (#276 rebuild, #300 differentiation) --
   hollow archive-decade lenses (tercile-tiered radius) in a horizontal row,
   a dashed gap divider, then solid modern-era MEF/BRH lens markers -- not
   the parallel-lane ribbon shared with macro-context/brh-fx/
   marketplace-inflation. */
/* #374: .story-inflation-lens-svg never had a max-width cap (pre-existing,
   unrelated to #371) -- capped for a modest height reduction on the
   step that also carries a decade-density list. */
.story-inflation-ribbon {
  margin: 0;
  max-width: 750px;
}

.story-inflation-ribbon--dimmed .story-inflation-lens.is-active {
  opacity: 0.55;
}

.story-inflation-ribbon-heading,
.story-inflation-decade-heading {
  margin: 0 0 0.35rem;
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.95rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.story-inflation-ribbon-caption,
.story-inflation-ribbon-gap-note,
.story-inflation-citation {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: #444;
}

.story-inflation-ribbon-gap-note,
.story-inflation-citation {
  font-style: italic;
  color: #666;
}

.story-inflation-axis-label,
/* #374: recomputed for the 750px cap above (scale ~1.34x, was ~1.92x
   uncapped) -- see .story-lang-ribbon-decade for the coupling reason. */
.story-inflation-lens-label,
.story-inflation-gap-label {
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.6rem;
  fill: #555;
}

.story-inflation-lens-svg {
  display: block;
  width: 100%;
  height: auto;
}

.story-inflation-lens-baseline {
  stroke: #e0d8d0;
  stroke-width: 1;
}

.story-inflation-gap-line {
  stroke: #b8a99a;
  stroke-width: 1;
}

.story-inflation-lens-ring {
  fill: none;
  stroke: #5c4a3a;
  stroke-width: 2;
}

.story-inflation-lens.is-dimmed .story-inflation-lens-ring {
  opacity: 0.35;
}

.story-inflation-lens-modern circle {
  stroke: #fff;
  stroke-width: 1;
}

.story-inflation-lens-modern--mef circle {
  fill: #2f5d50;
}

.story-inflation-lens-modern--brh circle {
  fill: #3d4f7a;
}

.story-inflation-lens-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0 0 0.5rem;
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.7rem;
  color: #444;
}

.story-inflation-lens-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.story-inflation-lens-legend-swatch {
  display: inline-block;
  width: 0.65rem;
  height: 0.65rem;
  border-radius: 50%;
}

.story-inflation-legend--archive .story-inflation-lens-legend-swatch {
  background: transparent;
  border: 2px solid #5c4a3a;
}

.story-inflation-legend--mef .story-inflation-lens-legend-swatch {
  background: #2f5d50;
}

.story-inflation-legend--brh .story-inflation-lens-legend-swatch {
  background: #3d4f7a;
}

.story-inflation-decade-density {
  margin-top: 1rem;
  padding-top: 0.75rem;
  border-top: 1px solid #e0d8d0;
}

.story-inflation-decade-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.story-inflation-decade-row {
  display: grid;
  grid-template-columns: 3.5rem 1fr 2rem;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.35rem;
  font-size: 0.8rem;
}

/* Discrete 3-dot tier, not a proportional bar (#298). */
.story-inflation-decade-dots {
  display: flex;
  gap: 0.3rem;
}

.story-inflation-decade-dot {
  display: block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: #e0d6cc;
}

.story-inflation-decade-dot.is-filled {
  background: #5c4a3a;
}

.story-inflation-figure-full {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* BRH FX depreciation coins (#277 rebuild, #300 differentiation) -- one
   solid coin per decade shrinking with the HTG/USD rate, archive signal as
   tercile-tiered dots above -- not the parallel-lane ribbon shared with
   macro-context/inflation-lenses/marketplace-inflation. */
/* #374: .story-brh-fx-coin-svg never had a max-width cap (pre-existing,
   unrelated to #371) -- capped for a modest height reduction on the step
   that also carries a decade-density list. */
.story-brh-fx-ribbon {
  margin: 0;
  max-width: 750px;
}

.story-brh-fx-ribbon--dimmed .story-brh-fx-coin,
.story-brh-fx-ribbon--dimmed .story-brh-fx-archive-dot {
  opacity: 0.55;
}

.story-brh-fx-ribbon-heading,
.story-brh-fx-decade-heading {
  margin: 0 0 0.35rem;
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.95rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.story-brh-fx-ribbon-caption,
.story-brh-fx-ribbon-gap-note,
.story-brh-fx-citation {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: #444;
}

.story-brh-fx-ribbon-gap-note,
.story-brh-fx-citation {
  font-style: italic;
  color: #666;
}

/* #374: recomputed for the 750px cap on .story-brh-fx-ribbon above
   (scale ~1.44x, was ~2.07x uncapped) -- see .story-lang-ribbon-decade
   for the coupling reason. */
.story-brh-fx-axis-label,
.story-brh-fx-coin-label,
.story-brh-fx-peg-divider text {
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.56rem;
  fill: #555;
}

.story-brh-fx-coin-svg {
  display: block;
  width: 100%;
  height: auto;
}

.story-brh-fx-coin-baseline {
  stroke: #e0d8d0;
  stroke-width: 1;
}

.story-brh-fx-peg-divider line {
  stroke: #b8a99a;
  stroke-width: 1;
}

.story-brh-fx-archive-dot {
  fill: #3d4f7a;
  stroke: #fff;
  stroke-width: 1;
}

.story-brh-fx-archive-dot.is-dimmed {
  opacity: 0.4;
}

.story-brh-fx-coin circle {
  fill: #2f5d50;
  stroke: #fff;
  stroke-width: 1;
}

.story-brh-fx-coin-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0 0 0.5rem;
  font-family: var(--font-sans, 'DM Sans', sans-serif);
  font-size: 0.7rem;
  color: #444;
}

.story-brh-fx-coin-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.story-brh-fx-coin-legend-swatch {
  display: inline-block;
  width: 0.65rem;
  height: 0.65rem;
  border-radius: 50%;
}

.story-brh-fx-legend--archive .story-brh-fx-coin-legend-swatch {
  background: #3d4f7a;
}

.story-brh-fx-legend--fx .story-brh-fx-coin-legend-swatch {
  background: #2f5d50;
}

.story-brh-fx-decade-density {
  margin-top: 1rem;
  padding-top: 0.75rem;
  border-top: 1px solid #e0d8d0;
}

.story-brh-fx-decade-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.story-brh-fx-decade-row {
  display: grid;
  grid-template-columns: 3.5rem 1fr 2rem;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.35rem;
  font-size: 0.8rem;
}

/* Discrete 3-dot tier, not a proportional bar (#298). */
.story-brh-fx-decade-dots {
  display: flex;
  gap: 0.3rem;
}

.story-brh-fx-decade-dot {
  display: block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: #ccd2e2;
}

.story-brh-fx-decade-dot.is-filled {
  background: #3d4f7a;
}

.story-brh-fx-figure-full {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

@media (prefers-reduced-motion: reduce) {
  .scrolly-step {
    transition: none;
  }

  .story-quarter-fill {
    transition: none;
  }
}
