/* ============================================================
   Journey Map Kit -- Starter Styles
   ------------------------------------------------------------
   Tokens, shared layout, and the components that travel across
   every artifact built from the kit. Project-specific styles
   should live in your project's own stylesheet, not here.
   ============================================================ */


/* ---------- Reset + base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  scroll-behavior: smooth;
  /* Anchor jumps land below the sticky top bar (rather than under it). */
  scroll-padding-top: var(--top-bar-height);
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}


/* ---------- Brand typography ----------
   Ember Modern Display + Ember Modern Text, Amazon's current
   canonical brand fonts (per design.amazon.com/styleguide brand
   expression guidelines). Self-hosted from the kit's fonts/
   folder so the kit works offline, behind corp-network blocks,
   and on any internal Amazon hosting (Protozoa, Harmony) without
   external CDN dependencies.

   The fallback chain is descending brand alignment:
   1. Ember Modern Display/Text (canonical, self-hosted)
   2. Amazon Ember Display/Ember (older Amazon brand, cdnfonts mirror)
   3. System font (last resort)

   The cdnfonts @import is a graceful fallback only. If Ember
   Modern files load (which they should, being local), cdnfonts
   styles never apply visually. If local fonts fail (corruption,
   404, parsing issue), older Amazon Ember loads from cdnfonts.
   If both fail (corp network block on cdnfonts.com, complete
   offline), system fonts catch everything.

   font-display: swap means the page renders immediately with the
   system-font fallback below, then swaps to Ember Modern when
   the local fonts load. format("opentype") is correct for .otf
   files; browsers handle them natively at slight size cost vs
   .woff2 (acceptable until we convert).

   --font-body: Ember Modern Text for paragraphs, UI, nav.
   --font-display: Ember Modern Display for hero and large
   headlines. Display is optically tuned for large sizes; using
   it on body text would make small copy feel anemic.

   The Amazon brand guideline (per the typography style guide)
   makes the Display vs Text distinction explicit -- Display
   for headlines and subheads, Text for body copy.
   ============================================================ */
@import url("https://fonts.cdnfonts.com/css/amazon-ember");

@font-face {
  font-family: "Ember Modern Display";
  src: url("fonts/EmberModernDisplay-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Ember Modern Display";
  src: url("fonts/EmberModernDisplay-Bold.otf") format("opentype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Ember Modern Text";
  src: url("fonts/EmberModernText-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Ember Modern Text";
  src: url("fonts/EmberModernText-Italic.otf") format("opentype");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Ember Modern Text";
  src: url("fonts/EmberModernText-Bold.otf") format("opentype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Ember Modern Text";
  src: url("fonts/EmberModernText-BoldItalic.otf") format("opentype");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

:root {
  --font-body: "Ember Modern Text", "Amazon Ember", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --font-display: "Ember Modern Display", "Amazon Ember Display", "Ember Modern Text", "Amazon Ember", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}

/* v0.8 typography note. The CS UX brand uses thin/light display
   weights for an airy presentation feel. The kit experimented
   with bringing those weights into hero/numeral surfaces but
   reverted to v0.7 weights for two reasons: (1) Ember Modern as
   shipped exposes only Regular/Bold, so Light fell back silently
   to Regular; (2) the airy CS-brand feel is achieved more
   reliably here through color, atmosphere, and surface tint
   (the new bg cast, the deep-teal accent, the dark-bloom
   retune) than through type weight on a face that wasn't
   designed for it. If a future revision adds a Light-weight
   face (Amazon Ember Light from cdnfonts is on the fallback
   chain), this is where to revisit. */


body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  /* Brand-correct kerning + ligatures + contextual alternates.
     Ember Modern's foundry-supplied kerning tables produce
     better results than the browser's default character-pair
     spacing. Per Amazon brand guidelines, optical kerning is
     suggested on headlines; setting at body level so it applies
     everywhere. "kern" enables pair kerning, "liga" enables
     standard ligatures (fi, fl), "calt" enables contextual
     alternates (Ember Modern uses these for letter combinations
     that need slight glyph adjustment). */
  font-kerning: normal;
  font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
  /* Modern CSS line-breaking discipline. Browsers that support
     text-wrap will avoid orphan words on the last line of every
     paragraph -- a kit-wide typographic standard so no single
     word ever hangs alone at the end of a paragraph. Older
     browsers fall through gracefully (text-wrap is unknown,
     CSS-cascade ignores it).
     "pretty" tunes the algorithm for body text: prevents orphans
     while preserving readable line lengths. Headlines get
     "balance" via the rule below for evenly distributed lines. */
  text-wrap: pretty;
}
/* Headlines use balance for evenly distributed wrap (h1-h3 are
   typically 1-3 lines; balance produces visually weighted line
   lengths). Pretty would be fine but balance is calibrated for
   shorter type. */
h1, h2, h3 {
  text-wrap: balance;
}

/* ---------- Link baseline ----------
   Kit-wide rule for inline prose links so they read as readable
   and on-theme in both light and dark modes. Without this, browser
   default `#0000EE` blue lands against the `#14161a` dark bg and
   fails contrast badly; saturated dark blue against off-white in
   light mode squeaks by but reads as legacy web chrome.

   Specificity discipline: wrapped in :where() so the entire
   baseline has ZERO specificity. Any class-level rule
   (`.nav-link { color: var(--ink-soft) }`, `.kit-rail-link`,
   `.sh-gallery-card`, etc.) automatically wins. Without :where(),
   `a:visited { color: ... }` has the same specificity as
   `.nav-link { color: ... }` and beats it via later source order
   -- which is what made every visited nav link turn accent blue.

   Approach:
   - Color resolves to var(--accent) so the link tints with the
     theme: dark navy in light mode (~7:1 contrast), light blue in
     dark mode (~9:1 contrast). Both pass WCAG AA for body text.
   - Subtle underline at rest with offset so descenders (g, p, y)
     don't get bisected. Underline brightens on hover/focus.
   - Visited matches unvisited -- the kit isn't archive content
     where "I've read this" carries meaning; the purple noise hurts
     the prose more than it helps.
   - Components with their own link styling keep their treatment
     because their class-based specificity (0,1,0) beats the
     :where() baseline (0,0,0).
   - Standalone components that DON'T set their own link color
     (e.g. links in sidecar prose, inline doc references in
     start-here body) inherit this baseline.

   Decoration discipline: text-decoration uses currentColor at rest
   so the underline tints with the link color; on hover, the color
   brightens via accent-mix and the underline thickens slightly to
   reinforce the affordance without changing the text size.
*/
:where(a) {
  color: var(--accent);
  text-decoration-line: underline;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: color 0.15s ease, text-decoration-color 0.15s ease;
}
:where(a:hover, a:focus-visible) {
  color: color-mix(in srgb, var(--accent) 75%, var(--ink));
  text-decoration-color: currentColor;
}
:where(a:visited) {
  /* Match unvisited -- kit pages aren't archive content; the
     legacy "you've been here" purple is noise, not signal. */
  color: var(--accent);
}
:where(a:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}
/* Dark mode: --accent resolves to a lighter blue automatically.
   On hover, mix against --ink (which is light in dark mode) makes
   the link MORE muted on hover -- inverted from light mode. Fix
   by using a brighter target instead. */
body[data-theme="dark"] :where(a:hover, a:focus-visible) {
  color: color-mix(in srgb, var(--accent) 100%, white 18%);
}


/* ---------- Theme tokens (light default) ---------- */
:root {
  /* Light theme: off-white over pure white. Softens the page so
     it doesn't feel sterile while keeping ink contrast strong
     (--ink #1f2026 against --bg #f7f6f1 is ~14:1, well above WCAG
     AAA). The "softer feel" comes from no element being pushed to
     visual extremes (no pure white, no pure black), not from
     reduced contrast. Pre-session-9 used --bg #fafaf8 and
     --bg-elev #ffffff which read as glaring on calibrated
     displays.

     v0.8 (CS brand alignment): the neutral off-white shifts to a
     teal-tinted off-white drawn from the Project Dogwood deck.
     The cast is intentionally subtle (around 3% teal) so dense
     journey-map content still reads airy. The accent moves from
     navy to deep teal pulled from the deck's primary brand mark
     (#037185 family). All contrast ratios re-verified against
     ink and the new bg. */
  --bg: #f3f6f4;
  --bg-elev: #f9fbfa;
  --ink: #0f242a;
  --ink-soft: #3a4a4f;
  --ink-muted: #5a6e74;
  --rule: #dde6e6;
  --accent: #036476;
  --accent-soft: #dceef0;

  /* State tokens: hold / flex / break / hold-mitigated.
     Dual-encoded with color, but every component using these
     must also include a glyph and a text label. Color alone
     fails accessibility. */
  --hold-ink: #1f7a4e;
  --hold-bg: #e6f4ec;
  --hold-edge: #5fb389;

  --flex-ink: #8a5a00;
  --flex-bg: #fbefcf;
  --flex-edge: #d4a72c;

  --break-ink: #a8281f;
  --break-bg: #fbe9e6;
  --break-edge: #d76b62;

  /* Spacing scale */
  --pad-xs: 0.5rem;
  --pad-s: 1rem;
  --pad-m: 1.5rem;
  --pad-l: 2.5rem;
  --pad-xl: 4rem;
  --section-pad: 5rem;

  /* Top bar height (sticky nav). Used by `scroll-padding-top` on
     html and `scroll-margin-top` on sections so anchor jumps
     don't get covered by the bar. Padding (0.75rem * 2) + content
     (~2.4rem for h1 + subtitle) + border = ~4.5rem. */
  --top-bar-height: 4.5rem;

  /* Layout */
  --container-max: 72rem;
  --reading-max: 60ch;

  /* Blueprint grid: number of stage columns. Override per artifact:
     <div class="blueprint" style="--bp-cols: 5"> for 5 stages.
     Default sized for 7 stages. */
  --bp-cols: 7;
  --bp-label-col: 12rem;
  --bp-stage-min: 10rem;
}

/* ============================================================
   v0.8 Teal Light theme — DEPRECATED at preview stage

   This theme block was developed alongside Light and Teal Dark
   as a brand-immersive light option pulled from the RSR HITL
   deck. In sandbox testing it competed with the journey map's
   chip and accent vocabulary (the deck's surface-saturation
   approach worked one slide at a time but stacked teal-on-teal
   on a long-scroll dense artifact).

   Kept here as opt-in only — projects can set
   data-theme="teal-light" manually if a use case earns the
   surface-tinted approach (e.g. for a brand-immersive landing
   page where content density is low). Not exposed via the
   user-facing toggle. v0.8 ships Light + Teal Dark.
   ============================================================ */
body[data-theme="teal-light"] {
  --bg: #e8f3ff;
  --bg-elev: #f0f7ff;
  --ink: #0d1f24;
  --ink-soft: #2c4651;
  --ink-muted: #4f6772;
  --rule: #c9dde8;
  --accent: #015d70;
  --accent-soft: #cfe3ee;

  /* State tokens unchanged from light theme (greens / ambers /
     reds aren't part of the brand palette; they communicate
     hold/flex/break across all themes). The Teal-light bg
     stays in the same lightness band as default light, so
     state contrast is preserved. */
  --hold-ink: #1f7a4e;
  --hold-bg: #d8ecde;
  --hold-edge: #5fb389;

  --flex-ink: #8a5a00;
  --flex-bg: #f3e5c0;
  --flex-edge: #d4a72c;

  --break-ink: #a8281f;
  --break-bg: #f4dad5;
  --break-edge: #d76b62;
}

/* ============================================================
   Legacy warm theme (deprecated in v0.8)

   Kept so projects that explicitly set data-theme="warm" before
   v0.8 still resolve. The kit's toggle no longer exposes warm;
   it exposes light, teal-light, teal-dark. localStorage values
   stored as 'warm' from older versions get coerced to 'light'
   on next load (see app.js initTheme).
   ============================================================ */
body[data-theme="warm"] {
  --bg: #f7f2ea;
  --bg-elev: #fdfaf4;
  --ink: #2a1f14;
  --ink-soft: #4a4035;
  --ink-muted: #685c50;
  --rule: #e8dfd0;
  --accent: #8a5a2a;
  --accent-soft: #f3e5d0;

  --hold-ink: #4a6b3a;
  --hold-bg: #ebf0d8;
  --hold-edge: #8aa56a;

  --flex-ink: #8a5a1a;
  --flex-bg: #f4e3c0;
  --flex-edge: #c89a4a;

  --break-ink: #8a3520;
  --break-bg: #f0d8c8;
  --break-edge: #c87560;
}

body[data-theme="dark"] {
  /* v0.8 Teal Dark theme — pure dark with teal tint.

     Conventional dark mode shape: near-black base, slightly
     lighter elev for elevated surfaces. The teal isn't carried
     by the surface; it's carried by the bright accent and the
     hero atmosphere bloom. The surface itself is dark first,
     teal-cast quietly so it doesn't read as generic charcoal.

     Two earlier iterations failed: pulling bg into the deck's
     mid-teal range made the page read as a wash; pulling elev
     up to mid-teal stacked tinted surfaces against tinted
     storyboard cells (the journey map's chip-and-card vocabulary
     uses teal accents internally, so an additional teal layer
     beneath them broke hierarchy). The right move is dark with
     a quiet teal cast and the brand carried by accent + bloom.

     Elev sits at standard dark-mode ~+8L lift over bg, both
     teal-cast. Same elegance the kit's light mode earns through
     the slight off-white tint. */
  --bg: #0d1518;
  --bg-elev: #15212a;
  --ink: #e8f1f3;
  --ink-soft: #b5c9ce;
  --ink-muted: #8fa6ac;
  --rule: #1f2d33;
  --accent: #5fc4d8;
  --accent-soft: #102a33;

  --hold-ink: #7fd4a3;
  --hold-bg: #1a2e24;
  --hold-edge: #4a8c6b;

  --flex-ink: #f0c060;
  --flex-bg: #2e2618;
  --flex-edge: #a08040;

  --break-ink: #f08070;
  --break-bg: #2e1a18;
  --break-edge: #a05040;
}


/* ---------- Skip link ---------- */
.skip-link {
  position: absolute;
  top: -100px;
  left: 0.5rem;
  padding: 0.5rem 1rem;
  background: var(--accent);
  color: var(--bg-elev);
  text-decoration: none;
  border-radius: 4px;
  z-index: 9999;
}
.skip-link:focus { top: 0.5rem; }


/* ---------- Top bar ---------- */
.top-bar {
  position: sticky;
  top: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--pad-m);
  padding: 0.75rem var(--pad-m);
  /* Translucent + backdrop-blur: the modern nav treatment. As
     content scrolls under the bar, the page beneath blurs and
     tints through, giving the bar a sense of being "above" the
     content rather than a solid lid. The fallback (browsers
     without backdrop-filter) is a near-opaque bg so legibility
     is preserved. color-mix derives the tint from --bg-elev so
     it follows theme tokens automatically. */
  background: color-mix(in srgb, var(--bg-elev) 75%, transparent);
  -webkit-backdrop-filter: saturate(160%) blur(12px);
  backdrop-filter: saturate(160%) blur(12px);
  border-bottom: 1px solid color-mix(in srgb, var(--rule) 70%, transparent);
}
/* Fallback for browsers without backdrop-filter: keep the bar
   visible by upping the bg opacity. Detected via @supports so
   modern browsers don't get hit. */
@supports not (backdrop-filter: blur(1px)) {
  .top-bar {
    background: color-mix(in srgb, var(--bg-elev) 96%, transparent);
  }
}
.top-bar h1 {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
}
/* v0.8.1: optional H1 accent span. Used on start-here where the
   H1 IS the kit's name (which functions as a wordmark). The
   accent-tinted prefix makes "CS UX" read as a logotype, with
   "Service Blueprint Kit" trailing in default ink. NOT applied
   to template.html, example-wmp.html, or other kit-built
   artifacts where the H1 is the project's name, not the kit's. */
.top-bar h1 .top-bar-h1-accent {
  color: var(--accent);
}
/* v0.8.1: .top-bar-parent rule retired. Tried as a parent-program
   eyebrow row above the H1 in the top-bar; resulting three-row
   header was too much chrome on every artifact, especially when
   the journey map scrolled. Brand parentage now lives where it
   doesn't tax the working surface: in the start-here hero eyebrow
   (a moment), and in the start-here footer brand line (closing
   beat). Builder artifacts (template, example-wmp, patterns) drop
   kit-side branding entirely so projects own their own brand. */
.top-bar .subtitle {
  display: block;
  font-size: 0.75rem;
  color: var(--ink-muted);
  margin-top: 2px;
}
/* Version link in the subtitle: quiet by default (matches subtitle
   color, no underline so the band stays calm), accent on hover/focus
   so the affordance shows up when the reader looks for it. The
   :where(a) kit-wide rule would otherwise tint this accent at rest;
   higher specificity wins for the rest treatment, hover passes
   through to the kit-wide hover. */
.top-bar .subtitle .subtitle-version {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--ink-muted) 30%, transparent);
  text-underline-offset: 2px;
  transition: color 0.15s ease, text-decoration-color 0.15s ease;
}
.top-bar .subtitle .subtitle-version:hover,
.top-bar .subtitle .subtitle-version:focus-visible {
  color: var(--accent);
  text-decoration-color: currentColor;
}
.top-bar-right {
  display: flex;
  align-items: center;
  gap: var(--pad-s);
}
.top-nav {
  display: flex;
  gap: 0.15rem;
}
.nav-link {
  font-size: 0.82rem;
  letter-spacing: 0.01em;
  color: var(--ink-soft);
  text-decoration: none;
  padding: 0.3rem 0.7rem;
  border-radius: 999px;
  transition: color 0.15s ease, background 0.15s ease;
}
.nav-link:hover {
  color: var(--ink);
  background: color-mix(in srgb, var(--accent-soft) 70%, transparent);
}
.nav-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  color: var(--ink);
}
.nav-link.nav-active {
  color: var(--ink);
  background: var(--accent-soft);
}
/* CTA variant: the action-oriented nav link ("Make one" on
   start-here) gets accent-fill treatment so it reads as the
   conversion moment, distinct from the four understanding-
   oriented links. The kit's discipline reserves chrome for
   interactive surfaces; here the chrome's saturation also
   reserves itself for the surface that's actually the goal
   of the page. Hover and focus states mirror the standard
   nav-link affordances. */
.nav-link.nav-cta {
  color: var(--bg);
  background: var(--accent);
  font-weight: 600;
}
.nav-link.nav-cta:hover {
  color: var(--bg);
  background: color-mix(in srgb, var(--accent) 88%, var(--ink));
}
.nav-link.nav-cta:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  color: var(--bg);
}
.nav-link.nav-cta.nav-active {
  color: var(--bg);
  background: color-mix(in srgb, var(--accent) 88%, var(--ink));
}
/* Reduced motion: drop the color/background transitions; the state
   change still happens, it just doesn't ease. */
@media (prefers-reduced-motion: reduce) {
  .nav-link { transition: none; }
}


/* ---------- Theme toggle ----------
   Single-icon toggle: one circular button that swaps between
   light and dark modes. Icon shows what you'd switch TO, not
   the current state -- this is the modern standard for two-
   state theme controls (GitHub, Vercel, Linear, Stripe).
   The button uses two stacked SVGs (sun + moon); CSS hides the
   one that matches the current theme so only the "switch to"
   icon is visible at any time. No JS required for the swap. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0;
  cursor: pointer;
  color: var(--ink-soft);
  transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
}
.theme-toggle:hover {
  color: var(--ink);
  background: var(--bg-elev);
}
.theme-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.theme-toggle-icon {
  width: 16px;
  height: 16px;
  display: block;
}
/* Show the icon that's the OPPOSITE of the current theme --
   what the user would switch to. Default page state is light;
   show moon (switch to dark). When data-theme="dark", show sun
   (switch to light). Smooth swap via opacity. */
.theme-toggle-icon.is-sun { display: none; }
.theme-toggle-icon.is-moon { display: block; }
body[data-theme="dark"] .theme-toggle-icon.is-sun { display: block; }
body[data-theme="dark"] .theme-toggle-icon.is-moon { display: none; }


/* ---------- Section shell (every Part uses this) ---------- */
.section {
  padding: var(--section-pad) var(--pad-m);
  border-bottom: 1px solid var(--rule);
  /* When a nav link jumps to this section, leave room above so
     the sticky top bar doesn't cover the section title. */
  scroll-margin-top: var(--top-bar-height);
}
.section:last-of-type { border-bottom: 0; }
.section-inner {
  max-width: var(--container-max);
  margin: 0 auto;
}

/* Part label -- the kit's signature spine vocabulary */
.section-label {
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 600;
  margin: 0 0 0.5rem;
  position: relative;
  padding-bottom: 0.5rem;
}
.section-label::after {
  content: "";
  display: block;
  width: 2rem;
  height: 1px;
  background: var(--rule);
  position: absolute;
  bottom: 0;
  left: 0;
}
/* Hero / atmospheric Parts: drop the trailing rule */
.section-label-hero::after { display: none; }

/* Section-job: the question this Part answers (visible, not tooltip) */
.section-job {
  font-size: 1.1rem;
  font-weight: 500;
  color: var(--ink-soft);
  margin: 0 0 0.75rem;
  font-style: italic;
}

.section-title {
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 600;
  margin: 0 0 var(--pad-s);
  line-height: 1.25;
  letter-spacing: 0;
}

.section-intro {
  font-size: 1.1rem;
  color: var(--ink-soft);
  line-height: 1.6;
  max-width: 55ch;
  margin: 0 0 var(--pad-l);
}

/* Section 2 elevation: scale gets a different background tone */
.section-scale { background: var(--bg-elev); }

/* v0.8 dark polish: scale section gets a quiet radial accent
   gradient behind content. Same atmospheric vocabulary as the
   hero bloom; light moment in the journey-map page where surface
   earns a halo rather than just lifting. Light theme stays
   chrome-clean (the off-white bg already breathes light). */
body[data-theme="dark"] .section-scale {
  background:
    radial-gradient(
      ellipse 80% 50% at 50% 0%,
      color-mix(in srgb, var(--accent) 10%, transparent) 0%,
      color-mix(in srgb, var(--accent) 4%, transparent) 50%,
      transparent 100%
    ),
    var(--bg-elev);
}

/* v0.8 dark polish: column-highlight pickup is brighter so an
   active stage column reads as a focused beam. Light theme keeps
   the quieter tint since light surfaces don't need as much help
   from the highlight to read as elevated. */
body[data-theme="dark"] .col-highlight {
  background-color: color-mix(in srgb, var(--col-glow, var(--accent)) 14%, transparent) !important;
}
body[data-theme="dark"] .bp-stage-head.col-highlight {
  background: color-mix(in srgb, var(--col-glow, var(--accent)) 22%, var(--bg));
}
body[data-theme="dark"] .narr-hit.col-highlight,
body[data-theme="dark"] .effort-hit.col-highlight {
  background-color: color-mix(in srgb, var(--col-glow, var(--accent)) 14%, transparent) !important;
}


/* ---------- Hero atmosphere (kit-level) ----------
   Adaptive backdrop for any artifact's Part 1 hero. Three layered
   elements (dot grid + radial bloom + flowing curve) derive their
   position and shape from a hashed seed. Default seed is the
   page <title>; override via data-flare-seed on the hero element.

   Discipline: apply ONLY to the Part 1 hero. The rest of the
   artifact has too much content competing for attention; an
   atmosphere there would compete with the journey grid, evidence
   cards, or takeaways. One backdrop per artifact, in the place
   that sets the tone.

   Markup contract:
     <section class="section section-problem hero-atmos-host">
       <div class="hero-atmos" aria-hidden="true">
         <div class="hero-atmos-grid"></div>
         <div class="hero-atmos-bloom"></div>
         <svg class="hero-atmos-flow" ...>
           <defs><linearGradient id="hero-flow-grad">...</linearGradient></defs>
           <path class="hero-atmos-flow-path" .../>
         </svg>
       </div>
       <div class="section-inner">...content...</div>
     </section>
*/
.hero-atmos-host {
  position: relative;
  overflow: hidden;
}
.hero-atmos-host > .section-inner {
  position: relative;
  z-index: 1;
}

.hero-atmos {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}

/* Layer 1: dot-grid fabric. Suggests "infrastructure" without
   being literal. Mask fades it out toward the bottom-right so
   it doesn't fight content below. */
.hero-atmos-grid {
  position: absolute;
  inset: 0;
  background-image: radial-gradient(
    circle at center,
    color-mix(in srgb, var(--ink) 22%, transparent) 1px,
    transparent 1px
  );
  background-size: 28px 28px;
  background-position: 0 0;
  opacity: 0.32;
  -webkit-mask-image: linear-gradient(
    to bottom right,
    rgba(0,0,0,0.85) 0%,
    rgba(0,0,0,0.4) 55%,
    rgba(0,0,0,0) 100%
  );
          mask-image: linear-gradient(
    to bottom right,
    rgba(0,0,0,0.85) 0%,
    rgba(0,0,0,0.4) 55%,
    rgba(0,0,0,0) 100%
  );
}

/* Layer 2: radial bloom. Position derived from --flare-x and
   --flare-y CSS variables (set by JS from the seed). Soft accent
   wash, generous spread, long feather so the gradient never reads
   as a "spotlight" -- it's atmospheric tint, not a focal point. */
/* Bloom: a soft accent wash whose horizontal position is driven
   by JS in lockstep with the thickness flow on the line. As the
   bell of thickness travels left-to-right through the curve, the
   bloom drifts right at the same cadence -- the eye reads the
   bloom as a light source the line is reflecting. Luminance
   breathes on a separate longer cycle so the relationship between
   the two motions never reads as mechanical lock-step.
   
   The gradient stops are gentle (no hot core) so the bloom feels
   like atmospheric tint rather than a localized spotlight. */
.hero-atmos-bloom {
  position: absolute;
  inset: 0;
  background: radial-gradient(
    circle 70rem at var(--flare-x, 22%) var(--flare-y, 38%),
    color-mix(in srgb, var(--accent) 18%, transparent) 0%,
    color-mix(in srgb, var(--accent) 12%, transparent) 30%,
    color-mix(in srgb, var(--accent) 5%, transparent) 60%,
    transparent 95%
  );
  will-change: opacity;
  animation: hero-bloom-breath 24s ease-in-out infinite;
}
@keyframes hero-bloom-breath {
  0%, 100% { opacity: var(--bloom-opacity-low, 0.26); }
  50%      { opacity: var(--bloom-opacity-high, 0.46); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-atmos-bloom {
    animation: none;
    opacity: 0.4;
  }
}

/* Layer 3: flowing curve. Hidden by default -- the bloom + grid
   already carry the kit's signature on every artifact, and a
   decorative curve over body content competes more than it adds.

   Opt in by adding .hero-atmos-flourish to the host element on
   pages that have room for the gesture (start-here, leadership-
   facing landing pages). Keep it off normal artifacts.
*/
.hero-atmos-flow {
  display: none;
}
.hero-atmos-flourish .hero-atmos-flow {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0.85;
  /* Promote the SVG to its own compositor layer so the browser
     paints the animating path on the GPU rather than triggering
     main-thread paints. Cheap insurance against scroll stutter
     on high-DPI displays without changing any behavior. */
  will-change: transform;
  transform: translateZ(0);
  /* Asymmetric horizontal mask: keeps a faint trace of the curve
     visible even through the text zone so the line reads as ONE
     continuous gesture rather than two disconnected strokes. The
     middle stays at 12% opacity -- present enough to connect the
     two ends, faint enough that body copy is unaffected. */
  -webkit-mask-image: linear-gradient(to right,
    rgba(0,0,0,1) 0%,
    rgba(0,0,0,1) 6%,
    rgba(0,0,0,0.12) 22%,
    rgba(0,0,0,0.12) 78%,
    rgba(0,0,0,1) 94%,
    rgba(0,0,0,1) 100%);
  mask-image: linear-gradient(to right,
    rgba(0,0,0,1) 0%,
    rgba(0,0,0,1) 6%,
    rgba(0,0,0,0.12) 22%,
    rgba(0,0,0,0.12) 78%,
    rgba(0,0,0,1) 94%,
    rgba(0,0,0,1) 100%);
}
/* Presenter-mode mask: slides have wider text spans than hero
   pillars, so the middle fade extends further (15%-85%) and the
   center is softened to 8% so headlines and body copy are
   unobstructed regardless of where they land in the viewport. */
body[data-view="presenter"] .hero-atmos-flourish .hero-atmos-flow {
  -webkit-mask-image: linear-gradient(to right,
    rgba(0,0,0,1) 0%,
    rgba(0,0,0,1) 4%,
    rgba(0,0,0,0.08) 15%,
    rgba(0,0,0,0.08) 85%,
    rgba(0,0,0,1) 96%,
    rgba(0,0,0,1) 100%);
  mask-image: linear-gradient(to right,
    rgba(0,0,0,1) 0%,
    rgba(0,0,0,1) 4%,
    rgba(0,0,0,0.08) 15%,
    rgba(0,0,0,0.08) 85%,
    rgba(0,0,0,1) 96%,
    rgba(0,0,0,1) 100%);
}
/* The edge-thick path: paints a 2.5px stroke layered behind the
   thin 1.4px path. Its gradient is a tileable bell-shape pattern
   that translates horizontally over time (driven from app.js)
   producing the visual of ink-density flowing forward along the
   curve. Mask drops the visibility through the text zone to a
   very low value (4%) so the flowing thickness doesn't flash
   through reading copy, but enough remains to keep the curve's
   weight feeling continuous as the bell flows past. */
.hero-atmos-flow-path-thick {
  stroke-linecap: round;
  -webkit-mask-image: linear-gradient(to right,
    rgba(0,0,0,1) 0%,
    rgba(0,0,0,1) 6%,
    rgba(0,0,0,0.04) 22%,
    rgba(0,0,0,0.04) 78%,
    rgba(0,0,0,1) 94%,
    rgba(0,0,0,1) 100%);
  mask-image: linear-gradient(to right,
    rgba(0,0,0,1) 0%,
    rgba(0,0,0,1) 6%,
    rgba(0,0,0,0.04) 22%,
    rgba(0,0,0,0.04) 78%,
    rgba(0,0,0,1) 94%,
    rgba(0,0,0,1) 100%);
}
.hero-atmos-flow-path {
  /* d attribute set by JS at boot. Default fallback wave so the
     page doesn't render flat if JS is blocked. */
  d: path("M -100 280 C 200 180, 500 380, 800 240 S 1100 120, 1300 200");
}

/* Theme adjustments: the bloom and flow re-theme via --accent
   automatically; we just nudge intensity so warm and dark feel
   right against their surface tones. The bloom uses opacity
   overrides per theme rather than separate keyframes so each
   theme reads with calibrated presence: dark needs more
   luminance (the accent gets absorbed by dark surfaces), warm
   takes a small lift, light is balanced as-is. */
body[data-theme="warm"] .hero-atmos-bloom {
  --bloom-opacity-low: 0.36;
  --bloom-opacity-high: 0.58;
}
body[data-theme="dark"] .hero-atmos-grid { opacity: 0.42; }
body[data-theme="dark"] .hero-atmos-bloom {
  /* v0.8: dark bloom dialed back. The new teal accent reads more
     luminous than the v0.7 light blue at the same opacity, so the
     bloom hits the same atmospheric weight at lower values. */
  --bloom-opacity-low: 0.32;
  --bloom-opacity-high: 0.55;
}
body[data-theme="dark"] .hero-atmos-flourish .hero-atmos-flow { opacity: 0.85; }
/* Light theme: the accent stroke against the off-white background.
   Now that the bg is meaningfully off-white (#f7f6f1, not pure
   white), the curve has surface to push against and doesn't
   need its pre-session-9 boost (1.8px / opacity 1.0). Softening
   to match dark-mode's quieter presence so light mode feels
   less foregrounded while preserving readability. */
body[data-theme="light"] .hero-atmos-flow-path,
body:not([data-theme]) .hero-atmos-flow-path {
  stroke-width: 1.5;
}
body[data-theme="light"] .hero-atmos-flourish .hero-atmos-flow,
body:not([data-theme]) .hero-atmos-flourish .hero-atmos-flow {
  opacity: 0.85;
}

/* Hide the atmosphere when it can't read or shouldn't compete. */
@media print {
  .hero-atmos { display: none; }
}
body[data-view="map"] .hero-atmos { display: none; }


/* ---------- Part 1 / Problem hero ---------- */
.problem-hero {
  /* No top padding. The eyebrow + thesis read as one beat — the
     eyebrow is a prefix to the thesis, not its own paragraph block.
     Earlier versions of the template had a `.section-job` paragraph
     between the eyebrow and the thesis (a kit-template guidance
     question like "Why are we here?"); the padding-top here was
     compensating for that paragraph's vertical weight. With
     section-job removed in project artifacts (per kit doctrine,
     see kit-feedback-triage.md Finding F sub-issue 6), the padding
     reads as orphan air that pulls the eyebrow away from the
     title. Bottom padding kept since it spaces the thesis off the
     pillar cards below. */
  padding-block: 0 var(--pad-m);
}
.problem-thesis {
  font-size: 2.5rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0;
  margin: 0 0 var(--pad-m);
  max-width: 22ch;
}
.problem-thesis-accent { color: var(--accent); }

/* Diagnostic line: one-line state-of-play under the hero.
   Frosted-glass treatment matches the chrome-band vocabulary the
   kit uses on .top-bar and .sh-hero-banner. Tinted bg-elev plus
   backdrop saturation and blur reads as continuous chrome across
   themes. Solid bg-elev was the v0.8 default; calibrated symmetric
   only in light mode and recessed in dark. The frosted formula
   gives both themes equal presence. */
.problem-diagnostic {
  display: inline-flex;
  gap: 0.75rem;
  align-items: center;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--ink-soft);
  padding: 0.5rem 0.85rem;
  background: color-mix(in srgb, var(--bg-elev) 60%, transparent);
  -webkit-backdrop-filter: saturate(160%) blur(14px);
  backdrop-filter: saturate(160%) blur(14px);
  border: 1px solid color-mix(in srgb, var(--rule) 50%, transparent);
  border-radius: 999px;
  margin-bottom: var(--pad-l);
}
body[data-theme="dark"] .problem-diagnostic {
  background: color-mix(in srgb, var(--bg-elev) 35%, transparent);
  border-color: color-mix(in srgb, var(--rule) 30%, transparent);
}
/* Fallback when backdrop-filter is unsupported. Solid-ish bg-elev
   keeps the band readable without the frosted illusion. */
@supports not (backdrop-filter: blur(1px)) {
  .problem-diagnostic {
    background: color-mix(in srgb, var(--bg-elev) 85%, transparent);
  }
}
.problem-diagnostic strong { color: var(--ink); }


/* ---------- Bet / pillar cards (used in Part 1) ----------
   Editorial pillars naming the kit's claims. Not interactive --
   no card chrome. Gutter hairlines separate columns. Same
   anti-vibe-code discipline applied across every kit artifact:
   chrome only on interactive surfaces, typography everywhere
   else. */
.bet-row {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--pad-l);
  position: relative;
}
.bet-item {
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  position: relative;
}
/* Vertical column-separator hairlines were removed: they fought
   the breathing curve in the hero atmosphere, and the pillars'
   typographic gutters carry enough separation on their own. The
   set reads as three editorial columns held by whitespace. */
.bet-item-num {
  display: inline-block;
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  color: var(--ink-muted);
  text-transform: uppercase;
  font-weight: 600;
  margin-bottom: var(--pad-xs);
}
.bet-item-name {
  font-size: 1.15rem;
  font-weight: 600;
  margin: 0 0 0.5rem;
}
.bet-item-why {
  font-size: 0.95rem;
  color: var(--ink-soft);
  margin: 0;
}


/* ---------- Needs rubric (used in Part 1 or sidecar) ----------
   A curated taxonomy of audience-side needs. AXNs (Associate
   Experience Needs) are the canonical worked example, but the
   shape generalizes: WCAG criteria, HEART metrics, heuristics, or
   any team-authored rubric that anchors the artifact's argument.

   The rubric is reference data, not editorial claim. Per the
   chrome principle, data containers earn chrome: chip-cards with
   a tinted surface so the rubric reads as a discrete inventory,
   not as freeform pillars. Restrained chrome (no per-card border,
   no shadow lift) keeps the chip register quiet next to the
   editorial surfaces it shares Part 1 with.

   Optional rating dot maps source rubric scores to the kit's
   three-state vocabulary (hold / flex / break). For AXNs, a
   7-point Likert: 5.1+ holds, 4.1-5.0 flexes, 4.0 or lower
   breaks. The mapping protocol lives in visual-language.md
   under "Mapping source-data states." */
.needs-rubric {
  display: grid;
  gap: var(--pad-m);
  margin: var(--pad-m) 0;
}
.needs-rubric-eyebrow {
  display: inline-block;
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  color: var(--ink-muted);
  text-transform: uppercase;
  font-weight: 600;
}
.needs-rubric-title {
  font-size: 1.5rem;
  font-weight: 600;
  margin: 0;
}
.needs-rubric-intro {
  font-size: 0.95rem;
  color: var(--ink-soft);
  margin: 0;
  max-width: 65ch;
}
.needs-rubric-group {
  display: grid;
  gap: var(--pad-s);
}
.needs-rubric-group-label {
  display: inline-block;
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
  text-transform: uppercase;
  font-weight: 600;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 0.25rem;
}
.needs-rubric-grid {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--pad-s);
}
.needs-rubric-item {
  position: relative;
  padding: 0.75rem 0.9rem;
  background: var(--bg-elev);
  border-radius: 8px;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.6rem;
  align-items: baseline;
}
.needs-rubric-id {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.needs-rubric-statement {
  font-size: 0.9rem;
  color: var(--ink);
  line-height: 1.4;
  margin: 0;
}
/* Optional rating dot: maps source rubric score to the kit's
   three-state vocabulary. Sits to the left of the ID. */
.needs-rubric-item.has-rating {
  grid-template-columns: auto auto 1fr;
}
.needs-rubric-dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  align-self: center;
  background: var(--ink-muted);
}
.needs-rubric-dot.is-hold { background: var(--hold-edge); }
.needs-rubric-dot.is-flex { background: var(--flex-edge); }
.needs-rubric-dot.is-break { background: var(--break-edge); }


/* ---------- Part 2 / Scale: KPI strip ---------- */
.scale-kpi-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--pad-m);
  margin: var(--pad-m) 0;
}
.scale-kpi {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.scale-kpi-num {
  font-size: 3rem;
  font-weight: 700;
  line-height: 1;
  color: var(--accent);
}
.scale-kpi-label {
  font-size: 1rem;
  font-weight: 500;
  color: var(--ink);
}
.scale-kpi-sub {
  font-size: 0.8rem;
  color: var(--ink-muted);
}


/* ---------- Part 3 / Journey: blueprint grid ----------
   .bp-wrap is the positioning context for floating scroll arrows --
   no overflow on it. The .blueprint itself owns the horizontal scroll.
   Each .bp-row is its own independent grid (HITL/CS Help pattern,
   not subgrid) -- this is the only structure where position: sticky
   on .bp-label reliably anchors to the blueprint's scrollport. */
.bp-wrap {
  margin: var(--pad-m) 0;
  position: relative;
  overflow: visible;
}
.blueprint {
  border: 1px solid var(--rule);
  background: var(--bg-elev);
  border-radius: 8px;
  overflow-x: auto;
  overflow-y: hidden;
  min-width: 0;
}
/* Each row defines its own column tracks identically. */
.bp-row {
  display: grid;
  grid-template-columns: var(--bp-label-col) repeat(var(--bp-cols), minmax(var(--bp-stage-min), 1fr));
  align-items: stretch;
  min-width: calc(var(--bp-label-col) + var(--bp-stage-min) * var(--bp-cols));
}
/* (Each .bp-row is its own grid -- defined above near .blueprint.
   No subgrid: position: sticky on .bp-label needs .blueprint as its
   scrollport ancestor, which subgrid would inadvertently break.) */
.bp-label {
  padding: var(--pad-s);
  background: var(--bg);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  /* Sticky-left so label column stays visible during horizontal scroll */
  position: sticky;
  left: 0;
  z-index: 5;
}
.bp-lbl-title {
  font-size: 0.85rem;
  font-weight: 600;
}
/* System-reference acronym inside a row title. Used when the row
   cites a known rubric system (AXN, CXN, WCAG, HEART) that the
   audience should recognize. The acronym sits in parens after the
   audience-facing title in muted weight so the title reads first
   and the system reference reads as a quiet pointer.
   Worked example: "Associate needs (AXN)". The title is what the
   reader scans; the parenthetical is what they need to look up the
   citation system if they don't recognize an ID. */
.bp-lbl-acronym {
  font-weight: 500;
  color: var(--ink-muted);
  letter-spacing: 0.02em;
  /* Slightly smaller than the title text so the parenthetical reads
     as subordinate without losing legibility. */
  font-size: 0.78rem;
}
.bp-lbl-sub {
  font-size: 0.72rem;
  color: var(--ink-muted);
  line-height: 1.35;
}
/* Use-when hint: a quiet third tier on row labels that carries
   one short "include this row when X" line. Helps a builder
   skimming the rendered template answer "is this row right for
   my project?" without reading source comments or asking Kiro.
   Only on optional rows where the decision is real -- not on
   always-present rows like Stages or Phases. Smaller and softer
   than sub-line; italic to signal "guidance, not data."

   Hidden by default. Toggle in the stages row label (or keyboard
   `g`) reveals all hints at once when the builder is making
   composition decisions. The "show when needed, hide when
   reading" pattern keeps the rendered template clean for
   normal use and teaching-rich on demand.

   When visible: neutral gray background + dashed ink-muted left
   border + compass glyph prefix matching the toggle icon. NOT in
   the kit's accent palette -- accent belongs to map content, and
   the hint is meta-guidance about the map, not part of it.
   Square corners, no border-radius, so the hint reads as an
   inline note rather than a card. */
.bp-lbl-when {
  display: none;
  position: relative;
  margin-top: 0.6rem;
  padding: 0.5rem 0.6rem 0.5rem 1.85rem;
  border-left: 2px dashed color-mix(in srgb, var(--ink-muted) 50%, transparent);
  background: color-mix(in srgb, var(--ink-muted) 7%, var(--bg-elev));
  font-size: 0.66rem;
  font-style: italic;
  color: var(--ink-soft);
  line-height: 1.45;
  max-width: 14rem;
}
body[data-theme="dark"] .bp-lbl-when {
  background: color-mix(in srgb, var(--ink-muted) 12%, var(--bg-elev));
}
body[data-row-guidance="on"] .bp-lbl-when {
  display: block;
}
.bp-lbl-when::before {
  content: "";
  position: absolute;
  left: 0.55rem;
  top: 0.5rem;
  width: 0.85rem;
  height: 0.85rem;
  background-color: var(--ink-muted);
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='8.5' fill='none' stroke='black' stroke-width='1.5'/><path d='M12 6 L13.4 10.6 L18 12 L13.4 13.4 L12 18 L10.6 13.4 L6 12 L10.6 10.6 Z' fill='black'/></svg>") no-repeat center / contain;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='8.5' fill='none' stroke='black' stroke-width='1.5'/><path d='M12 6 L13.4 10.6 L18 12 L13.4 13.4 L12 18 L10.6 13.4 L6 12 L10.6 10.6 Z' fill='black'/></svg>") no-repeat center / contain;
}
.bp-lbl-when strong {
  font-weight: 600;
  font-style: normal;
  color: var(--ink);
}

/* Row guidance toggle: lives at the bottom of the stages row's
   label cell so the affordance sits directly above the column of
   row labels it controls. Reads as a switch (track + thumb), not a
   CTA button -- "this is on/off" is the universal switch affordance.
   Quiet at rest, accent-filled track when on, thumb slides right. */
.row-guidance-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.55rem;
  padding: 0;
  background: transparent;
  border: 0;
  color: var(--ink-soft);
  cursor: pointer;
  font-size: 0.72rem;
  font-weight: 500;
  white-space: nowrap;
  transition: color 0.15s;
}
.row-guidance-toggle:hover {
  color: var(--ink);
}
.row-guidance-toggle[aria-checked="true"] {
  color: var(--ink);
}
.row-guidance-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}
.row-guidance-toggle-icon {
  display: inline-flex;
  width: 0.95rem;
  height: 0.95rem;
  flex-shrink: 0;
  color: var(--ink-muted);
  transition: color 0.15s;
}
.row-guidance-toggle-icon svg {
  width: 100%;
  height: 100%;
  display: block;
}
.row-guidance-toggle:hover .row-guidance-toggle-icon,
.row-guidance-toggle[aria-checked="true"] .row-guidance-toggle-icon {
  color: var(--ink-soft);
}
.row-guidance-toggle-label {
  white-space: nowrap;
}
/* The switch itself: track + thumb. Track is a 1.7rem-wide pill
   that fills with accent when checked; thumb is a circle that
   slides from the left edge to the right edge. Same vocabulary
   you'd see in iOS / Material switches, sized down. */
.row-guidance-toggle-switch {
  display: inline-flex;
  align-items: center;
  position: relative;
  width: 1.7rem;
  height: 1rem;
  flex-shrink: 0;
  margin-left: 0.15rem;
  background: color-mix(in srgb, var(--ink-muted) 25%, transparent);
  border-radius: 999px;
  transition: background 0.18s ease;
}
.row-guidance-toggle-thumb {
  position: absolute;
  top: 50%;
  left: 0.12rem;
  width: 0.78rem;
  height: 0.78rem;
  background: var(--bg-elev);
  border-radius: 50%;
  transform: translateY(-50%);
  transition: left 0.18s ease, background 0.18s ease;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
.row-guidance-toggle[aria-checked="true"] .row-guidance-toggle-switch {
  background: var(--accent);
}
.row-guidance-toggle[aria-checked="true"] .row-guidance-toggle-thumb {
  left: calc(100% - 0.9rem);
  background: var(--bg-elev);
}
@media (prefers-reduced-motion: reduce) {
  .row-guidance-toggle-switch,
  .row-guidance-toggle-thumb {
    transition: none;
  }
}
/* Interactive-row hint: small badge in the row label that signals
   "the nodes in this row are clickable." Use on rows with .bp-popover
   triggers (effort, emotion, etc.). */
.bp-lbl-hint {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.4rem;
  font-size: 0.65rem;
  font-weight: 500;
  color: var(--accent);
  letter-spacing: 0.01em;
  line-height: 1.3;
}
.bp-lbl-hint-icon {
  width: 11px;
  height: 11px;
  flex-shrink: 0;
  opacity: 0.85;
}
.bp-stage-head {
  padding: var(--pad-s);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  background: var(--bg);
}
.bp-stage-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  font-size: 0.75rem;
  font-weight: 700;
  background: var(--bg-elev);
  border: 1.5px solid var(--rule);
  border-radius: 50%;
  color: var(--ink-soft);
}
.bp-stage-name {
  font-size: 0.9rem;
  font-weight: 600;
}
.bp-stage-actor {
  font-size: 0.72rem;
  color: var(--ink-muted);
}

/* Cells */
.bp-c {
  padding: var(--pad-s);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  min-height: 5rem;
  position: relative;
  overflow: hidden;
}
.bp-c-headline {
  font-size: 0.88rem;
  font-weight: 600;
}
.bp-c-sub {
  font-size: 0.78rem;
  color: var(--ink-soft);
  line-height: 1.4;
}
.bp-c-empty .bp-c-dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--ink-muted);
  opacity: 0.4;
  margin-top: 0.5rem;
}

/* Layer rows: subtle background tint per layer */
.bp-layer-customer .bp-c { background: color-mix(in srgb, var(--accent-soft) 35%, var(--bg-elev)); }
.bp-layer-frontstage .bp-c { background: var(--bg-elev); }
.bp-layer-backstage .bp-c { background: color-mix(in srgb, var(--bg) 70%, var(--bg-elev)); }
.bp-layer-support .bp-c { background: color-mix(in srgb, var(--bg) 90%, var(--bg-elev)); }

/* Separation lines: rendered as named designed elements, not just borders.
   The row uses the same grid as other rows, with the band cell spanning
   columns 2 through end (.bp-sep-band has grid-column: 2 / -1). */
.bp-sep-label {
  background: var(--bg-elev);
  font-size: 0.72rem;
  color: var(--ink-muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 600;
  padding: 0.5rem var(--pad-s);
  border-right: 1px solid var(--rule);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}
.bp-sep-band {
  grid-column: 2 / -1;
  padding: 0.5rem var(--pad-s);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  background: var(--bg-elev);
  font-size: 0.78rem;
  color: var(--ink-muted);
}
/* Sticky-left so the line-of-interaction / line-of-visibility text
   stays readable as the reader pans across stages. The text is the
   line's *meaning*; it should follow the reader's eye, not scroll
   off the left edge. Same shape as .bp-label's sticky-left.

   Single-line by design: white-space: nowrap so the text reads as
   one continuous statement. The band cell spans all stage columns
   so there's plenty of horizontal room; wrapping to two lines made
   the row feel taller than its job warrants. */
.bp-sep-band-text {
  position: sticky;
  left: calc(var(--bp-label-col) + var(--pad-s));
  display: inline-block;
  white-space: nowrap;
  background: var(--bg-elev);
  padding-right: 0.5rem;
}
.bp-sep-visibility .bp-sep-band-text {
  background: var(--accent-soft);
}
/* Line of Visibility: the "designed" separator. If your artifact's
   invisibility / exposure / boundary bet lives here, this is its home. */
.bp-sep-visibility .bp-sep-label,
.bp-sep-visibility .bp-sep-band {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: var(--accent);
}


/* ---------- Part 4 / Evidence: hold / crack stress cards ---------- */
.stress-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: var(--pad-s);
}
.stress-item {
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 8px;
  overflow: hidden;
}
.stress-head {
  display: flex;
  align-items: baseline;
  gap: var(--pad-s);
  padding: var(--pad-s) var(--pad-m);
  border-bottom: 1px solid var(--rule);
}
.stress-num {
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  color: var(--ink-muted);
  text-transform: uppercase;
  font-weight: 600;
}
.stress-name { font-size: 1.15rem; font-weight: 600; margin: 0; }
.stress-body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
}
.stress-col {
  padding: var(--pad-m);
  border-right: 1px solid var(--rule);
}
.stress-col:last-child { border-right: 0; }

/* State pill: dual-encoded with color + glyph + text */
.stress-state {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 700;
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
  margin-bottom: 0.5rem;
}
.stress-state-hold {
  background: var(--hold-bg);
  color: var(--hold-ink);
  border: 1px solid var(--hold-edge);
}
.stress-state-flex {
  background: var(--flex-bg);
  color: var(--flex-ink);
  border: 1px solid var(--flex-edge);
}
.stress-state-break {
  background: var(--break-bg);
  color: var(--break-ink);
  border: 1px solid var(--break-edge);
}
/* Stress card state pills carry their state through bg + border +
   ink. Earlier the kit shipped an inner glyph dot (`.stress-state-
   glyph`) as a fourth signal saying the same thing as the other
   three; retired per the Chip dot discipline rule (visual-language
   .md). Markup may still carry the span for backwards compatibility;
   it renders nothing without these styles. */

.stress-text { font-size: 0.95rem; color: var(--ink); margin: 0 0 0.75rem; }
.stress-quote {
  border-left: 2px solid var(--rule);
  padding: 0.25rem 0.75rem;
  margin: 0;
  font-style: italic;
  color: var(--ink-soft);
  font-size: 0.9rem;
}
.stress-quote cite {
  display: block;
  font-style: normal;
  font-size: 0.75rem;
  color: var(--ink-muted);
  margin-top: 0.25rem;
}


/* ---------- Part 4.5 / Diagnosis bridge components ----------
   When an artifact is argumentative for VP / leadership audiences,
   the synthesis between Evidence and Takeaway sometimes earns its
   own Part. Evidence names the patterns; Takeaway recommends action;
   the Diagnosis bridge in between answers "so what is the strategic
   read of these failures, and what does inaction cost?" Adds a layer
   that quantifies the cost of the current architecture, names the
   investment imperative, and surfaces any failed prior attempt
   evidence (the kit calls this the "diagnostic signal" pattern).

   See narrative-structure.md "Diagnosis bridge as a 6-Part variant"
   and 03-component-library.md "Diagnosis bridge components" for when
   to use this Part vs. folding the synthesis into Takeaway.

   The kit ships four sub-components for the Diagnosis bridge:
   - .diagnosis-statement (display-register architectural diagnosis)
   - .cost-table (3-column dimension/metric/meaning table)
   - .investment-grid (priority-area cards)
   - .diagnostic-signal (failed prior attempt aside) */

/* In-section sub-heading for Parts that compose multiple structural
   blocks. Earlier the kit relied on .section-title (h2) + .section-
   intro for everything; the Diagnosis bridge needs h3-altitude
   sub-heads to introduce the cost table and investment grid as
   discrete blocks. Sits as a quiet typographic divider. */
.section-subheading {
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.005em;
  margin: var(--pad-l) 0 var(--pad-s);
  text-wrap: balance;
}
.section-subheading:first-child { margin-top: 0; }

.diagnosis-statement {
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 0.95rem + 0.7vw, 1.5rem);
  line-height: 1.5;
  font-weight: 500;
  color: var(--ink);
  margin: 0 0 var(--pad-l);
  max-width: 75ch;
  text-wrap: pretty;
  letter-spacing: -0.005em;
}
.diagnosis-statement strong {
  color: var(--accent);
  font-weight: 600;
}

.cost-table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 var(--pad-l);
  font-size: 0.9rem;
}
.cost-table thead th {
  text-align: left;
  padding: 0.55rem 1rem;
  background: var(--bg-elev);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  border-bottom: 1px solid var(--rule);
}
.cost-table tbody th,
.cost-table tbody td {
  text-align: left;
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
  line-height: 1.5;
}
.cost-table tbody tr:last-child th,
.cost-table tbody tr:last-child td {
  border-bottom: 0;
}
.cost-table tbody th {
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  width: 14ch;
}
.cost-table tbody td:first-of-type {
  font-weight: 600;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.cost-table tbody td:last-child {
  color: var(--ink-soft);
}

.investment-grid {
  list-style: none;
  margin: 0 0 var(--pad-l);
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--pad-s);
}
.investment-item {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: var(--pad-s) var(--pad-m);
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.investment-item-name {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  margin: 0;
  color: var(--ink);
  text-wrap: balance;
}
.investment-item-addresses {
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 700;
  margin: 0;
}
.investment-item-cost {
  font-size: 0.85rem;
  color: var(--ink-soft);
  line-height: 1.5;
  margin: 0;
}

.diagnostic-signal {
  margin: 0 0 var(--pad-l);
  padding: var(--pad-m) var(--pad-l);
  background: color-mix(in srgb, var(--break-bg) 30%, var(--bg-elev));
  border-left: 3px solid var(--break-edge);
  border-radius: 0 8px 8px 0;
}
body[data-theme="dark"] .diagnostic-signal {
  background: color-mix(in srgb, var(--break-bg) 20%, var(--bg-elev));
}
.diagnostic-signal-eyebrow {
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--break-edge);
  font-weight: 700;
  margin: 0 0 0.5rem;
}
.diagnostic-signal-claim {
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 var(--pad-s);
  line-height: 1.45;
  max-width: 70ch;
}
.diagnostic-signal-list {
  margin: 0;
  padding-left: 1.5rem;
  display: grid;
  gap: 0.4rem;
}
.diagnostic-signal-list li {
  font-size: 0.9rem;
  color: var(--ink-soft);
  line-height: 1.5;
  max-width: 70ch;
}
.diagnostic-signal-list li strong {
  color: var(--ink);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}


/* ---------- Part 5 / Takeaway: roadmap + recommendation ---------- */
.recommendation {
  padding: var(--pad-m);
  background: var(--accent-soft);
  border-left: 4px solid var(--accent);
  border-radius: 0 8px 8px 0;
  margin: var(--pad-m) 0 var(--pad-l);
}
.recommendation-eyebrow {
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 700;
  margin: 0 0 0.5rem;
}
.recommendation-text {
  font-size: 1.25rem;
  font-weight: 500;
  margin: 0;
  color: var(--ink);
}

.roadmap {
  display: grid;
  gap: var(--pad-s);
}
.roadmap-phase {
  padding: var(--pad-s) var(--pad-m);
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.roadmap-phase-eyebrow {
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 600;
  margin: 0 0 0.25rem;
}
.roadmap-phase-name {
  font-size: 1.1rem;
  font-weight: 600;
  margin: 0 0 0.35rem;
}
.roadmap-phase-body {
  font-size: 0.9rem;
  color: var(--ink-soft);
  margin: 0;
}


/* ---------- Section shift: in-section visual pivot ----------
   When a single section carries two beats of an argument (a
   diagnostic before / resolution after, a current-state roadmap
   followed by a future-state set of resolution pairs, an evidence
   block followed by a synthesis prompt), the second beat earns a
   visible pivot from the first.

   Usage: apply `.section-shift` to the first sibling of the second
   beat (typically a `.section-intro` introducing the new beat).
   The element gets a top margin equal to the section pad, top
   padding for the breathing room above its content, and a hairline
   rule across its top edge.

   Don't use this for routine paragraph-to-paragraph spacing inside
   a section. The shift earns its place when the reader needs to
   register "this is a different beat within the same section." */
.section-shift {
  margin-top: var(--section-pad);
  padding-top: var(--pad-l);
  border-top: 1px solid var(--rule);
}


/* ---------- Part 5 / Takeaway: resolution pairs ----------
   When the artifact's argument is "current state vs future state"
   (the kit's existing stress cards in Part 4 sit at hold-vs-break;
   resolution pairs sit at today-vs-with-the-new-architecture),
   the kit's resolution-pair component carries the parallel.

   Structural mirror of .stress-item (Part 4): a wrapper with a
   numbered head, a two-column body where each column is dual-encoded
   (state pill + body text + optional meta), and a closing meta line
   naming the system component(s) and stages where the resolution
   intervenes. The two columns use the kit's hold/break tokens so
   the visual contrast carries the temporal contrast: break (red)
   for the failure today, hold (green) for the resolution after.

   Use when:
   - Part 5 / Takeaway is about replacing infrastructure (DNR Service
     Blueprint is the canonical example: 5 architectural failures, 5
     replacements via Case Management).
   - The artifact's earlier parts establish the failure modes and
     Part 5 needs to show what changes for each.

   Avoid when:
   - The artifact's takeaway is a recommendation + roadmap (use the
     existing .recommendation + .roadmap pattern).
   - The before/after pairs are uneven (use the patterns-grid for
     thematic patterns instead). */
.resolution-pairs {
  display: grid;
  gap: var(--pad-s);
  list-style: none;
  padding: 0;
  margin: 0;
}
.resolution-pair {
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 1px 0 color-mix(in srgb, var(--rule) 50%, transparent);
}
.resolution-pair-head {
  display: flex;
  align-items: center;
  gap: var(--pad-m);
  padding: var(--pad-m) var(--pad-l);
  border-bottom: 1px solid var(--rule);
  background: color-mix(in srgb, var(--accent) 3%, var(--bg-elev));
}
/* Numbered seal: display-register stamp that anchors the card.
   Reads as identity (which item in the sequence) before the eye
   reaches the name. Tabular-nums so 01 / 02 / 03 align vertically
   when cards stack. The accent color signals the kit's narrative
   spine: each card is a numbered beat in the architectural shift. */
.resolution-pair-num {
  font-family: var(--font-display, inherit);
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  letter-spacing: -0.01em;
  flex-shrink: 0;
  min-width: 2.2rem;
}
.resolution-pair-name {
  font-family: var(--font-display, inherit);
  font-size: 1.4rem;
  font-weight: 600;
  margin: 0;
  color: var(--ink);
  letter-spacing: -0.01em;
  line-height: 1.2;
  text-wrap: balance;
}
.resolution-pair-body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  position: relative;
}
.resolution-pair-col {
  /* min-width: 0 lets grid items shrink below their min-content
     size. Without this, long body text forces the column wider than
     50% of the container and the card overflows horizontally. The
     CSS Grid default (min-width: auto = min-content) is the wrong
     default for content that needs to wrap. */
  min-width: 0;
  padding: var(--pad-l) var(--pad-l);
  border-right: 1px solid var(--rule);
  position: relative;
}
.resolution-pair-col:last-child { border-right: 0; }

/* Column-level visual differentiation. The TODAY column carries a
   subtle break-tinted background (the failure recedes); the FUTURE
   column carries a subtle hold-tinted background (the resolution
   asserts). The two tints are calibrated low (around 12% over
   bg-elev) so the cards stay airy at scale, but distinct enough
   that the eye registers "left = past, right = future" instantly. */
.resolution-pair-current {
  background: color-mix(in srgb, var(--break-bg) 35%, var(--bg-elev));
}
.resolution-pair-future {
  background: color-mix(in srgb, var(--hold-bg) 35%, var(--bg-elev));
}
/* Dark theme tunes both tints lower since the underlying surface
   absorbs more color. Same vocabulary, calibrated per theme. */
body[data-theme="dark"] .resolution-pair-current {
  background: color-mix(in srgb, var(--break-bg) 22%, var(--bg-elev));
}
body[data-theme="dark"] .resolution-pair-future {
  background: color-mix(in srgb, var(--hold-bg) 22%, var(--bg-elev));
}

/* Inline arrow between current and future. The chevron sits on the
   internal column rule, half on each side, so the eye registers
   "current points to future" without a separate grid column. The
   accent color makes it a true transitional anchor rather than a
   muted decoration. */
.resolution-pair-body::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 2rem;
  height: 2rem;
  margin: -1rem 0 0 -1rem;
  border-radius: 50%;
  background: var(--bg-elev);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--rule));
  z-index: 1;
  box-shadow: 0 0 0 4px var(--bg-elev);
}
.resolution-pair-body::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0.55rem;
  height: 0.55rem;
  margin: -0.32rem 0 0 -0.42rem;
  border-top: 2px solid var(--accent);
  border-right: 2px solid var(--accent);
  transform: rotate(45deg);
  z-index: 2;
}
/* Stack columns on narrow viewports. Hide the arrow chevron since
   the visual axis flips from left-right to top-bottom; the column
   order itself carries the temporal direction. */
@media (max-width: 720px) {
  .resolution-pair-body::before,
  .resolution-pair-body::after { display: none; }
  .resolution-pair-body { grid-template-columns: 1fr; }
  .resolution-pair-col { border-right: 0; border-bottom: 1px solid var(--rule); }
  .resolution-pair-col:last-child { border-bottom: 0; }
}

/* State pills: dual-encoded with color + text. Background, border,
   and ink color all encode the state band; an inner glyph dot would
   add a fourth signal saying the same thing as the other three.
   The kit retired the same redundancy from `.resolution-mark` in
   v0.8.1 (Subtractive over additive); same logic applies here. The
   current-state pill uses break tokens (the failure); the future-
   state pill uses hold tokens (the resolution). The reader who
   learned the kit's pill grammar in Part 4 reads the same grammar
   in Part 5 without retraining. */
.resolution-pair-state {
  display: inline-flex;
  align-items: center;
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 700;
  padding: 0.25rem 0.65rem;
  border-radius: 999px;
  margin-bottom: 0.85rem;
  border: 1px solid;
  white-space: nowrap;
}
/* Current state: break tokens. The thing that's failing today. */
.resolution-pair-state-current {
  background: var(--break-bg);
  color: var(--break-ink);
  border-color: var(--break-edge);
}
/* Future state: hold tokens. The replacement that holds. */
.resolution-pair-state-future {
  background: var(--hold-bg);
  color: var(--hold-ink);
  border-color: var(--hold-edge);
}

/* Body text: TODAY reads slightly muted (the past, receding);
   FUTURE reads at full ink (the present, asserting). Subtle
   visual hierarchy that pulls the eye toward the resolution
   without making the failure feel diminished or diminutive. */
.resolution-pair-text {
  font-size: 0.95rem;
  line-height: 1.55;
  margin: 0;
  text-wrap: pretty;
}
.resolution-pair-current .resolution-pair-text {
  color: var(--ink-soft);
}
.resolution-pair-future .resolution-pair-text {
  color: var(--ink);
}

/* Meta line: component(s) and stages where the resolution lands.
   Sits at the bottom of the pair, separated by a hairline. The
   component names and stage references read as small chip-shaped
   tags via the inline structure; the strong labels (Component,
   Active across) anchor the meaning. Quieter than the body so it
   reads as supporting detail, not as a third claim. */
.resolution-pair-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
  padding: var(--pad-s) var(--pad-l);
  border-top: 1px solid var(--rule);
  background: color-mix(in srgb, var(--ink-muted) 4%, var(--bg-elev));
  font-size: 0.8rem;
  color: var(--ink-muted);
  line-height: 1.5;
  letter-spacing: 0.01em;
  margin: 0;
}
.resolution-pair-meta strong {
  color: var(--ink-soft);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 0.7rem;
  margin-right: 0.25rem;
}


/* ---------- Part 5 / Takeaway: scope boundaries ----------
   Honest list of what the artifact's recommendation does NOT
   cover. Smaller register than the resolution pairs above (asides,
   not claims). Numbered list so each constraint reads as a
   discrete acknowledgment. Earns its place when the artifact's
   ask is substantial enough that the reader needs to know what
   2026 (or whatever scope) does NOT touch. */
.scope-boundaries {
  margin-top: var(--pad-l);
  padding: var(--pad-m);
  background: color-mix(in srgb, var(--ink-muted) 3%, var(--bg-elev));
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.scope-boundaries-eyebrow {
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 600;
  margin: 0 0 0.5rem;
}
.scope-boundaries-intro {
  font-size: 0.95rem;
  color: var(--ink);
  line-height: 1.55;
  margin: 0 0 var(--pad-s);
  max-width: 70ch;
}
.scope-boundaries-list {
  margin: 0;
  padding-left: 1.5rem;
  display: grid;
  gap: 0.65rem;
}
.scope-boundaries-list li {
  font-size: 0.9rem;
  color: var(--ink-soft);
  line-height: 1.55;
  max-width: 70ch;
}
.scope-boundaries-list li strong {
  color: var(--ink);
  font-weight: 600;
}


/* ---------- Sources sidecar with cross-linking ---------- */
.page-footer {
  padding: var(--pad-l) var(--pad-m);
  background: var(--bg-elev);
  border-top: 1px solid var(--rule);
}
.sources-details {
  max-width: var(--container-max);
  margin: 0 auto;
}
.sources-summary {
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink);
  padding: 0.5rem 0;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
.sources-summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.sources-summary::before {
  content: "+";
  font-size: 1.25rem;
  font-weight: 400;
  color: var(--ink-muted);
  transition: transform 0.2s;
  display: inline-block;
  width: 1rem;
}
.sources-details[open] .sources-summary::before {
  content: "-";
}
.sources-summary::-webkit-details-marker { display: none; }
.sources-list {
  margin: var(--pad-s) 0 0;
  padding: 0;
  display: grid;
  gap: var(--pad-s);
}
.sources-list dt {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-muted);
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.sources-qualifier {
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-soft);
  font-style: italic;
}
.sources-backlink {
  font-size: 0.72rem;
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px dotted var(--accent);
}
.sources-backlink:hover, .sources-backlink:focus-visible {
  border-bottom-style: solid;
}
.sources-list dd {
  margin: 0.25rem 0 0;
  padding: 0;
  font-size: 0.9rem;
  color: var(--ink-soft);
  line-height: 1.5;
}

/* Source-flash animation for cross-linking */
.source-flash {
  animation: sourceFlash 1.5s ease-out;
}
@keyframes sourceFlash {
  0%, 100% { background: transparent; }
  20% { background: var(--accent-soft); }
}
@media (prefers-reduced-motion: reduce) {
  .source-flash {
    animation: none;
    outline: 2px solid var(--accent);
    outline-offset: 2px;
  }
}


/* ---------- Map-only view toggle ----------
   Two reading modes:
     1. Narrative (default) -- all sections visible
     2. Map only -- body[data-view="map"] hides every section except
        .section-journey, plus the section nav and footer.
   Toggle button sits next to the theme pill in the top bar.
   Keyboard: m toggles, Esc exits. Persists in localStorage. */
.view-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  height: 34px;
  padding: 0 0.75rem;
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: var(--ink-soft);
  cursor: pointer;
  font-size: 0.8rem;
  font-weight: 500;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
  white-space: nowrap;
}
.view-toggle:hover {
  border-color: var(--accent);
  color: var(--accent);
}
.view-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.view-toggle[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
body[data-theme="dark"] .view-toggle[aria-pressed="true"] {
  color: var(--bg);
}
/* In map-only mode, compress to icon-only */
body[data-view="map"] .view-toggle .view-toggle-label {
  display: none;
}
body[data-view="map"] .view-toggle {
  padding: 0 0.5rem;
  width: 34px;
}
/* Hide everything except the journey section in map-only mode */
body[data-view="map"] .section:not(.section-journey),
body[data-view="map"] .top-nav,
body[data-view="map"] .page-footer,
body[data-view="map"] .btt {
  display: none !important;
}
body[data-view="map"] .section-journey {
  padding-top: 1.5rem !important;
  padding-bottom: 2rem !important;
}
/* Hide the "Part 3" eyebrow in map-only -- no other parts to anchor against */
body[data-view="map"] .section-journey .section-label {
  display: none;
}


/* ---------- Stage header gradient + splash on hover ----------
   The stages row has ONE solid gradient line across its top -- a single
   continuous bar that fades from accent (start) to red (end). Each stage
   tile is otherwise neutral; only the top stripe carries the gradient.

   On hover (or when col-highlight is applied), every cell in that column
   gets a top-anchored color splash that slides down the column. The
   splash color comes from --col-glow, set per stage column below so it
   matches the stage row gradient.

   IMPORTANT: the gradient bar is rendered as a top border on each stage
   head (not a row-level ::before) so the row stays NON-positioned --
   that lets position:sticky on .bp-label use .blueprint as its scroll
   ancestor and the label column stays pinned during horizontal scroll. */
.bp-row.bp-stages .bp-stage-head {
  position: relative;
  overflow: hidden;
  background: var(--bg);
  transition: background-color 0.25s cubic-bezier(0.16, 1, 0.3, 1);
  border-top: 3px solid var(--col-glow, var(--accent));
}
/* Stages-row label needs to match the stage heads' border-top so the
   row baselines line up, and needs z-index above the row's splash
   ::after pseudos so the label content isn't dimmed. */
.bp-row.bp-stages > .bp-label {
  background: var(--bg);
  border-top: 3px solid transparent;
  z-index: 6;
}
.bp-stage-num {
  background: var(--bg-elev);
  border: 1.5px solid var(--rule);
  color: var(--ink-soft);
  position: relative;
  z-index: 2;
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1),
              border-color 0.25s ease,
              color 0.25s ease;
}

/* Per-stage glow color is set by app.js at boot (applyStageGlows).
   For each [data-stage] element, JS samples the stages-row gradient
   at that stage's center position -- (N - 0.5) / total -- and writes
   --col-glow inline. This works for any stage count and keeps helper
   elements (effort hits, sticky-header clones) in sync without
   stage-specific CSS rules.

   Default fallback (used until JS runs, or if JS is disabled): every
   stage falls back to --accent so the column treatment still reads. */
[data-stage] { --col-glow: var(--accent); }

/* The splash itself: a top-anchored gradient that grows from height: 0
   to height: 100%. Lives ONLY on the stage header cell -- it's the
   header that gets the showy splash. Other cells in the column use the
   simpler .col-highlight background tint defined further down. */
.bp-stage-head::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 0;
  background: linear-gradient(180deg,
    var(--col-glow, transparent) 0%,
    transparent 100%);
  opacity: 0.18;
  transition: height 0.35s cubic-bezier(0.16, 1, 0.3, 1);
  pointer-events: none;
  z-index: 0;
}
.bp-stage-head:hover::after,
.bp-stage-head.col-highlight::after {
  height: 100%;
}
/* When the stage header is hovered/highlighted, the number tile pops
   forward subtly. */
.bp-stage-head:hover .bp-stage-num,
.bp-stage-head.col-highlight .bp-stage-num {
  border-color: var(--col-glow, var(--accent));
  color: var(--col-glow, var(--accent));
  transform: scale(1.06);
}

/* Cell content sits above the splash */
.bp-c > * { position: relative; z-index: 1; }
.bp-stage-head > * { position: relative; z-index: 1; }


/* ---------- Tasteful animations: reveal-on-scroll ----------
   When the journey is in view, each row fades in. We use ONLY opacity
   (not transform) because transform on a row creates a containing
   block that breaks position:sticky on the .bp-label children -- the
   left label column would stop sticking during horizontal scroll.
   Fade-only keeps the cascade elegant without that side effect.
   Single bezier (CS Help curve), short duration, no distraction.
   Runs once per page load. Respects prefers-reduced-motion via the
   global reset at the top.

   Cascade ordering: app.js writes --reveal-i (the row's index inside
   .blueprint) onto each .bp-row at boot. CSS multiplies that by 50ms
   so layers cascade top-down without enumerating row classes. Adding
   or removing rows works without touching CSS. */
.blueprint .bp-row {
  opacity: 0;
  transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--reveal-i, 0) * 50ms);
}
.blueprint.is-revealed .bp-row { opacity: 1; }


/* ---------- Cell hover pop ----------
   Cells lift slightly when hovered. Subtle. Stops the blueprint from
   feeling static while reading. */
.bp-c {
  transition:
    background-color 0.2s cubic-bezier(0.16, 1, 0.3, 1),
    box-shadow 0.2s ease;
}
.bp-c:hover {
  z-index: 2;
}


/* ---------- Loop pill: marching dashes (CS Help pattern) ----------
   The dashed border on .lp-pill marches around the perimeter, signaling
   continuous backtracking. Animation is slow enough to be ambient,
   not distracting. */
@keyframes lp-march {
  to { stroke-dashoffset: -72; }
}


/* ---------- Column highlight on hover ----------
   Hovering any cell lights up its entire column via .col-highlight.
   JS adds the class to all elements with matching data-stage.
   The tint uses each column's --col-glow so the highlight matches
   the stage header splash and the gradient bar above. */
.col-highlight {
  background-color: color-mix(in srgb, var(--col-glow, var(--accent)) 8%, transparent) !important;
  transition: background-color 0.15s ease;
}
.bp-stage-head.col-highlight {
  background: color-mix(in srgb, var(--col-glow, var(--accent)) 14%, var(--bg));
}


/* ---------- Sticky stage header on scroll ----------
   Appears when blueprint is in view AND user has scrolled past the real
   stage header row. JS positions left/width to match the blueprint wrap
   so columns align exactly. JS also mirrors horizontal scroll. */
/* ---------- Sticky stage header on scroll ----------
   Appears when blueprint is in view AND user has scrolled past the real
   stage header row. JS positions left/width to match the blueprint wrap
   so columns align exactly. JS also mirrors horizontal scroll position
   onto the inner cloned blueprint.

   Architecture:
     .bp-sticky-header  -- fixed-position wrapper, clipped horizontally
     .blueprint (clone) -- inside the wrapper, owns horizontal scroll
                           (so the cloned label can position:sticky to
                           its own scrollport) -- but JS hides the
                           scrollbar and writes scrollLeft to mirror
                           the real blueprint. */
.bp-sticky-header {
  position: fixed;
  top: 50px; /* below top-bar */
  z-index: 90;
  background: var(--bg-elev);
  border-bottom: 1px solid var(--rule);
  box-shadow: 0 4px 12px -8px rgba(0,0,0,0.15);
  opacity: 0;
  /* Use opacity-only for show/hide -- transform on a fixed element
     would create a containing block and break the sticky-left on the
     cloned .bp-label inside. */
  transition: opacity 0.15s ease;
  pointer-events: none;
  /* Outer wrapper: no scrolling here. The cloned .blueprint inside
     owns the scroll (overflow-x: auto from base .blueprint rule),
     which makes it the scrollport for sticky-left on the cloned
     label cell. JS keeps that scrollLeft synced with the real
     blueprint so columns align. */
  overflow: hidden;
}
.bp-sticky-header::-webkit-scrollbar { display: none; }
.bp-sticky-header.is-visible {
  opacity: 1;
  pointer-events: auto;
}
.bp-sticky-header .blueprint {
  border: 0;
  border-radius: 0;
  background: transparent;
  /* Don't set min-width here. The clone .blueprint must size to its
     wrapper (which JS pegs to the live blueprint's bounding rect) so
     that scrollWidth - clientWidth equals the live blueprint's
     scrollable distance. The .bp-row inside still carries
     min-width: calc(...) from the base rule, so it overflows
     internally and the .blueprint scrolls horizontally just like the
     live one. With those two widths matched, absolute scrollLeft
     mirroring keeps every column lined up. */
  scrollbar-width: none;
}
.bp-sticky-header .blueprint::-webkit-scrollbar { display: none; }
/* Sticky header clone: bypass the reveal-cascade opacity:0 (the cloned
   stage row would otherwise inherit those styles and stay invisible). */
.bp-sticky-header .blueprint .bp-row,
.bp-sticky-header .blueprint .bp-row.bp-stages {
  opacity: 1 !important;
  transform: none !important;
}
.bp-sticky-header .bp-stage-head {
  padding: 0.35rem 0.6rem 0.4rem;
  background: var(--bg-elev);
  /* Compress the stage head: actor sub-line and any decoration drops
     so the cloned head reads as a column-identifier (just number +
     name), not as content. Keeps the sticky strip slim. */
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.55rem;
}
.bp-sticky-header .bp-stage-head .bp-stage-num {
  /* Smaller number tile in the sticky strip -- still recognizable
     but doesn't dominate vertical real estate. */
  width: 1.4rem;
  height: 1.4rem;
  font-size: 0.72rem;
  flex-shrink: 0;
}
.bp-sticky-header .bp-stage-head .bp-stage-name {
  font-size: 0.85rem;
  font-weight: 600;
  line-height: 1.2;
  /* Truncate so long stage names don't force the row taller. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bp-sticky-header .bp-stage-head .bp-stage-actor {
  /* Actor sub-line is editorial detail, not orientation -- drops in
     the clone. Live row keeps it. */
  display: none;
}
.bp-sticky-header .bp-stage-head .bp-stage-slide-cue {
  /* Slide-cue glyph stays for click target but smaller. */
  top: 0.2rem;
  right: 0.3rem;
}
.bp-sticky-header .bp-label {
  background: var(--bg-elev);
  font-size: 0.75rem;
  padding: 0.4rem 0.75rem;
}

/* Slim phases strip in the sticky header. Compresses vertical padding
   so it adds about 1.6rem of height total when phases are present --
   enough to read the chapter label without crowding the stage row
   below. The horizontal padding stays at the live blueprint value so
   the spans still align column-for-column. Cloned phases inherit the
   per-phase --col-glow and grid-column from the original (cloneNode
   copies inline styles), so positioning is automatic. */
.bp-sticky-header .bp-phase {
  padding: 0.25rem var(--pad-s);
  background: var(--bg-elev);
  /* Drop the bottom border so the strip reads as one cohesive header. */
  border-bottom: 0;
}
.bp-sticky-header .bp-phase-label {
  font-size: 0.62rem;
  letter-spacing: 0.16em;
}
/* The phases label cell in the sticky strip uses the same compressed
   padding as the stages label cell so vertical alignment matches. */
.bp-sticky-header .bp-layer-phases > .bp-label {
  padding: 0.25rem 0.75rem;
}

/* ---------- Sticky-clone treatment for orienting rows ----------
   Rows in the sticky tier (Phases, Stages, and any row tagged
   .is-sticky-eligible -- defaults include Jobs to be done) get
   compressed visual treatment when cloned into the sticky strip.
   Vertical padding tightens; sub-lines drop; supports lists drop;
   row-label sub text drops. The clone reads as chrome (orientation),
   not content (data). */

/* Generic compression for any sticky-clone row label */
.bp-sticky-header .is-sticky-clone .bp-label {
  padding: 0.3rem 0.75rem;
  background: var(--bg-elev);
  /* Center the label vertically with its row content -- the row is
     a single horizontal band, the label is its identifier, both
     should sit on the same baseline. */
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.bp-sticky-header .is-sticky-clone .bp-label .bp-lbl-sub,
.bp-sticky-header .is-sticky-clone .bp-label .bp-lbl-when {
  display: none;
}

/* Jobs to be done: compress headline-only -- supports list drops in
   the sticky clone since it's editorial detail, not orientation. The
   first-person headline is what tells the reader "why is the actor
   here at this stage" and that's the orientation signal worth keeping.
   Single-line truncated headline keeps the sticky strip slim. */
.bp-sticky-header .bp-layer-jobs.is-sticky-clone {
  background: color-mix(in srgb, var(--accent-soft) 14%, var(--bg-elev));
  border-top: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
}
/* Keep the label cell in family with the row band -- otherwise the
   live row's `.bp-layer-jobs .bp-label { background: var(--bg) }`
   wins and the label sits on a different background than its row. */
.bp-sticky-header .bp-layer-jobs.is-sticky-clone .bp-label {
  background: color-mix(in srgb, var(--accent-soft) 14%, var(--bg-elev));
}
.bp-sticky-header .bp-layer-jobs.is-sticky-clone .bp-c-jobs {
  padding: 0.25rem var(--pad-s);
  gap: 0;
  /* Center single-line headline vertically in the cell so the row
     reads as a tight band, not a tall column with content floating. */
  justify-content: center;
  min-height: 0;
}
.bp-sticky-header .bp-layer-jobs.is-sticky-clone .jobs-headline {
  font-size: 0.78rem;
  line-height: 1.25;
  font-family: inherit;
  font-weight: 600;
  letter-spacing: 0;
  /* Single line max with ellipsis -- chrome shouldn't wrap. The live
     row carries the full headline + supports for readers who scroll
     back to engage with the framing. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bp-sticky-header .bp-layer-jobs.is-sticky-clone .jobs-supports {
  /* Drop supports in the sticky clone -- they're elaboration, not
     orientation. The live row carries them. */
  display: none;
}


/* ---------- Horizontal scroll arrows ----------
   Appear when blueprint overflows. Float over the wrap. */
.bp-wrap {
  position: relative;
}
/* Scroll arrows: position: fixed so they stay anchored to the
   viewport center while the user scrolls vertically. JS sets the
   left/right values to match the live blueprint's bounds, and
   toggles visibility based on whether the blueprint is in view.
   Visual treatment: filled accent disc with white chevron -- reads
   as a deliberate control without competing with map content. */
.bp-scroll-arrow {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  z-index: 80;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  border: 0;
  border-radius: 50%;
  color: var(--bg-elev);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s, background-color 0.15s, transform 0.15s ease;
  box-shadow: 0 4px 14px -2px rgba(0, 0, 0, 0.25),
              0 2px 6px -1px rgba(0, 0, 0, 0.15);
}
.bp-scroll-arrow svg {
  width: 22px;
  height: 22px;
  stroke-width: 2.5;
}
.bp-scroll-arrow:hover {
  background: color-mix(in srgb, var(--accent) 88%, black);
  transform: translateY(-50%) scale(1.06);
  box-shadow: 0 6px 18px -2px rgba(0, 0, 0, 0.32),
              0 3px 8px -1px rgba(0, 0, 0, 0.18);
}
.bp-scroll-arrow:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
/* Dark theme: keep the chevron readable when the accent token shifts */
body[data-theme="dark"] .bp-scroll-arrow {
  color: var(--bg);
  box-shadow: 0 4px 14px -2px rgba(0, 0, 0, 0.5),
              0 2px 6px -1px rgba(0, 0, 0, 0.3);
}
.bp-wrap.has-overflow.bp-in-view:not(.scrolled-start) .bp-scroll-left,
.bp-wrap.has-overflow.bp-in-view:not(.scrolled-end) .bp-scroll-right {
  opacity: 1;
  pointer-events: auto;
}


/* ---------- Time row (CS Help pattern) ---------- */
.bp-layer-time .bp-label { background: var(--bg); }
.bp-c-time {
  align-items: center;
  justify-content: center;
  text-align: center;
  min-height: 4rem;
  background: var(--bg-elev);
  flex-direction: column;
  gap: 0.3rem;
}
/* Time row sizing. Ring at 42 (was 32) gives the arc-fill room to
   communicate proportion at typical viewing distance; label at
   0.9rem (was 0.75) moves the data from minimum-legible threshold
   to scannable. Stroke width scales automatically with SVG size. */
.tm-ring {
  width: 42px;
  height: 42px;
  flex-shrink: 0;
}
.tm-bg {
  fill: none;
  stroke: var(--rule);
  stroke-width: 3;
}
.tm-fg {
  fill: none;
  stroke-width: 3;
  stroke-linecap: round;
  transition: stroke 0.2s;
}
.tm-c-ok { stroke: var(--hold-edge); }
.tm-c-mid { stroke: var(--accent); }
.tm-c-over { stroke: var(--flex-edge); }
.tm-c-bad { stroke: var(--break-edge); }
.tm-num {
  font-size: 0.9rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}
.tm-num-bad { color: var(--break-ink); }


/* ---------- Clicks row (CS Help pattern) ----------
   Wedge gradient ramp rises from bottom-left to top-right -- a single
   triangular shape that visibly accumulates effort across stages.
   Each cell carries a click count with a cursor icon prefix. */
.bp-layer-clicks .bp-label { background: var(--bg); }
.bp-layer-clicks { position: relative; }
.bp-layer-clicks::before {
  content: "";
  position: absolute;
  inset: 0;
  left: var(--bp-label-col);
  clip-path: polygon(0 100%, 100% 0, 100% 100%);
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--accent) 8%, transparent) 0%,
    color-mix(in srgb, var(--flex-edge) 16%, transparent) 65%,
    color-mix(in srgb, var(--break-edge) 28%, transparent) 100%);
  pointer-events: none;
  z-index: 0;
}
.bp-c-clicks {
  align-items: center;
  justify-content: center;
  text-align: center;
  min-height: 3rem;
  background: transparent;
  position: relative;
  z-index: 1;
  flex-direction: row;
  gap: 0.4rem;
}
.rk-num {
  font-size: 0.95rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
/* Cursor icon prefix on each click count -- mirrors CS Help mask-image
   approach using an inline SVG. */
.rk-num::before {
  content: "";
  display: inline-block;
  width: 14px;
  height: 14px;
  background: currentColor;
  opacity: 0.55;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='currentColor' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 0l16 12.279-6.951 1.17 4.325 8.817-3.596 1.734-4.35-8.879L4 20V0z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='currentColor' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 0l16 12.279-6.951 1.17 4.325 8.817-3.596 1.734-4.35-8.879L4 20V0z'/%3E%3C/svg%3E");
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
.rk-num-bad { color: var(--break-ink); }


/* ---------- Touchpoint chips (CS Help pattern) ----------
   Frontstage cells carry an .ev-desc one-liner plus .tag chips naming
   actual touchpoints. */
.bp-c-touch {
  align-items: flex-start;
  gap: 0.4rem;
}
.ev-desc {
  font-size: 0.85rem;
  color: var(--ink-soft);
  line-height: 1.4;
  margin: 0 0 0.25rem;
  font-weight: 500;
}
.tag {
  display: inline-block;
  font-size: 0.75rem;
  padding: 0.2rem 0.5rem;
  border-radius: 4px;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 500;
  margin: 0 0.25rem 0.2rem 0;
  white-space: nowrap;
}


/* ---------- Pain points row (CS Help pattern) ----------
   Severity dual-encoded:
     - .pc-critical: red filled circle with alert SVG
     - .pc-detractor: amber filled circle (no SVG)
     - .pc-ok: green tint, no severity mark
   Background gradient rises from left (calm) to right (broken) -- same
   visual logic as the clicks row, but in the red palette. */
.bp-layer-pain { position: relative; }
.bp-layer-pain::before {
  content: "";
  position: absolute;
  inset: 0;
  left: var(--bp-label-col);
  background: linear-gradient(90deg,
    transparent 0%,
    transparent 15%,
    color-mix(in srgb, var(--break-edge) 5%, transparent) 40%,
    color-mix(in srgb, var(--break-edge) 14%, transparent) 100%);
  pointer-events: none;
  z-index: 0;
}
.bp-layer-pain .bp-label { background: var(--bg); }
.bp-c-pain {
  align-items: stretch;
  flex-direction: column;
  gap: 0.4rem;
  padding: var(--pad-s);
  position: relative;
  z-index: 1;
}
.pc-item {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  padding: 0.25rem 0;
}
.pc-item + .pc-item {
  border-top: 1px solid color-mix(in srgb, var(--break-edge) 15%, transparent);
}
.pc-text {
  font-size: 0.85rem;
  color: var(--ink);
  line-height: 1.5;
}
.pc-sev-mark {
  flex-shrink: 0;
  margin-top: 3px;
  width: 9px;
  height: 9px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.pc-detractor .pc-sev-mark {
  background: var(--flex-edge);
  border-radius: 50%;
  width: 9px;
  height: 9px;
}
.pc-critical .pc-sev-mark {
  color: var(--break-ink);
  width: 13px;
  height: 13px;
}
.pc-critical .pc-sev-mark svg {
  width: 13px;
  height: 13px;
}
/* "OK" cells -- where there's no pain at this stage */
.pc-ok {
  background: color-mix(in srgb, var(--hold-bg) 35%, transparent) !important;
}
.pc-ok .pc-text {
  color: var(--hold-ink);
  font-weight: 500;
}

/* Pain row legend (kit primitive).
   Sits inside the .bp-label of bp-layer-pain. Names the two
   severity marks readers see in cells: detractor (amber dot) and
   critical (red ! glyph). Without the legend, readers infer the
   severity from cell colors alone, which is ambiguous and fails
   accessibility (color is not the only encoding of state).

   Reusable beyond the Pain row: any row that uses dual-encoded
   severity marks should use this same .bp-lbl-legend pattern in
   its label so readers learn the vocabulary in place.

   Layout: tight inline pairs (mark + name), separated by quiet
   middots, set in the kit's whisper register so the legend reads
   as quiet annotation rather than competing with the row title.
   Same compositional logic as CS Help's inline legend pattern.

   Authoring:
     <span class="bp-lbl-legend">
       <span class="bp-lbl-legend-item">
         <span class="pc-sev-mark pc-sev-mark-detractor" aria-hidden="true"></span>
         <span class="bp-lbl-legend-name">Detractor</span>
       </span>
       <span class="bp-lbl-legend-item">
         <span class="pc-sev-mark pc-sev-mark-critical" aria-hidden="true">
           <svg ...>!</svg>
         </span>
         <span class="bp-lbl-legend-name">Critical</span>
       </span>
     </span>
*/
.bp-lbl-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.6rem;
  margin-top: 0.4rem;
  padding-top: 0.4rem;
  border-top: 1px dashed color-mix(in srgb, var(--rule) 60%, transparent);
  font-size: 0.68rem;
  color: var(--ink-muted);
  line-height: 1.2;
  letter-spacing: 0.02em;
}
.bp-lbl-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
/* Quiet middot separator between adjacent legend items. Matches
   the kit's existing middot register used elsewhere (subtitle
   separators, breadcrumbs). */
.bp-lbl-legend-item + .bp-lbl-legend-item::before {
  content: "·";
  color: color-mix(in srgb, var(--ink-muted) 60%, transparent);
  margin-right: 0.25rem;
}
.bp-lbl-legend-name {
  /* Slightly tighter weight than running prose so the legend reads
     as annotation, not as a label competing with the row title. */
  font-weight: 500;
  color: var(--ink-soft);
}
/* Standalone severity-mark variants for the legend.
   The cell variants (.pc-detractor .pc-sev-mark, etc.) require an
   ancestor class. The legend has no ancestor, so it uses these
   modifier classes directly on .pc-sev-mark. Same visual output
   as the cell marks. */
.pc-sev-mark.pc-sev-mark-detractor {
  background: var(--flex-edge);
  border-radius: 50%;
  width: 9px;
  height: 9px;
}
.pc-sev-mark.pc-sev-mark-critical {
  color: var(--break-ink);
  width: 13px;
  height: 13px;
}
.pc-sev-mark.pc-sev-mark-critical svg {
  width: 13px;
  height: 13px;
}
.pc-bad {
  background: color-mix(in srgb, var(--break-bg) 50%, transparent) !important;
}


/* ---------- Resolution row ----------
   How each stage ended for the customer. Three-state per cell using
   the kit's hold/flex/break vocabulary. Distinct from Pain (only
   what's broken) and Decision gates (system-side decisions): pain
   shows what went wrong, gates show system rules, resolution shows
   the customer's outcome.

   Authoring shape per cell: colored dot + bold name + optional
   sub-line of detail. Most stages stay empty; the row teaches by
   where it does and doesn't land. Same dot vocabulary the kit uses
   for stress cards and effort dots, so readers learn the color
   language once and apply it everywhere. */
.bp-layer-resolution .bp-label { background: var(--bg); }
.bp-c-resolution {
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0.4rem;
  padding: var(--pad-s);
}
/* Resolution mark: tinted state pill mirroring the .stress-state
   primitive used by Part 4 stress cards. Same three-state vocabulary
   (hold / flex / break), same bg + ink + edge token treatment, same
   filled-circle glyph. The visual continuity reinforces semantic
   continuity: when Part 3 names a stage's outcome state and Part 4
   names a pillar's stress state, the same surface treatment carries
   the same meaning. Reader learns the pill once and recognizes it
   across the artifact.

   Distinct in form from Pain (cell-background severity gradient) and
   Needs (left-tab ID badges) so a reader scanning a column can tell
   the three rows apart by shape, not just by row label.

   Per-cell data-resolution attribute drives the pill's color tokens
   without per-cell CSS, identical to how Part 4 stress cards work. */
.resolution-mark {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 700;
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  line-height: 1.25;
  /* Default neutral fallback when data-resolution is absent. Authoring
     should always specify state, but the unset case shouldn't render
     as broken. */
  background: var(--bg-elev);
  color: var(--ink-soft);
  border: 1px solid var(--rule);
}
/* No leading dot. The chip's bg + border carry the state signal,
   the text carries the meaning. A pseudo-element dot in the same
   color band added a fourth state indicator that said the same
   thing as bg + border, didn't aid colorblind readers (same hue),
   and added visual chrome without earning its place. */
.bp-c-resolution[data-resolution="hold"] .resolution-mark {
  background: var(--hold-bg);
  color: var(--hold-ink);
  border-color: var(--hold-edge);
}
.bp-c-resolution[data-resolution="flex"] .resolution-mark {
  background: var(--flex-bg);
  color: var(--flex-ink);
  border-color: var(--flex-edge);
}
.bp-c-resolution[data-resolution="break"] .resolution-mark {
  background: var(--break-bg);
  color: var(--break-ink);
  border-color: var(--break-edge);
}
.resolution-detail {
  font-size: 0.75rem;
  color: var(--ink-soft);
  line-height: 1.4;
  margin: 0;
  /* Pill is a self-contained block; detail prose sits below at flush
     left for readability. No indentation needed since the pill no
     longer presents a vertical anchor line. */
}


/* ---------- Needs row (bp-layer-needs) ----------
   Per-stage chips citing items from a curated rubric of
   audience-side needs. Reuses the .needs-rubric-item chip
   primitive from the Needs Rubric block component (Part 1 /
   sidecar) so the journey-row chip and the Part 1 chip are
   visually identical. The reader learns the chip vocabulary
   once and applies it across surfaces.

   Sparse by design: most stages stay empty. The row teaches
   by where it does and doesn't land. 1-4 chips per stage; if
   you need more, fold to the Needs Rubric block component
   instead. */
.bp-layer-needs .bp-label { background: var(--bg); }
.bp-c-needs {
  /* Cells stack chips vertically and align top so a single chip
     doesn't center-float in a tall cell. */
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  padding: var(--pad-s);
  gap: 0.4rem;
}
.needs-cell-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
/* Chip variant inside a journey cell: tighter padding than the
   Part 1 / sidecar variant since cell width is constrained, but
   same color tokens and same dot vocabulary. */
.bp-c-needs .needs-rubric-item {
  padding: 0.45rem 0.6rem;
  gap: 0.45rem;
  font-size: 0.8rem;
  align-items: center;
}
.bp-c-needs .needs-rubric-id {
  font-size: 0.75rem;
}
.bp-c-needs .needs-rubric-statement {
  font-size: 0.75rem;
  line-height: 1.3;
  /* Statements wrap inside a constrained cell; allow line breaks
     at any opportunity so long IDs + statements don't push the
     cell wider. */
  overflow-wrap: anywhere;
}


/* ---------- Backstage System / Gap pattern (CS Help) ----------
   Each backstage cell carries two stacked blocks:
     - .bk-sys: "System" -- what supports this stage (green dot)
     - .bk-gap: "Gap" -- where it falls short (amber dot)
   The pairing makes "what's there vs what's missing" legible per stage. */
.bp-c-bk {
  flex-direction: column;
  gap: 0.5rem;
  padding: var(--pad-s);
  align-items: stretch;
}
.bk-section {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.5rem 0.6rem;
  border-radius: 5px;
}
.bk-sys { background: color-mix(in srgb, var(--hold-bg) 60%, var(--bg)); }
.bk-gap { background: color-mix(in srgb, var(--flex-bg) 50%, var(--bg)); }
.bk-label {
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  line-height: 1;
}
/* The bk-section's bg tint plus the bk-label's color encode the band
   (System = hold, Gap = flex). An inner dot would add a third signal
   saying the same thing the bg + ink already say. Retired per the
   Chip dot discipline rule (visual-language.md). */
.bk-sys .bk-label { color: var(--hold-ink); }
.bk-gap .bk-label { color: var(--flex-ink); }
.bk-text {
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--ink);
}


/* ---------- Reading guide ----------
   Optional first-time-reader primer that sits between the journey
   lede and the journey-selector. Native <details> for free disclosure
   semantics; closed by default. Four micro-cards explain the kit's
   visual encoding using glyphs that mirror the actual encoding (rows
   = layers, columns = time, hover = column highlight, ringed nodes =
   popovers). Disable by deleting the markup.

   Markup contract:
     <details class="reading-guide">
       <summary class="reading-guide-summary">...</summary>
       <div class="reading-guide-grid" role="list">
         <div class="reading-guide-card" role="listitem">
           <span class="reading-guide-glyph">SVG</span>
           <h3 class="reading-guide-card-title">Title</h3>
           <p class="reading-guide-card-body">Body</p>
         </div>
         ... three more cards ...
       </div>
     </details>

   Discipline: if a card no longer matches the kit's visual encoding,
   fix the kit OR fix the card. The guide is a contract with the
   reader; drift between guide and artifact is worse than no guide. */
.reading-guide {
  margin: var(--pad-l) 0 var(--pad-m);
  border-top: 1px solid var(--rule);
  padding-top: var(--pad-m);
}

/* Trigger: feels native, never decorative. Three honest signals:
   info glyph (this is help), label (what it does), chevron (it
   discloses). Pill shape matches the kit's other affordance pills
   (theme pill, view toggle) without copying their visual weight. */
.reading-guide-summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.4rem 0.85rem 0.4rem 0.7rem;
  margin: 0;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 999px;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.reading-guide-summary::-webkit-details-marker { display: none; }
.reading-guide-summary:hover {
  color: var(--ink);
  border-color: color-mix(in srgb, var(--accent) 35%, var(--rule));
  background: color-mix(in srgb, var(--accent-soft) 30%, transparent);
}
.reading-guide-summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  color: var(--ink);
}
.reading-guide[open] .reading-guide-summary {
  color: var(--ink);
  border-color: color-mix(in srgb, var(--accent) 50%, var(--rule));
  background: color-mix(in srgb, var(--accent-soft) 40%, transparent);
}

.reading-guide-info {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--accent);
}
.reading-guide-chevron {
  width: 10px;
  height: 10px;
  flex-shrink: 0;
  color: var(--ink-muted);
  transition: transform 0.2s ease;
  margin-left: 0.1rem;
}
.reading-guide[open] .reading-guide-chevron {
  transform: rotate(180deg);
  color: var(--accent);
}

/* Four-card grid. Auto-fits gracefully from 4-across on desktop to
   2-across on tablet to 1-across on mobile. No shadows, no borders
   thicker than 1px -- the primer must never compete with the journey
   it's introducing. */
.reading-guide-grid {
  margin-top: var(--pad-m);
  padding: var(--pad-m) var(--pad-m) calc(var(--pad-m) - 0.25rem);
  /* Chrome stripped to honor the editorial register more strictly.
     The bg-elev tint alone holds the four primer cards as one group
     without adding a competing frame. The summary pill above (with
     border) and the section's own structure carry enough framing;
     the grid doesn't need its own border to communicate "these
     belong together." */
  background: var(--bg-elev);
  border-radius: 6px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--pad-m);
}

.reading-guide-card {
  display: grid;
  grid-template-columns: 2.25rem 1fr;
  grid-template-rows: auto auto;
  column-gap: 0.85rem;
  row-gap: 0.2rem;
  align-items: start;
}
.reading-guide-glyph {
  grid-column: 1;
  grid-row: 1 / span 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 1.5rem;
  margin-top: 0.15rem;
  color: var(--accent);
  flex-shrink: 0;
}
.reading-guide-glyph svg {
  width: 100%;
  height: 100%;
  display: block;
}
.reading-guide-card-title {
  grid-column: 2;
  grid-row: 1;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0;
  line-height: 1.3;
  letter-spacing: -0.005em;
}
.reading-guide-card-body {
  grid-column: 2;
  grid-row: 2;
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--ink-soft);
  margin: 0.15rem 0 0;
  max-width: 32ch;
}

/* Reduced motion: drop the chevron rotation transition. The state
   change still happens; it just doesn't animate. */
@media (prefers-reduced-motion: reduce) {
  .reading-guide-summary,
  .reading-guide-chevron {
    transition: none;
  }
}

/* Print: always show the guide content (printed copies need the
   primer most), drop the chevron since the disclosure is moot, and
   remove the trigger frame so the page reads as continuous document. */
@media print {
  .reading-guide-summary {
    border: 0;
    padding-left: 0;
    background: transparent;
  }
  .reading-guide-chevron { display: none; }
  .reading-guide-grid {
    display: grid !important;
    background: transparent;
    border: 0;
    padding: var(--pad-s) 0 0;
  }
}


/* ---------- Journey selector (multi-journey pattern) ----------
   Tab strip that switches between multiple .bp-wrap journeys on the
   same page. Use when an artifact carries more than one journey worth
   showing (CS Help DNA: shipping + device troubleshooting). For
   single-journey artifacts (HITL, WMP, the template), skip the
   selector entirely; the .bp-wrap stands alone.

   Markup contract:
     <div class="journey-selector" role="tablist">
       <button class="jtab" data-journey="X" aria-selected="true|false">
         <span class="jtab-num">1</span>
         <span class="jtab-content">
           <span class="jtab-label">Journey name</span>
           <span class="jtab-meta">Short context</span>
         </span>
       </button>
       ...
     </div>
     <div class="bp-wrap" id="journey-X"> ... </div>
     <div class="bp-wrap" id="journey-Y"> ... </div>

   Visual treatment: numbered chips with label + meta text, sitting in
   a horizontal strip above the journey wraps. Active tab carries an
   accent underline so the connection between the selector and the
   blueprint below is unambiguous. */
.journey-selector {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0 0 var(--pad-m);
  padding: 0.25rem 0 0;
}
.journey-selector-label {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--ink);
  font-weight: 600;
  margin: 0 0 0.5rem;
}
.journey-selector-label::before {
  content: "";
  display: inline-block;
  width: 1rem;
  height: 1rem;
  background-color: var(--accent);
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M3 8h13M16 8l-4-4M16 8l-4 4M21 16H8M8 16l4-4M8 16l4 4' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/></svg>") no-repeat center / contain;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M3 8h13M16 8l-4-4M16 8l-4 4M21 16H8M8 16l4-4M8 16l4 4' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/></svg>") no-repeat center / contain;
  flex-shrink: 0;
}
.jtab {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.5rem 0.85rem 0.55rem 0.6rem;
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
  color: var(--ink-soft);
  font: inherit;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.jtab:hover {
  border-color: color-mix(in srgb, var(--accent) 50%, var(--rule));
  color: var(--ink);
}
.jtab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.jtab.active,
.jtab[aria-selected="true"] {
  border-color: var(--accent);
  color: var(--ink);
  background: var(--bg-elev);
  box-shadow: 0 0 0 1px var(--accent);
}
.jtab-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  font-size: 0.72rem;
  font-weight: 700;
  border-radius: 50%;
  background: var(--bg);
  color: var(--ink-muted);
  border: 1.5px solid var(--rule);
  flex-shrink: 0;
}
.jtab.active .jtab-num,
.jtab[aria-selected="true"] .jtab-num {
  background: var(--accent);
  color: var(--bg-elev);
  border-color: var(--accent);
}
.jtab-content {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  line-height: 1.25;
}
.jtab-label {
  font-size: 0.85rem;
  font-weight: 600;
}
.jtab-meta {
  font-size: 0.72rem;
  color: var(--ink-muted);
  font-weight: 500;
}
.jtab[aria-selected="true"] .jtab-meta { color: var(--ink-soft); }


/* Slim sticky-header variant of the journey selector. JS clones the
   original .journey-selector into each .bp-sticky-header so the reader
   can switch journeys mid-scroll without jumping back to the top
   (CS Help pattern). The sticky variant compresses padding and font
   sizes to fit alongside the cloned stage row without making the
   sticky strip too tall. */
.bp-sticky-header .journey-selector-sticky {
  /* Override the base .journey-selector flex defaults: tighter gap,
     no bottom margin (the sticky strip handles vertical rhythm). */
  display: flex;
  gap: 0.4rem;
  margin: 0;
  padding: 0.4rem var(--pad-s);
  background: var(--bg-elev);
  border-bottom: 1px solid var(--rule);
  /* Hide horizontal overflow if the strip gets too wide -- selector
     stays single-line and the reader can scroll the page if it
     truncates rather than overflowing the strip vertically. */
  overflow-x: auto;
  scrollbar-width: thin;
  flex-wrap: nowrap;
}
.bp-sticky-header .journey-selector-sticky .jtab {
  /* Compact: lower padding, smaller chip number, tighter content. */
  padding: 0.3rem 0.6rem 0.3rem 0.4rem;
  gap: 0.5rem;
  flex-shrink: 0;
}
.bp-sticky-header .journey-selector-sticky .jtab-num {
  width: 1.25rem;
  height: 1.25rem;
  font-size: 0.65rem;
}
.bp-sticky-header .journey-selector-sticky .jtab-label {
  font-size: 0.78rem;
}
.bp-sticky-header .journey-selector-sticky .jtab-meta {
  /* Drop the meta line in the sticky variant -- label alone carries
     enough context, and meta would force two-line tabs that bloat
     the strip vertically. */
  display: none;
}



   Chapter-level grouping above stages. Each .bp-phase spans a range
   of stage columns via grid-column, set by JS from data-from / data-to
   so designers don't have to translate stage numbers to grid line
   numbers. The label sits centered between two horizontal rules
   (CS Help "Between the Rounds" pattern). Color samples from the
   journey's gradient automatically -- the rules use --col-glow at the
   phase's center stage so phases inherit the kit's left-to-right
   gradient without introducing a new color system. Override per phase
   by setting --phase-color inline.

   Use when the journey has natural chapters that group stages -- onboarding
   / active / churn, before / during / after, or any tri-fold story arc.
   Skip when stages don't cluster, or when the gradient bar above stages
   already does enough. */
.bp-layer-phases .bp-label { background: var(--bg); }
.bp-phase {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.75rem;
  padding: 0.65rem var(--pad-s);
  background: var(--bg);
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}
/* Phases row: drop the right border on the last phase so the gradient
   bar at the top of stages still reads as a continuous line. */
.bp-phase:last-child { border-right: 0; }
.bp-phase-rule {
  height: 1.5px;
  background: color-mix(in srgb,
    var(--phase-color, var(--col-glow, var(--accent))) 65%,
    transparent);
  border-radius: 1px;
}
.bp-phase-label {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--phase-color, var(--col-glow, var(--accent)));
  white-space: nowrap;
}


/* ---------- Narrative row (optional) ----------
   Storytelling overlay. Each card sits CENTERED inside its stage's
   column, with a vertical accent rail along one side. The rail
   extends past the card's track-facing edge as a short leader,
   terminating in a dot on the track. The whole composition stays
   inside the stage column, so column-hover highlight remains
   coherent (the card and its dot belong to the same stage visually).

   Card body is pure typography on the canvas; the connector
   vocabulary (rail + leader + dot) carries the visual association
   to the stage in the stage's accent color so the kit's left-to-right
   gradient flows through narrative beats too.

   Authoring:
     data-stage="N"             -- which column to anchor to (required)
     data-side="above|below"    -- which side of the track (required)
     data-anchor="left|right"   -- which side of the card the rail
                                   sits on. Default is auto: alternates
                                   left/right based on stage parity so
                                   the rhythm reads as a zigzag without
                                   manual choreography. Override per
                                   card when the layout calls for it. */
.bp-layer-narrative .bp-label { background: var(--bg); }
.bp-c-narrative {
  /* Single spanning cell. Height is set by JS at boot from measured
     card heights so cards never spill into the rows above or below.
     The fallback min-height here keeps the cell from collapsing
     before JS runs.

     v0.5 polish: cards default to a single-side stack (below the
     track). The earlier above/below alternation was borrowed from
     NN/g-style experience-map callouts but didn't earn its vertical
     cost here -- alternation conveyed no semantic information and
     roughly doubled the row height for an aesthetic outcome.
     Single-side stacks let the row breathe at single-card height
     and read as a clean horizontal beat sequence. Cards with
     explicit data-side="above" still anchor above for the rare
     project that needs the dual-side rhythm. */
  grid-column: 2 / -1 !important;
  position: relative;
  min-height: 8rem;
  padding: 0;
  background: var(--bg-elev);
  overflow: visible;
}

/* Track line -- thin horizontal rule at the cell's top edge. Sized
   stronger than the row hairlines so it reads as the journey's
   spine across all three themes; the colored dots and rails sit
   above it visually thanks to higher contrast and z-index.

   When any card explicitly opts into data-side="above", JS adds
   .narr-track-centered so the track returns to vertical center
   and the dual-side rhythm reads correctly. */
.narr-track {
  position: absolute;
  top: 1.25rem;
  left: 1.5rem;
  right: 1.5rem;
  height: 1px;
  background: color-mix(in srgb, var(--ink-muted) 55%, transparent);
  pointer-events: none;
}
.narr-track.narr-track-centered {
  top: 50%;
}
body[data-theme="dark"] .narr-track {
  background: color-mix(in srgb, var(--ink-muted) 70%, transparent);
}
body[data-theme="warm"] .narr-track {
  background: color-mix(in srgb, var(--ink-muted) 60%, transparent);
}

.narr-card {
  position: absolute;
  /* Width and horizontal placement set by JS based on stage count
     (each card occupies its column's width minus side padding). */
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  /* Equal horizontal padding on both sides so the body isn't pinned
     to the rail-facing edge. The rail sits OUTSIDE the padding box
     via border-left/right. */
  padding: 0.6rem 0.75rem;
  /* Subtle frame: a faint tint of the stage's accent on top of the
     cell's elevated bg. Reads as a discrete panel without competing
     with the cells on either side. The mix lifts on column-hover via
     the existing .col-highlight rule (which writes its own background
     with !important, so it overrides this one cleanly). */
  background: color-mix(in srgb,
    var(--col-glow, var(--accent)) 4%, var(--bg-elev));
  /* Default rail: left edge, in the stage's accent color. */
  border-left: 2px solid var(--col-glow, var(--accent));
  z-index: 2;
}

/* Right-anchored variant: rail flips to right edge. */
.narr-card[data-anchor="right"] {
  border-left: 0;
  border-right: 2px solid var(--col-glow, var(--accent));
}


/* Per-stage hit zones inside the narrative spanning cell. The cell
   itself has aria-colindex="2" but no data-stage (it spans all stage
   columns), so without these the column-hover highlight wouldn't fire
   from empty space in the row. The hits give each column a hover
   target that participates in the kit's column-highlight system.
   Same pattern as .effort-hits.

   z-index: 1 keeps these BEHIND cards (which sit at z-index: 2) so
   cards still receive their own hover and pointer events. */
.narr-hits {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(var(--bp-cols, 9), 1fr);
  z-index: 1;
  pointer-events: auto;
}
.narr-hit {
  transition: background-color 0.15s ease;
}
.narr-hit.col-highlight {
  background-color: color-mix(in srgb,
    var(--col-glow, var(--accent)) 8%, transparent) !important;
}

/* Leader: a 1rem extension of the rail past the card edge that faces
   the track. Same 2px width and color as the rail so they read as one
   continuous mark. */
.narr-card::before {
  content: "";
  position: absolute;
  width: 2px;
  height: 1rem;
  background: var(--col-glow, var(--accent));
  pointer-events: none;
}
.narr-card[data-anchor="left"]::before { left: -2px; }
.narr-card[data-anchor="right"]::before { right: -2px; }
.narr-card[data-side="below"]::before { top: -1rem; }
.narr-card[data-side="above"]::before { bottom: -1rem; }

/* Dot: terminates the leader on the track, at the same x-position
   as the rail. Tight halo so the dot reads cleanly against the
   track line below it. */
.narr-card::after {
  content: "";
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--col-glow, var(--accent));
  box-shadow: 0 0 0 2.5px var(--bg-elev);
  pointer-events: none;
}
.narr-card[data-anchor="left"]::after { left: -4.5px; }
.narr-card[data-anchor="right"]::after { right: -4.5px; }
.narr-card[data-side="below"]::after { top: calc(-1rem - 3.5px); }
.narr-card[data-side="above"]::after { bottom: calc(-1rem - 3.5px); }

.narr-card-eyebrow {
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--col-glow, var(--accent));
}
.narr-card-body {
  font-size: 0.82rem;
  line-height: 1.5;
  margin: 0;
  color: color-mix(in srgb, var(--ink) 92%, var(--ink-soft));
}

/* Closer treatment: the resolution / takeaway card.
   Marked with data-role="closer". Anchors to the rightmost stage
   automatically (JS handles); receives heavier visual weight so
   the artifact's editorial closer reads as structurally distinct
   from per-stage beats. The rail thickens, the eyebrow tightens,
   the body inks darker. Same vocabulary as Section 4's centerpiece
   treatment in start-here.html: heavier weight + accent-colored
   body fragment to mark a card carrying more meaning than its
   row siblings. */
.narr-card[data-role="closer"] {
  border-left-width: 3px;
  background: color-mix(in srgb,
    var(--col-glow, var(--accent)) 8%, var(--bg-elev));
}
.narr-card[data-role="closer"][data-anchor="right"] {
  border-left: 0;
  border-right-width: 3px;
}
.narr-card[data-role="closer"]::before {
  width: 3px;
}
.narr-card[data-role="closer"]::after {
  width: 9px;
  height: 9px;
}
.narr-card[data-role="closer"][data-anchor="left"]::after {
  left: -5.5px;
}
.narr-card[data-role="closer"][data-anchor="right"]::after {
  right: -5.5px;
}
.narr-card[data-role="closer"] .narr-card-eyebrow {
  letter-spacing: 0.14em;
}
.narr-card[data-role="closer"] .narr-card-body {
  color: var(--ink);
  font-weight: 500;
}



   Mark a cell with .bp-c-bad to flag a stage where the action breaks
   down. The cell picks up a left accent strip in break-edge and a
   subtle break-bg tint -- no new color introduced; same break semantic
   used in stress cards, pain points, and time-ring overruns.
   Headline + sub structure stays the same. */
.bp-c-bad {
  background: color-mix(in srgb, var(--break-bg) 35%, var(--bg-elev));
  box-shadow: inset 2px 0 0 var(--break-edge);
}
.bp-c-bad .bp-c-headline {
  color: var(--break-ink);
}

/* ---------- Cell list (multi-step within one stage) ----------
   When a single cell genuinely contains more than one discrete action,
   replace .bp-c-sub with a .bp-c-list ordered list. Numbers come from
   <ol> automatically -- no inline indices needed. Stays compact so a
   row of multi-step cells still scans top-down without bloating. */
.bp-c-list {
  list-style: none;
  counter-reset: bpli;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.bp-c-list li {
  counter-increment: bpli;
  position: relative;
  padding-left: 1.25rem;
  font-size: 0.78rem;
  color: var(--ink-soft);
  line-height: 1.4;
}
.bp-c-list li::before {
  content: counter(bpli);
  position: absolute;
  left: 0;
  top: 0.05rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 0.95rem;
  height: 0.95rem;
  border-radius: 50%;
  border: 1px solid var(--rule);
  background: var(--bg-elev);
  font-size: 0.6rem;
  font-weight: 600;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}


/* ---------- Actors rows (paired: human + digital) ----------
   Two rows distinguish HUMAN actors from DIGITAL actors. The split is
   structural (who's doing the work), not state -- so it's encoded as
   FORM (icon + border style) rather than color. The kit's color budget
   stays reserved for state, severity, and accent.
     - Human chips: solid border, person icon
     - Digital chips: dashed border, screen/system icon
   Both share the same neutral chip foreground/background.
   The Digital row's label uses .bp-label-pair to drop the title (since
   the label "Actors" already named the pair on the Human row). */
.bp-layer-actors .bp-label { background: var(--bg); }
.bp-actors-human .bp-label { padding-bottom: 0.5rem; }
.bp-actors-digital .bp-label { padding-top: 0.5rem; border-top: 0; }
.bp-label-pair {
  background: var(--bg);
}
.bp-label-pair .bp-lbl-sub {
  font-size: 0.78rem;
}
/* Human row sits flush above Digital row (no border between them) */
.bp-actors-human .bp-c { border-bottom-color: transparent; }

.bp-c-actor {
  align-items: center;
  justify-content: center;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 0.3rem;
  min-height: 2.5rem;
  padding: 0.4rem 0.5rem;
}

/* Actor row label glyphs: inline person / screen marks next to
   the Human / Digital sub-labels. Reinforces the chip vocabulary
   used inside the row. The label glyph mirrors the chip glyph at
   12px (chip glyph is 13). Smaller and lower-opacity so the label
   reads as label, not chip. */
.bp-layer-actors .bp-lbl-sub {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}
.actor-label-glyph {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  opacity: 0.7;
}
.actor-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.2rem 0.55rem 0.2rem 0.4rem;
  border-radius: 999px;
  white-space: nowrap;
  background: var(--bg-elev);
  color: var(--ink-soft);
}
.actor-chip-icon {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  opacity: 0.85;
}
/* Human: solid border, person icon (already in markup) */
.actor-human {
  border: 1px solid var(--rule);
}
/* Digital: dashed border, screen/system icon (already in markup) */
.actor-digital {
  border: 1px dashed var(--ink-muted);
  background: color-mix(in srgb, var(--bg) 60%, var(--bg-elev));
}

/* Multi-actor cells: when a single cell has more than one chip of
   the same kind (two humans, two digital), they need to read as
   distinct actors at a glance. The chips differentiate primarily
   through their authored labels -- builders name the actors
   ("Customer" / "CS associate"); the names do the work.

   CSS adds a quiet visual rhythm so two same-kind chips don't read
   as duplicates: the second chip gets a slightly muted background.
   No chrome added, no labels overridden -- just enough form delta
   for the eye to register "two different actors." Position 3+ steps
   the muting further; rare, included for completeness. */
.actor-chip.actor-chip-pos-2 {
  background: color-mix(in srgb, var(--bg) 70%, var(--bg-elev));
  color: var(--ink-muted);
}
.actor-chip.actor-chip-pos-2 .actor-chip-icon {
  opacity: 0.7;
}
.actor-chip.actor-chip-pos-3,
.actor-chip.actor-chip-pos-4 {
  background: transparent;
  color: var(--ink-muted);
  border-style: dotted;
}
.actor-chip.actor-chip-pos-3 .actor-chip-icon,
.actor-chip.actor-chip-pos-4 .actor-chip-icon {
  opacity: 0.55;
}


/* ---------- Channels row (optional) ----------
   What surface the customer is on at each stage. Channel chips share
   the actor-chip vocabulary (icon + label, no per-channel color) so
   the kit's color budget stays reserved for state and severity. The
   icon distinguishes the channel: phone, chat, email, web, app, etc. */
.bp-layer-channel .bp-label { background: var(--bg); }
.bp-c-channel {
  align-items: flex-start;
  justify-content: center;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 0.3rem;
  min-height: 2.5rem;
  padding: 0.4rem 0.5rem;
}
.channel-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.2rem 0.55rem 0.2rem 0.4rem;
  border-radius: 4px;
  white-space: nowrap;
  background: var(--bg-elev);
  color: var(--ink-soft);
  border: 1px solid var(--rule);
}
.channel-chip-icon {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  opacity: 0.85;
}


/* ---------- Customer Thoughts row (optional) ----------
   First-person inner monologue. Each cell holds an unordered list of
   thoughts (questions, concerns, doubts). Italic, quote-style, slightly
   recessed background. Bullets are small dots in ink-muted -- enough
   structure to scan multiple thoughts without competing with the
   stronger row labels above. */
.bp-layer-thoughts .bp-label { background: var(--bg); }
.bp-c-thought {
  background: color-mix(in srgb, var(--bg) 70%, var(--bg-elev));
  padding: var(--pad-s);
  align-items: flex-start;
}
.ca-thoughts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.ca-thoughts li {
  position: relative;
  padding-left: 0.85rem;
  font-size: 0.82rem;
  font-style: italic;
  line-height: 1.45;
  color: var(--ink-soft);
}
.ca-thoughts li::before {
  content: "";
  position: absolute;
  left: 0.15rem;
  top: 0.6em;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--ink-muted);
  opacity: 0.7;
}


/* ---------- Effort arc row (optional) ----------
   A single cell that spans all stage columns. Inside: an SVG path that
   curves through effort levels (low/medium/high) with colored dots at
   each stage. Use when effort variation is part of the argument. */
.bp-layer-effort .bp-label { background: var(--bg); }
.bp-c-effort {
  /* Span all stage columns -- the arc is a single visual across the row */
  grid-column: 2 / -1 !important;
  padding: 0.5rem var(--pad-s) 0.5rem 0;
  min-height: 4.5rem;
  position: relative;
  background: color-mix(in srgb, var(--bg) 60%, var(--bg-elev));
  overflow: visible;
}
.effort-arc-wrap {
  /* Positioning context for the SVG, axis labels, ref line, AND
     the .effort-node click targets. Fixed height so the SVG below
     can absolute-fill it without stretching to its viewBox aspect.
     z-index 3 here lifts the wrap (and its node buttons) above the
     sibling .effort-hits layer at z-index 2; pointer-events: none on
     the wrap itself lets the hits below still capture mouseover for
     column highlight in the gaps between buttons. */
  position: relative;
  width: 100%;
  height: 4.5rem;
  z-index: 3;
  pointer-events: none;
}
.effort-arc {
  /* Fill the wrap exactly. Absolute positioning keeps the SVG's CSS
     box independent of its viewBox aspect ratio -- combined with
     preserveAspectRatio="none" on the SVG, the path stretches to fill
     any wrap dimensions. */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 2;
  /* Visual-only -- node buttons are siblings, not children */
  pointer-events: none;
}
.effort-axis-label {
  /* Sticky-left so the High / Medium / Low labels stay anchored to
     the left edge of the blueprint scrollport during horizontal
     scroll. The offset uses --bp-label-col (the sticky label column
     width) plus a small inset so the labels appear just inside the
     stage area, never overlapping the label column. Same shape as
     .bp-label's sticky-left. Vertical positioning is handled by the
     .effort-axis flex-column wrapper below. */
  position: sticky;
  left: calc(var(--bp-label-col) + 0.5rem);
  font-size: 0.65rem;
  color: var(--ink-soft);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  pointer-events: none;
  z-index: 4;
  background: color-mix(in srgb, var(--bg) 70%, var(--bg-elev));
  padding: 0.05rem 0.35rem;
  border-radius: 3px;
  width: max-content;
  align-self: flex-start;
}
/* Container for the three axis labels. Flex-column with
   space-between distributes them vertically across the wrap
   height. Sits behind the SVG arc (z-index handled by labels
   themselves). pointer-events: none so the row's column highlight
   still works through the labels. */
.effort-axis {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 0.25rem 0;
  pointer-events: none;
  z-index: 4;
}
.effort-ref-line {
  position: absolute;
  top: 50%;
  left: 4rem;
  right: 1rem;
  height: 0;
  /* Higher-contrast dashed line, stronger than rule color so it reads
     across all three themes. Sits BEHIND the arc (z-index 1). */
  border-top: 1px dashed color-mix(in srgb, var(--ink-muted) 60%, transparent);
  pointer-events: none;
  z-index: 1;
}

/* Effort nodes: shared positioning + visible dot. JS positions each
   .effort-node by percentage and draws the connecting curve into the
   .effort-arc SVG below. The visible dot is a ::before pseudo so the
   interactive variant can keep a larger click hit area while the
   visual stays small.

   INTERACTIVITY DISCIPLINE: only nodes that open a popover get the
   discoverability ring + hover + cursor treatment. Static nodes render
   as flat dots -- no false affordance. Mark interactive ones by using
   <button class="effort-node" data-popover-target="..."> and static
   ones with <span class="effort-node">. Both share the same dot color
   (from data-effort), the same coordinate system, and the same
   reveal/scroll behavior; only the ring + hover differ. */
.effort-node {
  position: absolute;
  width: 9px;
  height: 9px;
  margin-left: -4.5px;
  margin-top: -4.5px;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 50%;
  z-index: 3;
  pointer-events: none;
  transition: background-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.effort-node::before {
  /* The visible dot. Color from data-effort. */
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  background: var(--accent);
  pointer-events: none;
  transition: transform 0.15s ease;
}
.effort-node[data-effort="low"]::before  { background: var(--hold-edge); }
.effort-node[data-effort="mid"]::before  { background: var(--flex-edge); }
.effort-node[data-effort="high"]::before { background: var(--break-edge); }

/* Interactive variant: any <button class="effort-node"> -- typically
   one with data-popover-target set. Larger hit area, discoverability
   ring, hover state, focus ring. The ring is meaningful: it tells the
   reader "this dot has more detail behind it." Static dots have none. */
button.effort-node {
  width: 22px;
  height: 22px;
  margin-left: -11px;
  margin-top: -11px;
  cursor: pointer;
  pointer-events: auto;
  /* Resting-state ring -- discoverability cue */
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink-muted) 35%, transparent);
}
button.effort-node:hover {
  background-color: color-mix(in srgb, var(--accent) 18%, transparent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 50%, transparent),
              0 0 0 6px color-mix(in srgb, var(--accent) 15%, transparent);
  transform: scale(1.15);
}
button.effort-node:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  background-color: color-mix(in srgb, var(--accent) 18%, transparent);
}

/* Legacy: kept for any project still using separate .effort-dot +
   .effort-node-btn pairs. New work should use the unified .effort-node. */
.effort-dot {
  position: absolute;
  width: 9px;
  height: 9px;
  margin-left: -4.5px;
  margin-top: -4.5px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 3;
}
.effort-dot[data-effort="low"]  { background: var(--hold-edge); }
.effort-dot[data-effort="mid"]  { background: var(--flex-edge); }
.effort-dot[data-effort="high"] { background: var(--break-edge); }
/* Per-stage hit zones: invisible overlay grid that gives each effort
   stage its own data-stage hit target. Without these, the spanning
   cell's data-stage="1" would highlight stage 1 across the whole row.
   Column count comes from --bp-cols so this works for any stage count;
   JS generates the per-stage .effort-hit children in initEffortRows. */
.effort-hits {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(var(--bp-cols, 9), 1fr);
  z-index: 2;
}
.effort-hit {
  /* Transparent hit target -- catches hover and gets data-stage from
     the markup so the column highlight handler picks the right stage.
     When a stage is highlighted across the row, the matching hit zone
     paints the column-glow tint over its slice of the effort cell. */
  transition: background-color 0.15s ease;
}
.effort-hit.col-highlight {
  background-color: color-mix(in srgb, var(--col-glow, var(--accent)) 8%, transparent) !important;
}

/* Legacy: .effort-node-btn was the old name for the click target before
   it was unified with the visible dot into a single .effort-node. Kept
   as alias rules so any in-flight projects don't break on upgrade. */
.effort-node-btn {
  position: absolute;
  width: 22px;
  height: 22px;
  margin-left: -11px;
  margin-top: -11px;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  z-index: 3;
  transition: background-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink-muted) 35%, transparent);
}
.effort-node-btn:hover {
  background-color: color-mix(in srgb, var(--accent) 18%, transparent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 50%, transparent),
              0 0 0 6px color-mix(in srgb, var(--accent) 15%, transparent);
  transform: scale(1.15);
}
.effort-node-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  background-color: color-mix(in srgb, var(--accent) 18%, transparent);
}


/* ---------- Emotion row (optional) ----------
   Custom face glyphs (positive / neutral / negative / anxious) paired
   with inline feeling chips. The glyph signals valence at a glance,
   the chips name the specific feelings. Both are readable WITHOUT
   clicking -- popovers are opt-in for stages where research provenance
   or longer detail is worth pulling forward.

   Glyphs are SVG symbols defined once at the top of the document and
   referenced via <use href="#emotion-N"/>. Color comes from the kit's
   hold/break tokens via .emotion-positive / .emotion-neutral /
   .emotion-negative / .emotion-anxious modifiers on the wrapper. */
.bp-layer-emotion .bp-label { background: var(--bg); }
.bp-c-emotion {
  background: color-mix(in srgb, var(--bg) 60%, var(--bg-elev));
  align-items: center;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 0.5rem;
  min-height: 2.75rem;
  padding: 0.5rem 0.6rem;
}

/* Face glyph wrapper. Renders the SVG via <use> at a consistent
   size. Color flows through currentColor on the <use> reference.
   Default size raised from 22 to 36. At 22 the face read as a
   token; at 36 expression detail (eye placement, brow lines,
   mouth shape) carries through to the reader. The vertical-stack
   variant (.bp-layer-emotion.is-emotion-headline) pushes further
   to 42 when emotion is the row's argument. */
.emotion-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  flex-shrink: 0;
  color: var(--ink-muted);
}
.emotion-glyph svg {
  width: 100%;
  height: 100%;
  display: block;
}
/* Severity color routing. Form (glyph shape) carries the precise
   affect; color carries the severity band. Frustrated and angry
   sit in the break band (peak negative). Resigned sits in the
   muted-ink range (withdrawn, distinct from active negative). */
.emotion-positive   { color: var(--hold-edge); }
.emotion-neutral    { color: var(--ink-muted); }
.emotion-negative   { color: var(--break-edge); }
.emotion-anxious    { color: var(--flex-edge); }
.emotion-frustrated { color: var(--break-edge); }
.emotion-angry      { color: var(--break-edge); }
.emotion-resigned   { color: var(--ink-muted); }

/* Variant: emotion as headline. Vertical stack (face above chips)
   for artifacts whose argument is load-bearing on the customer
   emotional arc. Apply `.is-emotion-headline` to `.bp-layer-emotion`
   to opt in. The face becomes the first signal the eye registers
   per stage; chips become labels for what the face has already
   communicated. Default treatment stays side-by-side because most
   journeys carry emotion as one row among many, not as the headline. */
.bp-layer-emotion.is-emotion-headline .bp-c-emotion {
  flex-direction: column;
  gap: 0.5rem;
}
.bp-layer-emotion.is-emotion-headline .emotion-glyph {
  width: 42px;
  height: 42px;
}
.bp-layer-emotion.is-emotion-headline .emotion-feelings {
  justify-content: center;
  flex: 0 1 auto;
}

/* Glyph as a popover trigger (only on stages with optional depth).
   Subtle resting-state hint that this one is interactive, distinct
   from the static glyphs around it. */
.emotion-glyph-btn {
  background: transparent;
  border: 0;
  padding: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
  /* Resting-state ring -- discoverability cue */
  box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent);
}
.emotion-glyph-btn:hover {
  transform: scale(1.12);
  box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 50%, transparent),
              0 0 0 6px color-mix(in srgb, currentColor 14%, transparent);
}
.emotion-glyph-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Inline feeling chips next to the glyph -- the load-bearing content,
   readable at a glance without clicking. */
.emotion-feelings {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  flex: 1;
  min-width: 0;
}
.emotion-feeling {
  display: inline-flex;
  align-items: center;
  font-size: 0.7rem;
  font-weight: 500;
  padding: 0.1rem 0.45rem;
  border-radius: 3px;
  white-space: nowrap;
  background: var(--bg);
  color: var(--ink-soft);
  border: 1px solid var(--rule);
}
.emotion-feeling-positive {
  background: color-mix(in srgb, var(--hold-bg) 60%, transparent);
  color: var(--hold-ink);
  border-color: color-mix(in srgb, var(--hold-edge) 30%, transparent);
}
.emotion-feeling-negative {
  background: color-mix(in srgb, var(--break-bg) 55%, transparent);
  color: var(--break-ink);
  border-color: color-mix(in srgb, var(--break-edge) 30%, transparent);
}
.emotion-feeling-neutral {
  background: var(--bg);
  color: var(--ink-soft);
  border-color: var(--rule);
}
/* Anxious-band feeling. Mirrors the existing chip pattern with
   flex-bg / flex-ink / flex-edge tokens. Closes a face-chip color
   coherence gap: anxious-faced cells previously forced red chips
   (no anxious chip class existed), breaking the rule that face
   and chip sit in the same severity band per cell unless the
   content genuinely calls for mixed-band chips. */
.emotion-feeling-anxious {
  background: color-mix(in srgb, var(--flex-bg) 60%, transparent);
  color: var(--flex-ink);
  border-color: color-mix(in srgb, var(--flex-edge) 30%, transparent);
}


/* ---------- Popover component (.bp-popover) ----------
   Reusable progressive-disclosure pattern. Triggered by any element
   with [data-popover-target="ID"] -- the ID matches the popover panel.
   JS positions the popover next to the trigger, traps focus, and
   handles Esc / click-outside dismiss.
   Used by effort dots, emotion dots, and any future node-style row. */
.bp-popover {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1000;
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 8px;
  box-shadow: 0 12px 32px -8px rgba(0,0,0,0.18),
              0 4px 12px -4px rgba(0,0,0,0.10);
  padding: 1rem 1.25rem 1.1rem;
  min-width: 18rem;
  max-width: 22rem;
  opacity: 0;
  transform: scale(0.96);
  pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s cubic-bezier(0.16, 1, 0.3, 1);
}
.bp-popover.is-open {
  opacity: 1;
  transform: scale(1);
  pointer-events: auto;
}

/* Pointer arrow: a 12px triangle that anchors the popover to its
   trigger. Two stacked pseudos -- the outer matches the popover border,
   the inner sits 1px in to mask that border line so the triangle reads
   as a continuation of the popover surface, not a separate shape.
   JS sets:
     --bp-pop-side -- which side the arrow points from (top|bottom|left|right)
     --bp-pop-arrow -- horizontal/vertical offset along that side
   so the arrow always points back at the trigger even when the popover
   is clamped to the viewport edge. */
.bp-popover::before,
.bp-popover::after {
  content: "";
  position: absolute;
  width: 0;
  height: 0;
  pointer-events: none;
}
/* Default (popover above trigger) -- arrow on bottom edge pointing down */
.bp-popover {
  --bp-pop-arrow: 50%;
}
.bp-popover[data-pop-side="top"]::before,
.bp-popover[data-pop-side="top"]::after {
  left: var(--bp-pop-arrow);
  top: 100%;
  transform: translateX(-50%);
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
}
.bp-popover[data-pop-side="top"]::before {
  border-top: 8px solid var(--rule);
}
.bp-popover[data-pop-side="top"]::after {
  border-top: 8px solid var(--bg-elev);
  margin-top: -1px;
}
/* Popover below trigger -- arrow on top edge pointing up */
.bp-popover[data-pop-side="bottom"]::before,
.bp-popover[data-pop-side="bottom"]::after {
  left: var(--bp-pop-arrow);
  bottom: 100%;
  transform: translateX(-50%);
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
}
.bp-popover[data-pop-side="bottom"]::before {
  border-bottom: 8px solid var(--rule);
}
.bp-popover[data-pop-side="bottom"]::after {
  border-bottom: 8px solid var(--bg-elev);
  margin-bottom: -1px;
}
/* Popover left of trigger -- arrow on right edge pointing right */
.bp-popover[data-pop-side="left"]::before,
.bp-popover[data-pop-side="left"]::after {
  top: var(--bp-pop-arrow);
  left: 100%;
  transform: translateY(-50%);
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
}
.bp-popover[data-pop-side="left"]::before {
  border-left: 8px solid var(--rule);
}
.bp-popover[data-pop-side="left"]::after {
  border-left: 8px solid var(--bg-elev);
  margin-left: -1px;
}
/* Popover right of trigger -- arrow on left edge pointing left */
.bp-popover[data-pop-side="right"]::before,
.bp-popover[data-pop-side="right"]::after {
  top: var(--bp-pop-arrow);
  right: 100%;
  transform: translateY(-50%);
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
}
.bp-popover[data-pop-side="right"]::before {
  border-right: 8px solid var(--rule);
}
.bp-popover[data-pop-side="right"]::after {
  border-right: 8px solid var(--bg-elev);
  margin-right: -1px;
}
.bp-popover-close {
  position: absolute;
  top: 0.4rem;
  right: 0.5rem;
  background: transparent;
  border: 0;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  font-size: 1.25rem;
  line-height: 1;
  color: var(--ink-muted);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.15s, color 0.15s;
}
.bp-popover-close:hover {
  background: var(--bg);
  color: var(--ink);
}
.bp-popover-close:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---- Hover-to-open model ----
   Desktop hover opens the FULL popover (not a peek preview). Click
   does the same on any device. Touch and coarse-pointer devices
   skip hover entirely (gated in JS via window.matchMedia).

   Two states inside the popover:
     - .is-open without .is-pinned: hover-opened. Closes on mouseleave
       (with grace period to transit between trigger and popover).
       Tabbing away closes it. No focus shift, no focus trap.
     - .is-open with .is-pinned: click-opened. Survives mouseleave.
       Focus moves to close button, focus trap installed, dismissed
       by Esc / × / click-outside.
     - Clicking a hover-opened popover promotes it to pinned.

   Same content either way; hover is just a faster path to the same
   reading experience. */
.bp-popover-eyebrow {
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: 0.4rem;
}
.bp-popover-title {
  font-size: 1rem;
  font-weight: 600;
  margin: 0 0 0.6rem;
  color: var(--ink);
  padding-right: 1.5rem;
}
.bp-popover-body {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--ink-soft);
  margin: 0 0 0.85rem;
}
.bp-popover-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  align-items: center;
  margin-bottom: 0.6rem;
}
.bp-popover-chip-label {
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
  width: 100%;
  margin-bottom: 0.2rem;
}
.emotion-chip {
  display: inline-flex;
  align-items: center;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.18rem 0.55rem;
  border-radius: 4px;
  background: var(--bg);
  color: var(--ink-soft);
  border: 1px solid var(--rule);
}
.emotion-chip-positive {
  background: color-mix(in srgb, var(--hold-bg) 70%, transparent);
  color: var(--hold-ink);
  border-color: color-mix(in srgb, var(--hold-edge) 35%, transparent);
}
.emotion-chip-negative {
  background: color-mix(in srgb, var(--break-bg) 65%, transparent);
  color: var(--break-ink);
  border-color: color-mix(in srgb, var(--break-edge) 35%, transparent);
}
.emotion-chip-neutral {
  background: var(--bg);
  color: var(--ink-soft);
  border-color: var(--rule);
}
.bp-popover-meta {
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.bp-popover-meta-label {
  font-size: 0.65rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.bp-popover-meta-value {
  font-size: 0.78rem;
  color: var(--ink-soft);
}


/* ---------- Insights row (optional) ----------
   Sits at the bottom of the blueprint. Heavier top separator + elevated
   background so it reads as a separate section that still aligns with
   stage columns. */
.bp-layer-insights {
  border-top: 2px solid var(--rule);
  background: color-mix(in srgb, var(--accent-soft) 35%, var(--bg-elev));
}
.bp-label-insights {
  background: color-mix(in srgb, var(--accent-soft) 50%, var(--bg)) !important;
  color: var(--accent);
}
.bp-label-insights .bp-lbl-title { color: var(--accent); }
.bp-c-insight {
  background: transparent;
  align-items: flex-start;
  gap: 0.4rem;
  padding: var(--pad-s);
  min-height: 5rem;
}
.insight-eyebrow {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent);
}
.insight-text {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--ink);
  margin: 0;
}


/* ---------- Recommendations row (optional) ----------
   Pairs with Insights above. Insights = observed read. Recs = imperative
   action. Same structural shell so the two rows read as a paired closer
   to the journey grid; discipline says no new color, so the differentiator
   is FORM: each rec cell carries a 2px left-rail accent (echoing the Part
   5 .recommendation block) so the row reads as "do this," not "this is
   true." */
.bp-layer-recs {
  border-top: 1px solid var(--rule);
  background: var(--bg-elev);
}
.bp-label-recs {
  background: var(--bg-elev) !important;
}
.bp-label-recs .bp-lbl-title { color: var(--ink); }
.bp-c-rec {
  background: transparent;
  align-items: flex-start;
  gap: 0.4rem;
  padding: var(--pad-s);
  /* Left-rail accent: 2px stripe along the inner edge of every recs
     cell. The stripe IS the affordance signal -- "this is action." */
  border-left: 2px solid var(--accent);
  min-height: 5rem;
}
.bp-c-rec.bp-c-empty {
  /* Empty cells drop the rail so they don't masquerade as actions. */
  border-left: 2px solid transparent;
}
.rec-eyebrow {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent);
}
.rec-text {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--ink);
  margin: 0;
}


/* ---------- Journey closer (optional) ----------
   Post-grid block at the end of Part 3. Pulls systemic conclusions
   out of the journey into stated form. Pairs with the Recs row at
   different altitudes: Recs = per-stage tactical, closer = whole-
   journey systemic. Mirrors HITL's part3-implications pattern.

   Visual register: pulled-quote tier. Display font + larger size +
   citation in the kit's whisper voice. No card frame, no shadow --
   the editorial weight is what makes it land, not decoration. */
.section-inner-post-bp {
  /* Wrapper that re-applies the section's container max-width to
     post-blueprint content, so the closer aligns with section copy
     even though the .bp-wrap above broke out of it. */
  max-width: var(--container-max);
  margin: 0 auto;
}
.journey-closer {
  display: flex;
  flex-direction: column;
  gap: var(--pad-m);
  margin: 4rem 0 0;
  padding: 2.5rem 0 1rem;
  border-top: 1px solid var(--rule);
}
.journey-closer-eyebrow {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin: 0;
}
.journey-closer-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3rem;
  align-items: start;
}
.journey-implication {
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
/* Vertical hairline between paired anchors. The second anchor gets
   the rule on its left edge so it reads as a divider, not a frame. */
.journey-implication + .journey-implication {
  border-left: 1px solid var(--rule);
  padding-left: 3rem;
}
.journey-implication-anchor {
  font-size: clamp(1.3rem, 1.1rem + 0.5vw, 1.55rem);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0;
  margin: 0;
  color: var(--ink);
}
.journey-implication-text {
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--ink-soft);
  margin: 0;
  max-width: 38ch;
}
.journey-implication-citation {
  font-size: 0.75rem;
  font-style: italic;
  color: var(--ink-muted);
  margin: 0;
  max-width: 38ch;
}

/* Single-anchor layout: when only one .journey-implication is
   present, the pair grid collapses to one column at full width. */
.journey-closer-pair:has(.journey-implication:only-child) {
  grid-template-columns: 1fr;
}

/* Mobile: stack the pair, drop the vertical rule, replace with a
   horizontal one above the second anchor. */
@media (max-width: 720px) {
  .journey-closer { margin-top: 3rem; padding-top: 2rem; }
  .journey-closer-pair {
    grid-template-columns: 1fr;
    gap: 2.25rem;
  }
  .journey-implication + .journey-implication {
    border-left: 0;
    border-top: 1px solid var(--rule);
    padding-left: 0;
    padding-top: 2.25rem;
  }
}


/* ---------- Loop row (CS Help pattern) ----------
   Each .lp-pill uses left: -50% to physically straddle the boundary
   between this cell and the previous one. The pill IS the visual span
   between two columns; no SVG arc. The dashed border on the pill
   reinforces "loops back across this boundary".
   Place the pill in the cell that is the DESTINATION of the loop (where
   the customer lands after looping back). */
.bp-layer-loop .bp-label { background: var(--bg); }
.bp-c-loop {
  background: var(--bg-elev);
  min-height: 3.5rem;
  align-items: center;
  justify-content: center;
  padding: 0.6rem 0;
  position: relative;
  overflow: visible;
  /* Column dividers KEPT (inherited from .bp-c). The long pill
     with marching dashes still visually crosses them (z-index 2
     puts it above), and the dividers help readers see exactly
     which stages the pill spans. Earlier iteration retired these
     borders to keep the row visually quiet, but reader feedback
     was the opposite: without column lines, the loop's span isn't
     legible. Same hairline as every other row. */
}
.lp-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  background: var(--bg-elev);
  color: var(--flex-ink);
  border: 2px dashed var(--flex-edge);
  border-radius: 999px;
  padding: 0.3rem 0.85rem;
  font-size: 0.78rem;
  font-weight: 600;
  white-space: nowrap;
  position: relative;
  left: -50%;
  z-index: 2;
  /* Pills are visual only -- never intercept hover, so column highlight
     works whenever a pill overlays a cell. */
  pointer-events: none;
}
.lp-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--flex-edge);
}

/* LONG loop variant: the pill itself widens via --lp-span to physically
   span the loop. --lp-span = number of stage hops the loop covers
   (= cell-widths of pill width). No separate SVG arc -- the pill IS
   the visual span, same vocabulary as the short loop. */
.bp-c-loop-long {
  /* Allow the wide pill to overflow leftward across prior cells */
  overflow: visible;
}
.lp-pill-long {
  /* Anchor the pill's right edge at this cell's center; extend leftward
     by --lp-span * cell-widths so the pill spans from origin (its left
     end) to destination (this cell's center). */
  position: absolute;
  top: 50%;
  right: 50%;
  transform: translateY(-50%);
  /* Width = span hops (left of this cell's center) + 0.5 cell to center pill horizontally on this cell */
  width: calc((var(--lp-span, 2) + 0.5) * 100% - var(--pad-s) * 2);
  justify-content: center;
  z-index: 2;
  /* Reset the short-loop's left:-50% offset */
  left: auto;
}


/* ---------- Screenshots row ---------- */
/* The thumbnail is a real interactive element, not a placeholder.
   Frame styling reads as "device screen" (rounded corners, subtle
   elevation), not "image upload zone" (dashed border, generic icon).
   The .ev-thumb-mock SVG inside fills the frame edge-to-edge so each
   stage's mock reads as the actual screen at thumbnail size. The
   lightbox renders the same SVG at full canvas size on click. */
.bp-layer-screenshots .bp-label { background: var(--bg); }
.bp-c-shot {
  background: var(--bg-elev);
  align-items: stretch;
  justify-content: center;
  padding: 0.4rem;
  min-height: 5.25rem;
}
.ev-thumb-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  width: 100%;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--ink-soft);
  transition: color 0.15s ease;
}
.ev-thumb-btn:hover { color: var(--ink); }
.ev-thumb-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 8px;
}

/* Stage wrapper: holds the frame plus its badges. The wrapper is
   inline-flex sized to its content (the frame), with a small inset
   gutter that the badges live in. This keeps the badges near the
   thumbnail at any cell width without ever overlapping the mock
   itself, and without ballooning out when the cell is wide.
   The wrapper is the positioning context for both badges. */
.ev-thumb-stage {
  position: relative;
  display: inline-flex;
  align-self: center;
  /* The padding IS the badge gutter -- enough room for badges to sit
     at the corners without crowding. Tuned to read as breathing room,
     not awkward space. */
  padding: 0.85rem 0.85rem 0;
  margin: 0 auto;
}

/* The frame is the visible "device" -- a rounded rectangle with a
   1px outline and a soft shadow so it lifts off the row. Hover
   strengthens the rim and shadow to signal the affordance.
   Discipline: no transform on hover; badges sit absolutely on the
   parent stage wrapper, so any frame transform would create
   subpixel shifts on the badge corners. Stronger border + shadow
   is the safer cue. */
.ev-thumb-frame {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 5;
  max-width: 5rem;
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 1px 0 color-mix(in srgb, var(--ink) 5%, transparent);
  transition: border-color 0.15s ease, box-shadow 0.2s ease;
}
.ev-thumb-btn:hover .ev-thumb-frame {
  border-color: color-mix(in srgb, var(--accent) 50%, var(--rule));
  box-shadow: 0 2px 8px color-mix(in srgb, var(--ink) 14%, transparent);
}
.ev-thumb-btn:focus-visible .ev-thumb-frame {
  border-color: var(--accent);
}
.ev-thumb-mock {
  display: block;
  width: 100%;
  height: 100%;
  color: var(--ink);
}

/* Expand icon: top-LEFT corner of the stage wrapper. Sits OUTSIDE
   the frame in the wrapper's gutter, so the mock screen is never
   obscured by the affordance. Always visible (not hover-revealed);
   the reader knows on first glance the thumbnail is interactive.
   Pattern mirrors CS Help's expand-icon overlay. */
.ev-thumb-expand {
  position: absolute;
  top: 0;
  left: 0;
  width: 1.4rem;
  height: 1.4rem;
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 2;
  box-shadow: 0 1px 2px rgba(0,0,0,0.06);
  color: var(--ink-soft);
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.ev-thumb-expand svg {
  width: 60%;
  height: 60%;
  display: block;
}
.ev-thumb-btn:hover .ev-thumb-expand {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 60%, var(--rule));
  background: var(--bg-elev);
}

/* Set badge: top-RIGHT corner of the stage wrapper. Sits opposite
   the expand icon and outside the frame for the same reason -- never
   covers the mock. Pure text ("1 of N") -- the badge IS the signal. */
.ev-thumb-set-badge {
  position: absolute;
  top: 0;
  right: 0;
  font-size: 0.65rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  pointer-events: none;
  z-index: 2;
  line-height: 1;
  box-shadow: 0 1px 2px rgba(0,0,0,0.06);
  white-space: nowrap;
}
.ev-thumb-btn:hover .ev-thumb-set-badge {
  color: var(--ink);
  border-color: color-mix(in srgb, var(--accent) 50%, var(--rule));
}

.ev-thumb-btn .whisper {
  font-size: 0.7rem;
  letter-spacing: 0.02em;
}

/* Reduced motion: drop the shadow transition; keep border-color cue. */
@media (prefers-reduced-motion: reduce) {
  .ev-thumb-frame,
  .ev-thumb-btn:hover .ev-thumb-frame {
    transition: border-color 0.15s ease;
  }
  .ev-thumb-expand,
  .ev-thumb-btn:hover .ev-thumb-expand {
    transition: color 0.15s ease, border-color 0.15s ease;
  }
}

/* Legacy placeholder class -- kept for backward compatibility with
   artifacts that haven't migrated to .ev-thumb-frame yet. New artifacts
   should use the frame markup. */
.ev-thumb-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
}
.ev-thumb-placeholder svg {
  opacity: 0.6;
}


/* ---------- Lightbox (HITL / CS Help pattern) ----------
   Floating pill header with prev/next/dots, separate close button,
   content area with image. Built dynamically by app.js. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 99999;
  background: rgba(0,0,0,0);
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  visibility: hidden;
  pointer-events: none;
  transition: background 0.3s ease, visibility 0s 0.35s;
}
.lightbox.open {
  visibility: visible;
  pointer-events: auto;
  background: rgba(0,0,0,0.85);
  transition: background 0.3s ease, visibility 0s 0s;
}

/* Hide top bar and sticky header when lightbox is open */
body:has(.lightbox.open) .top-bar,
body:has(.lightbox.open) .bp-sticky-header {
  opacity: 0;
  pointer-events: none;
}

/* Floating pill header. The header is `position: fixed` so it can
   sit above the dimmed lightbox content, but its visibility is tied
   to .lightbox.open via the parent state. When the lightbox closes,
   the header fades in lockstep with the dim backdrop -- no orphaned
   pill lingering after close. */
.lightbox-header {
  position: fixed;
  top: 20px;
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  padding: 0 20px;
  z-index: 100001;
  pointer-events: none;
}
.lb-top-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  pointer-events: none;
}
.lb-top-row > * { pointer-events: auto; }

.lb-pill {
  display: flex;
  align-items: stretch;
  background: rgba(0,0,0,0.75);
  border-radius: 50px;
  border: 1px solid rgba(255,255,255,0.06);
  box-shadow: 0 8px 32px rgba(0,0,0,0.4);
  pointer-events: auto;
  width: 460px;
  max-width: calc(100vw - 120px);
  overflow: hidden;
  position: relative;
}
.lb-header-center {
  flex: 1;
  text-align: center;
  min-width: 0;
  padding: 14px 8px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.lb-step-counter {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-bottom: 6px;
}
.lb-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: none;
  padding: 0;
  background: rgba(255,255,255,0.2);
  cursor: pointer;
  transition: background 0.2s, transform 0.2s;
}
.lb-dot:hover { background: rgba(255,255,255,0.5); transform: scale(1.3); }
.lb-dot-visited { background: rgba(255,255,255,0.45); }
.lb-dot-active { background: #fff; }
.lb-dot-active:hover { background: #fff; }
.lb-dot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.lb-step-title {
  font-size: 14px;
  font-weight: 400;
  color: #fff;
  line-height: 1.3;
}
.lb-step-num {
  font-weight: 400;
  color: rgba(255,255,255,0.75);
}
.lb-step-name { font-weight: 600; }
.lb-step-set {
  font-weight: 400;
  font-size: 0.85em;
  color: rgba(255,255,255,0.6);
  margin-left: 0.25rem;
}
.lb-step-desc {
  font-size: 11px;
  font-weight: 400;
  color: rgba(255,255,255,0.7);
  line-height: 1.3;
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 320px;
  margin-left: auto;
  margin-right: auto;
}

.lb-pill-nav {
  width: 44px;
  min-width: 44px;
  height: 44px;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: rgba(255,255,255,0.5);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: background 0.2s, color 0.2s;
  margin: auto 6px;
}
.lb-pill-nav:hover { background: rgba(255,255,255,0.1); color: #fff; }
.lb-pill-nav[hidden] { visibility: hidden; }
.lb-pill-nav:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.lb-close-float {
  width: 56px;
  height: 56px;
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: 50%;
  background: rgba(0,0,0,0.7);
  color: rgba(255,255,255,0.5);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  pointer-events: auto;
  transition: background 0.2s, color 0.2s;
  box-shadow: 0 4px 16px rgba(0,0,0,0.3);
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 100003;
}
.lb-close-float:hover { background: rgba(0,0,0,0.8); color: #fff; }
.lb-close-float:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Parent-state-tied visibility: when the lightbox is closed, the
   floating header, close button, and keyboard hint fade out in
   lockstep with the dim backdrop. Without this, those elements
   (which are position: fixed at high z-indexes to escape the dimmed
   surface) linger at full opacity for the duration of the JS cleanup
   timeout. Tied to the same 0.3s window as the .lightbox background
   transition so the whole close animation reads as one motion. */
.lightbox-header,
.lb-close-float,
.lb-keyboard-hint {
  opacity: 1;
  transition: opacity 0.25s ease;
}
.lightbox:not(.open) .lightbox-header,
.lightbox:not(.open) .lb-close-float,
.lightbox:not(.open) .lb-keyboard-hint {
  opacity: 0;
  pointer-events: none;
}
/* On open, the keyboard hint fades in slightly later so the eye
   lands on the screenshot first, then notices the affordance. The
   delay matches HITL's pattern -- 300ms past the open transition. */
.lightbox.open .lb-keyboard-hint {
  transition: opacity 0.4s ease 0.3s;
}

/* Keyboard shortcut hint -- top-LEFT corner, opposite the floating
   close button (top-right). Pure decoration; AT users already get
   the keyboard model from the dialog's own semantics. Tokens kept
   in line with the existing dark-on-dark lightbox vocabulary so the
   hint reads as part of the modal, not a tooltip. */
.lb-keyboard-hint {
  position: fixed;
  top: 28px;
  left: 24px;
  background: rgba(0,0,0,0.75);
  border: 1px solid rgba(255,255,255,0.06);
  color: rgba(255,255,255,0.6);
  font-size: 11px;
  letter-spacing: 0.01em;
  padding: 6px 14px;
  border-radius: 20px;
  z-index: 100002;
  pointer-events: none;
  box-shadow: 0 4px 12px rgba(0,0,0,0.3);
  white-space: nowrap;
}
.lb-keyboard-hint kbd {
  display: inline-block;
  background: rgba(255,255,255,0.12);
  border-radius: 3px;
  padding: 1px 5px;
  font-family: inherit;
  font-size: 10px;
  color: rgba(255,255,255,0.78);
  margin: 0 1px;
}
/* Reduced motion: drop the open-fade delay so the hint appears
   immediately instead of feeling like a sluggish tooltip. */
@media (prefers-reduced-motion: reduce) {
  .lightbox.open .lb-keyboard-hint {
    transition: opacity 0.01ms;
  }
}
/* Print: the hint is purely interactive, no role on a printed page. */
@media print {
  .lb-keyboard-hint { display: none; }
}

.lightbox-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  padding: 100px 40px 40px;
  transform: scale(0.98);
  opacity: 0;
  transition: transform 0.25s ease, opacity 0.2s ease;
  margin: auto;
  min-width: min-content;
  will-change: opacity, transform;
}
.lightbox.open .lightbox-content {
  transform: scale(1);
  opacity: 1;
}
/* Lightbox content layout. Single-screen stages render one centered
   focal screen at near-fullscreen height. Multi-screen stages render
   every screen for the stage side by side, equal height, in a
   horizontally scrollable row -- the reader sees the whole set at a
   glance and can scroll if there are too many to fit. Mirrors the CS
   Help pattern. */
.lightbox-imgs {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 24px;
  max-width: 95vw;
  max-height: 86vh;
}
.lightbox-imgs.is-set {
  /* When the set overflows the viewport horizontally, allow scroll
     instead of squashing screens. Padding so the first/last frames
     get breathing room when scrolled to the edge. */
  overflow-x: auto;
  overflow-y: hidden;
  padding: 0 16px;
  justify-content: flex-start;
  scrollbar-color: rgba(255,255,255,0.3) transparent;
}
.lightbox-imgs.is-set::-webkit-scrollbar { height: 10px; }
.lightbox-imgs.is-set::-webkit-scrollbar-track { background: transparent; }
.lightbox-imgs.is-set::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,0.25);
  border-radius: 999px;
}
.lightbox-imgs img {
  height: 78vh;
  width: auto;
  max-width: none;
  border-radius: 12px;
  box-shadow: 0 24px 60px rgba(0,0,0,0.45);
  object-fit: contain;
  flex-shrink: 0;
}
.lightbox-imgs.is-single img {
  height: auto;
  max-height: 86vh;
  max-width: 92vw;
}
/* Legacy single-image class -- kept for backward compatibility. */
.lightbox-imgs img.lightbox-single {
  height: auto;
  max-height: 86vh;
  max-width: 92vw;
}

/* Lightbox mock frame: container for the kit's mock and storyboard
   symbols. Aspect ratio is set per-frame inline (from the source
   symbol's viewBox) so phone-portrait mocks and landscape storyboard
   panels both render correctly without clipping or squashing. The
   .lb-mock-frame-story modifier swaps the height-cap for a width-cap
   so wide panels don't shrink to phone-frame proportions. */
.lb-mock-frame {
  height: 78vh;
  max-width: 92vw;
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 24px 60px rgba(0,0,0,0.45);
  color: var(--ink);
  flex-shrink: 0;
}
.lb-mock-frame-story {
  /* Landscape panel: width-bounded instead of height-bounded so it
     doesn't render at phone-frame width. */
  height: auto;
  width: min(82vw, 1100px);
  max-height: 82vh;
}
.lightbox-imgs.is-single .lb-mock-frame {
  height: 80vh;
  max-width: 92vw;
}
.lightbox-imgs.is-single .lb-mock-frame-story {
  height: auto;
  width: min(86vw, 1200px);
  max-height: 84vh;
}
.lb-mock {
  display: block;
  width: 100%;
  height: 100%;
}
.lb-no-img {
  font-size: 15px;
  color: rgba(255,255,255,0.5);
  font-style: italic;
  padding: 80px 40px;
  text-align: center;
  background: rgba(255,255,255,0.04);
  border: 1px dashed rgba(255,255,255,0.2);
  border-radius: 8px;
  max-width: 480px;
}


/* ---------- Bridge actor lane (kept for patterns.html) ---------- */
.bp-layer-bridge .bp-c {
  background: color-mix(in srgb, var(--accent-soft) 20%, var(--bg-elev));
  border-left: 2px solid var(--accent);
}
.bp-layer-bridge .bp-label {
  border-left: 2px solid var(--accent);
}


/* ---------- Emotion arc row (kept for patterns.html) ---------- */
.bp-layer-emotion .bp-c {
  min-height: 3rem;
  align-items: center;
  justify-content: center;
  text-align: center;
}
.bp-layer-emotion .bp-label { background: var(--bg); }
.bp-emotion-calm .bp-c-headline,
.bp-emotion-relieved .bp-c-headline { color: var(--hold-ink); }
.bp-emotion-calm,
.bp-emotion-relieved { background: color-mix(in srgb, var(--hold-bg) 50%, var(--bg-elev)); }
.bp-emotion-anxious .bp-c-headline { color: var(--flex-ink); }
.bp-emotion-anxious { background: color-mix(in srgb, var(--flex-bg) 50%, var(--bg-elev)); }
.bp-emotion-frustrated .bp-c-headline { color: var(--break-ink); }
.bp-emotion-frustrated { background: color-mix(in srgb, var(--break-bg) 50%, var(--bg-elev)); }
.bp-emotion-uncertain .bp-c-headline { color: var(--ink-soft); }
.bp-emotion-uncertain { background: var(--bg-elev); }


/* ---------- Back-to-top FAB ----------
   Appears bottom-right when scrolled past 1.5 viewport heights.
   Reads as native UI chrome, not decoration: --bg-elev surface,
   --accent stroke, layered shadow that lifts on hover. The reveal
   animation slides the FAB up 8px while fading in -- the motion
   signals "appearing because something changed," not "always here
   just invisible." Theme-tuned shadows for warm and dark so the
   button never blends into the page. */
.btt {
  position: fixed;
  right: 1.5rem;
  bottom: 1.5rem;
  z-index: 90;
  width: 2.75rem;
  height: 2.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-elev);
  color: var(--accent);
  border: 1px solid var(--rule);
  border-radius: 50%;
  cursor: pointer;
  padding: 0;
  box-shadow: 0 4px 14px -6px rgba(26, 26, 26, 0.14),
              0 1px 3px rgba(26, 26, 26, 0.06);
  opacity: 0;
  pointer-events: none;
  transform: translateY(8px);
  transition: opacity 0.25s ease, transform 0.25s ease,
              background 0.2s ease, color 0.2s ease,
              border-color 0.2s ease, box-shadow 0.2s ease;
}
.btt.visible {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}
.btt:hover {
  background: var(--accent-soft);
  border-color: var(--accent);
  box-shadow: 0 6px 18px -6px color-mix(in srgb, var(--accent) 32%, transparent),
              0 1px 3px rgba(26, 26, 26, 0.08);
  transform: translateY(-2px);
}
.btt:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.btt svg {
  width: 18px;
  height: 18px;
}

/* Per-theme shadow tuning. Warm gets a warmer ambient + warmer hover
   glow; dark gets a deeper ambient + accent-blue hover glow so the
   button reads against the dark surface without going neon. */
body[data-theme="warm"] .btt {
  box-shadow: 0 4px 14px -6px rgba(42, 31, 20, 0.14),
              0 1px 3px rgba(42, 31, 20, 0.06);
}
body[data-theme="warm"] .btt:hover {
  box-shadow: 0 6px 18px -6px rgba(138, 90, 42, 0.30),
              0 1px 3px rgba(42, 31, 20, 0.08);
}
body[data-theme="dark"] .btt {
  box-shadow: 0 4px 16px -6px rgba(0, 0, 0, 0.5),
              0 1px 3px rgba(0, 0, 0, 0.3);
}
body[data-theme="dark"] .btt:hover {
  background: var(--accent-soft);
  box-shadow: 0 6px 20px -6px rgba(140, 176, 255, 0.35),
              0 1px 3px rgba(0, 0, 0, 0.3);
}

/* Reduced motion: drop the lift + slide-up reveal; keep opacity for
   the show/hide cue and color transitions for hover feedback. */
@media (prefers-reduced-motion: reduce) {
  .btt {
    transform: none !important;
    transition: opacity 0.15s ease, background 0.15s ease,
                color 0.15s ease, border-color 0.15s ease;
  }
  .btt:hover { transform: none !important; }
}

/* Mobile: tighter inset, slightly smaller target. */
@media (max-width: 720px) {
  .btt {
    right: 1rem;
    bottom: 1rem;
    width: 2.5rem;
    height: 2.5rem;
  }
  .btt svg {
    width: 16px;
    height: 16px;
  }
}


/* ---------- Whisper class: muted small text ---------- */
.whisper {
  font-size: 0.8rem;
  color: var(--ink-muted);
}


/* ---------- Section closer: display-register pull-quote ----------
   A section closer is one sentence (or a tight rhetorical phrase)
   that distills the section's argument and sits at the section's
   end as its earned conclusion. Display register matches the
   rhetorical work: triplet, parallel construction, statement-
   and-capper, imperative thesis.

   The class earns its place when the prose has the bones to
   carry display weight. Conversational caveats, factual
   qualifications, and pointers to deeper content do NOT earn
   this treatment -- they live in the muted-aside register
   (.sh-section-aside or your project's equivalent). Putting
   display register on prose that doesn't deliver dilutes both
   the closer pattern and the prose itself.

   Sized one notch above body, one notch below section title.
   Medium weight (500) so it reads as deliberate without
   competing with headings. Tight letter-spacing for the
   editorial pull-quote feel. --pad-l above so the line lands
   with proper breathing room.

   Used on start-here.html for the For-the-builder closer
   ("Bring what's true. Answer what's asked. The kit shapes
   the rest.") and the For-the-reader closer ("Your reader
   gets the artifact, not the build behind it. That's the
   whole point."). Available kit-wide for builder artifacts. */
.section-closer {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 0.85rem + 0.6vw, 1.35rem);
  font-weight: 500;
  line-height: 1.4;
  color: var(--ink);
  margin: var(--pad-l) 0 0;
  max-width: 60ch;
  letter-spacing: -0.005em;
}


/* ---------- Mobile ---------- */
@media (max-width: 720px) {
  :root { --section-pad: 3rem; }
  .top-nav { display: none; }
  .problem-thesis { font-size: 1.75rem; }
  .section-title { font-size: 1.5rem; }
  .stress-body { grid-template-columns: 1fr; }
  .stress-col { border-right: 0; border-bottom: 1px solid var(--rule); }
  .stress-col:last-child { border-bottom: 0; }
}


/* ============================================================
   ADDITIONAL COMPONENTS (CS Help patterns)
   ------------------------------------------------------------
   Available components beyond the default 5-Part shell. Use as
   needed in your project. None are required for kit conformance.
   ============================================================ */


/* ---------- Hero with count-up stat (CS Help opener pattern) ----------
   Use as an alternative to .problem-hero when your Part 1 leads with
   a single dramatic number rather than thesis + pillars.
*/
.hero-stat {
  padding: var(--pad-xl) var(--pad-m);
  background: var(--bg-elev);
  border-bottom: 1px solid var(--rule);
}
.hero-stat-inner {
  max-width: var(--container-max);
  margin: 0 auto;
}
.hero-stat-eyebrow {
  font-size: 0.85rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 600;
  margin: 0 0 var(--pad-m);
}
/* Claim is the headline. The number is the evidence inside it. */
.hero-stat-claim {
  font-size: clamp(1.8rem, 1.4rem + 1.6vw, 2.6rem);
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1.25;
  margin: 0 0 var(--pad-m);
  max-width: 28ch;
  color: var(--ink);
}
/* The number sits inline inside the claim as an accent span,
   so it reads as part of the headline sentence rather than
   competing with it. Inline-block keeps the line break behavior
   tight when the claim wraps. */
.hero-stat-num {
  font-family: var(--font-display);
  font-size: 1.15em;
  font-weight: 700;
  color: var(--accent);
  line-height: 1;
  letter-spacing: 0;
  margin: 0;
  display: inline-block;
}
.hero-stat-detail {
  font-size: 1rem;
  color: var(--ink-soft);
  max-width: 55ch;
  margin: 0;
  line-height: 1.6;
}


/* ---------- Ingress fan-in (CS Help Part 1 pattern) ----------
   Two columns of source items flow into a central hub. Use when your
   artifact answers "where do they come from / what feeds this?" The
   hub is the convergence point; the sides are the sources.
*/
.ingress {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: var(--pad-l);
  align-items: stretch;
  margin: var(--pad-l) 0;
}
.ing-side {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: var(--pad-m);
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 10px;
}
.ing-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-muted);
  font-weight: 600;
  padding-bottom: 0.65rem;
  margin-bottom: 0.35rem;
  border-bottom: 1px solid var(--rule);
}
.ing-header > :last-child {
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0;
  color: var(--ink);
  text-transform: none;
}
.ing-src {
  font-size: 0.92rem;
  padding: 0.55rem 0 0.55rem 0.85rem;
  color: var(--ink-soft);
  border-left: 2px solid color-mix(in srgb, var(--rule) 70%, transparent);
  line-height: 1.4;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.ing-src:hover {
  border-left-color: var(--accent);
  color: var(--ink);
}
.ing-src-lead {
  font-weight: 600;
  color: var(--ink);
}
.ing-src-note {
  font-size: 0.78rem;
  color: var(--ink-muted);
  margin-left: 0.4rem;
}
.ing-hub {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--pad-l) var(--pad-m);
  background: color-mix(in srgb, var(--accent-soft) 70%, var(--bg-elev));
  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--rule));
  border-radius: 12px;
  min-width: 13rem;
}
.ing-hub-title {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--accent);
  letter-spacing: 0.01em;
}
.ing-hub-num {
  font-size: 2.6rem;
  font-weight: 700;
  color: var(--accent);
  line-height: 1;
  letter-spacing: 0;
  margin: 0.5rem 0 0.35rem;
}
.ing-hub-stat {
  font-size: 0.85rem;
  color: var(--ink-soft);
}
.ing-hub-note {
  font-size: 0.75rem;
  color: var(--ink-muted);
  font-style: italic;
  margin-bottom: var(--pad-s);
  max-width: 14ch;
  line-height: 1.4;
}
@media (max-width: 720px) {
  .ingress { grid-template-columns: 1fr; }
}


/* ---------- Pattern cards (CS Help Part 3 pattern) ----------
   Grid of theme cards. Use when your artifact closes by surfacing
   patterns observed across the journey -- a "this repeats" closing.

   Layout uses flex (not CSS grid auto-fit) so the LAST ROW cards
   grow proportionally to fill the page width when card count
   doesn't divide evenly into rows. Avoids the "orphan row" effect
   where 5 cards render as 4 + 1 (one narrow card on its own line)
   and instead renders as 3 + 2 with row 2's two cards each taking
   the width of 1.5 row-1 cards. The visual rhythm reads as
   intentional rather than as "we ran out of cards."

   `flex: 1 1 280px` lets each card start at a 280px ideal, grow to
   fill its row, and never wrap below 280px (so cards never get so
   narrow they truncate the headline). `flex-grow: 1` is the move
   that makes orphan rows fill the line.

   The 5-card case requires per-card targeting: cards 1-3 stay at
   the 33% cap, cards 4-5 lift to 50% so the second row fills
   half-and-half. CSS :nth-child within a :has(exactly 5 cards)
   parent achieves this. */
.patterns-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--pad-m);
  margin: var(--pad-m) 0;
}
.pattern-card {
  flex: 1 1 280px;
  max-width: calc(33% - var(--pad-m));
  padding: var(--pad-m);
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
/* 1, 2 cards: pair at 50% each.
   3 cards: 33% each (one row).
   4 cards: 2x2 at 50% each.
   5 cards: 3+2 (1-3 at 33%, 4-5 at 50%, see per-card rule below).
   6+ cards: keep 33% default for 3-per-row dense layout. */
.patterns-grid:has(> .pattern-card:nth-child(2):last-child) .pattern-card,
.patterns-grid:has(> .pattern-card:only-child) .pattern-card {
  max-width: calc(50% - var(--pad-m) / 2);
}
.patterns-grid:has(> .pattern-card:nth-child(4):last-child) .pattern-card {
  max-width: calc(50% - var(--pad-m) / 2);
}
.patterns-grid:has(> .pattern-card:nth-child(5):last-child) > .pattern-card:nth-child(4),
.patterns-grid:has(> .pattern-card:nth-child(5):last-child) > .pattern-card:nth-child(5) {
  max-width: calc(50% - var(--pad-m) / 2);
}
@media (max-width: 720px) {
  .pattern-card,
  .patterns-grid:has(> .pattern-card:nth-child(4):last-child) .pattern-card,
  .patterns-grid:has(> .pattern-card:nth-child(3):last-child) .pattern-card,
  .patterns-grid:has(> .pattern-card:nth-child(2):last-child) .pattern-card,
  .patterns-grid:has(> .pattern-card:only-child) .pattern-card,
  .patterns-grid:has(> .pattern-card:nth-child(5):last-child) > .pattern-card:nth-child(4),
  .patterns-grid:has(> .pattern-card:nth-child(5):last-child) > .pattern-card:nth-child(5) {
    max-width: 100%;
    flex-basis: 100%;
  }
}

/* Pattern category glyph (optional). Sits at the top of the card,
   above the eyebrow. Encodes the SHAPE of the pattern (mismatch /
   drift / gap / loop / cliff / compounding) via geometry; the kit
   ships six variants in the SVG defs library at the top of each
   page. Color stays --accent so it reads as a category marker, not
   a decoration. Subtle accent-soft circle behind it lifts the glyph
   off the card surface without competing with the pattern name.
   Cards without data-pattern-type render glyph-less. */
.pattern-card-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  margin-bottom: 0.15rem;
  background: color-mix(in srgb, var(--accent-soft) 70%, var(--bg-elev));
  border-radius: 999px;
  color: var(--accent);
  flex-shrink: 0;
}
.pattern-card-glyph svg {
  width: 60%;
  height: 60%;
  display: block;
}
.pattern-card-num {
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 600;
}
.pattern-card-name {
  font-size: 1.1rem;
  font-weight: 600;
  margin: 0;
}
.pattern-card-body {
  font-size: 0.9rem;
  color: var(--ink-soft);
  margin: 0;
  line-height: 1.5;
}
.pattern-card-example {
  font-size: 0.8rem;
  color: var(--ink-muted);
  font-style: italic;
  margin-top: auto;
  padding-top: 0.5rem;
  border-top: 1px solid var(--rule);
}


/* ---------- Compressed-shape Part adjustments ----------
   When folding Part 2 (Scale) into Part 1 (Problem), use .scale-inline
   on the kpi strip so it sits inside the hero rather than as its own
   section.
*/
.scale-inline {
  margin-top: var(--pad-l);
  padding-top: var(--pad-l);
  border-top: 1px solid var(--rule);
}


/* ============================================================
   PRINT STYLES
   ------------------------------------------------------------
   When a reader exports to PDF, the artifact should remain readable:
   no fixed top bar, full blueprint, no scroll affordances, sources
   sidecar always expanded.
   ============================================================ */
@media print {
  body { background: white; color: black; }
  .top-bar, .btt, .skip-link { display: none !important; }
  .section { padding: 1.5rem 0; break-inside: avoid; page-break-inside: avoid; }
  .section-title { break-after: avoid; }
  .blueprint { min-width: 0; overflow: visible; }
  .bp-wrap { overflow: visible; }
  .sources-details[open] .sources-summary::before,
  .sources-details .sources-summary::before { content: ""; }
  .sources-details summary { font-weight: 700; }
  .sources-list { display: block !important; }
  .sources-details { open: true; }
  details { display: block; }
  details > summary { list-style: none; }
  details:not([open]) > *:not(summary) { display: block !important; }
  a { color: black; text-decoration: underline; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.8em; color: #555; }
}


/* ============================================================
   ROW TAXONOMY EXPANSIONS (added in v0.5+ polish)

   Plain-text rows polished and new rows visualized so the
   template reads as a complete menu of what's possible. Each
   block below is independent; remove rows your project doesn't
   need from template.html and the corresponding CSS stays
   inert.

   See `03-component-library.md` "Row taxonomy" for the
   inventory and reasoning behind which rows got which
   treatments.
   ============================================================ */

/* ---- Customer Actions: verb iconography ----
   A small glyph slot at the start of each cell, before the
   headline. Verbs are categorical (tap / swipe / type / wait
   / look / speak / listen / click / scroll / choose); the
   glyph encodes the category, the headline carries the
   specifics. Same pattern as channel-chip in bp-layer-channel.

   Glyph occupies a fixed 1.1rem slot so cells with and without
   verbs align horizontally. Empty slot collapses to nothing
   so cells without a verb don't carry a phantom indent. */
.bp-layer-customer .bp-c {
  position: relative;
}
.ca-verb {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem;
  height: 1.1rem;
  flex-shrink: 0;
  color: var(--ink-soft);
  margin-right: 0.4rem;
  margin-bottom: 0.15rem;
  vertical-align: middle;
}
.ca-verb svg {
  width: 100%;
  height: 100%;
  display: block;
}
/* Verb sits inline with the headline, not on its own line. */
.bp-layer-customer .bp-c-headline {
  display: inline-flex;
  align-items: center;
  gap: 0;
}

/* ---- Customer Thoughts: row label glyph ----
   Thought-bubble glyph in the row label distinguishes inner
   monologue from spoken voice (which a future Voice/Verbatim
   row would carry as a speech bubble). Cells stay quiet --
   italic quoted text is doing the work; row label carries the
/* ---- Row label glyph (canonical treatment) ----
   Some rows carry a glyph next to the row title in the label cell.
   The glyph earns its place when:
     1. The row introduces a content vocabulary cells use (thought-
        bubble, lens, arrow, target, diamond) -- the label glyph is
        the legend.
     2. The row's name is abstract enough that a glyph adds semantic
        specificity (Jobs to be done -> target; reads as goal-
        oriented, not administrative).
   The glyph does NOT belong on rows whose name carries its own
   concept (Stages, Phases, Time, Channels), or on rows whose cells
   already carry rich glyph vocabulary (Emotion, Trust, Pain).
   See `03-component-library.md` for the rule and review process.

   One canonical treatment for any row title that contains a
   .bp-lbl-glyph: consistent flex alignment, glyph sizing, spacing.
   Per-row overrides only when a row genuinely needs a different
   treatment (e.g. .bp-label-insights uses accent color for the
   title text -- color, not size, distinguishes it). */
.bp-lbl-title:has(.bp-lbl-glyph) {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.bp-lbl-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 0.95rem;
  height: 0.95rem;
  flex-shrink: 0;
  color: var(--ink-soft);
}
.bp-lbl-glyph svg { width: 100%; height: 100%; display: block; }

/* ---- Frontstage / Backstage: visible-side / hidden-side chrome ----
   Frontstage gets a subtle accent tint (3% mix on bg-elev) --
   reads as the visible side. Backstage gets a muted darker
   fill -- reads as the hidden side. The visibility line
   already separates them positionally; this layer makes the
   distinction visually explicit so a reader skimming the row
   labels alone can read which side they're on. */
.bp-layer-frontstage .bp-c {
  background: color-mix(in srgb,
    var(--accent) 3%, var(--bg-elev));
}
.bp-layer-backstage .bp-c {
  /* Already muted by the existing rule at line ~971; reinforce
     with a slightly stronger desaturation so the side reads as
     hidden, not just deeper-elevation. */
  background: color-mix(in srgb,
    var(--ink-muted) 6%, var(--bg-elev));
}
body[data-theme="dark"] .bp-layer-frontstage .bp-c {
  background: color-mix(in srgb,
    var(--accent) 4%, var(--bg-elev));
}
body[data-theme="dark"] .bp-layer-backstage .bp-c {
  background: color-mix(in srgb,
    var(--ink-muted) 8%, var(--bg-elev));
}

/* ---- Insights vs Recommendations: altitude split ----
   Insights = observation altitude. Lens glyph in the row
   label, cells stay neutral -- observation should not visually
   shout.
   Recommendations = prescription altitude. Forward-arrow
   glyph in the row label, cells get a subtle 2px accent left
   rule. The arrow vocabulary plus the rule signals "do this"
   so the row reads as more weighted than its sibling.
   The flex/gap chrome for the glyph is provided by the
   canonical `.bp-lbl-title:has(.bp-lbl-glyph)` rule above --
   no per-row overrides needed for layout, only for color
   semantics (insights uses accent text). */
.bp-layer-recs .bp-c {
  border-left: 2px solid color-mix(in srgb,
    var(--accent) 60%, transparent);
}
.bp-layer-recs .bp-c:first-of-type {
  /* The label cell still hairlines normally; only the data
     cells carry the accent rule. */
}
.bp-layer-recs .bp-label {
  border-left: none;
}

/* ============================================================
   NEW ROWS (Tier 1 from the row taxonomy Held queue)
   Trust, System effort, Decision gate, Latency.
   ============================================================ */

/* ---- Trust row ----
   Three-state per cell: trusts / verifies / doubts. State
   carries via glyph + color, not color alone (color-blind safe).
   Optional data-trust-event="gained" or "lost" adds a small
   pin to mark the moment trust was won or lost. */
.bp-layer-trust .bp-label { background: var(--bg); }
.bp-c-trust {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
  padding-top: 0.7rem;
  padding-bottom: 0.7rem;
}
.trust-state {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--ink-soft);
}
.trust-state-glyph {
  width: 1.1rem;
  height: 1.1rem;
  flex-shrink: 0;
}
.trust-state-glyph svg { width: 100%; height: 100%; display: block; }
.trust-state-trusts .trust-state-glyph {
  color: color-mix(in srgb, var(--accent) 70%, var(--ink));
}
.trust-state-verifies .trust-state-glyph {
  color: var(--ink-soft);
}
.trust-state-doubts .trust-state-glyph {
  color: color-mix(in srgb, var(--state-break, #c75a5a) 80%, var(--ink-soft));
}
.trust-state-doubts {
  color: color-mix(in srgb, var(--ink) 80%, var(--state-break, #c75a5a));
}
.trust-event {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--accent) 50%, transparent);
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.trust-event[data-event="lost"] {
  border-color: color-mix(in srgb, var(--state-break, #c75a5a) 50%, transparent);
  color: color-mix(in srgb, var(--state-break, #c75a5a) 90%, var(--ink));
  background: color-mix(in srgb, var(--state-break, #c75a5a) 8%, transparent);
}

/* ---- System effort row ----
   Sibling to the existing customer-effort row. Same proportional
   bar visual vocabulary so the two rows read as a paired set.
   Cumulative across stages; optional unit label ($, CSA-min,
   GPU-sec) lives in the row label as the sub-line. */
.bp-layer-syseffort .bp-label { background: var(--bg); }
.bp-c-syseffort {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: 0.7rem;
  padding-bottom: 0.7rem;
  gap: 0.3rem;
}
.syseffort-bar {
  position: relative;
  height: 0.5rem;
  width: 100%;
  background: color-mix(in srgb, var(--ink-muted) 12%, transparent);
  border-radius: 999px;
  overflow: hidden;
}
.syseffort-bar-fill {
  position: absolute;
  inset: 0;
  width: var(--syseffort, 0%);
  background: color-mix(in srgb, var(--col-glow, var(--accent)) 75%, var(--ink));
  border-radius: 999px;
}
.syseffort-value {
  font-size: 0.75rem;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* ---- Decision gate row ----
   Diamond glyph at any stage where a decision occurs. Most
   cells stay empty (decisions don't happen everywhere). Three
   outcome states: pass / hold / fail. Color encodes outcome,
   glyph variation reinforces it (color-blind safe). Tooltip
   on hover carries the policy text. */
.bp-layer-gate .bp-label { background: var(--bg); }
.bp-c-gate {
  display: flex;
  align-items: center;
  justify-content: center;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}
.bp-c-gate.bp-c-empty {
  /* Inherits standard empty-cell dot pattern from the kit's
     existing .bp-c-empty rule. No additional treatment needed. */
}
.gate-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  position: relative;
  /* Ring affordance: matches the kit's "this opens detail" signal
     used on effort-node and emotion-glyph-btn. The ring tells the
     reader the diamond has more behind it. Static gates (no
     popover) render without the ring; same vocabulary as effort. */
  background: transparent;
  border: 0;
  padding: 0;
  border-radius: 50%;
  color: inherit;
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
  /* Resting-state ring -- discoverability cue */
  box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent);
}
.gate-mark:hover {
  transform: scale(1.1);
  box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 50%, transparent),
              0 0 0 6px color-mix(in srgb, currentColor 14%, transparent);
}
.gate-mark:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
/* Static gate (no popover): no ring, no hover. Same shape as the
   non-button .effort-node static variant. Use .gate-mark-static
   on a <span> when the cell wants to mark the outcome glyph
   without offering depth. */
span.gate-mark,
.gate-mark-static {
  cursor: default;
  box-shadow: none;
  transform: none;
}
span.gate-mark:hover,
.gate-mark-static:hover {
  transform: none;
  box-shadow: none;
}
.gate-mark svg {
  width: 100%;
  height: 100%;
  display: block;
}
.gate-mark[data-outcome="pass"] {
  color: color-mix(in srgb, var(--state-hold, #5fa367) 80%, var(--ink));
}
.gate-mark[data-outcome="hold"] {
  color: color-mix(in srgb, #c89b3c 75%, var(--ink));
}
.gate-mark[data-outcome="fail"] {
  color: color-mix(in srgb, var(--state-break, #c75a5a) 80%, var(--ink));
}
/* .gate-policy retired in favor of the kit's standard popover
   pattern. The CSS-only tooltip didn't work on touch, broke in
   dark theme (white-on-white), and bypassed the kit's keyboard
   and dismissal contract. Decision gates with detail now use
   <button class="gate-mark" data-popover-target="popover-gate-N">
   plus a panel authored alongside effort and emotion popovers.
   The hover-peek label fires automatically on desktop. */

/* ---- Latency row ----
   Logarithmic bar: latencies span 50ms to 14 days, so a linear
   bar would compress the human-perceptible range to nothing.
   Use --latency-log to set the visual fill (0.0 = real-time,
   0.5 = minutes-to-hours, 1.0 = days-or-more). Builder
   computes the log mapping; CSS just renders.

   Color-coded by SLA breach when data-sla="breach" set on
   the cell. */
.bp-layer-latency .bp-label { background: var(--bg); }
.bp-c-latency {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: 0.7rem;
  padding-bottom: 0.7rem;
  gap: 0.3rem;
}
.latency-bar {
  position: relative;
  height: 0.45rem;
  width: 100%;
  background: color-mix(in srgb, var(--ink-muted) 10%, transparent);
  border-radius: 999px;
  overflow: hidden;
}
.latency-bar-fill {
  position: absolute;
  inset: 0;
  width: calc(var(--latency-log, 0) * 100%);
  background: color-mix(in srgb, var(--ink-soft) 60%, var(--accent));
  border-radius: 999px;
}
.bp-c-latency[data-sla="breach"] .latency-bar-fill {
  background: color-mix(in srgb, var(--state-break, #c75a5a) 75%, var(--ink));
}
.latency-value {
  font-size: 0.75rem;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.latency-band {
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.bp-c-latency[data-sla="breach"] .latency-value {
  color: color-mix(in srgb, var(--state-break, #c75a5a) 90%, var(--ink));
}


/* ============================================================
   JOBS TO BE DONE ROW
   ------------------------------------------------------------
   Goal/intent altitude. First-person headline + supporting
   nuances. Each cell spans 1+ stages via grid-column inline
   style "span N". Sits in the row stack above Customer Actions
   so the reader's eye runs intent (why) -> verbs (how).

   Visual vocabulary: subtle background tint reads as a "framing
   band" without competing with content rows below. The headline
   carries display-font weight; the supports list reads quieter
   as italic inline bullets. Compass / target glyph in the row
   label signals goal-orientation.
   ============================================================ */

.bp-layer-jobs {
  background: color-mix(in srgb, var(--accent-soft) 18%, var(--bg-elev));
  border-top: 1px solid var(--rule);
}
.bp-layer-jobs .bp-label { background: var(--bg); }
.bp-c-jobs {
  /* Stage-spanning cells -- inline grid-column "span N" sets the
     width. Reads as one continuous framing band, not a deck of
     cards: no internal dividers, asymmetric padding (horizontal
     breathes, vertical compresses) so the row sits in family with
     surrounding rows without bloating. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.4rem;
  padding: var(--pad-s) var(--pad-m);
}
.jobs-headline {
  font-family: var(--font-display, inherit);
  font-size: clamp(1rem, 0.9rem + 0.3vw, 1.2rem);
  line-height: 1.3;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--ink);
  margin: 0;
  /* Sticky-within-bounds: the paragraph anchors to the left edge
     of the blueprint scrollport (just past the sticky label column)
     for the duration of its parent cell's stage range. As the user
     pans right, the paragraph tracks the visible viewport so the
     job's intent stays readable. When the cell's right edge passes,
     the sticky bound releases and the paragraph scrolls off; the
     next job's headline takes over.

     The paragraph stays full-cell-width (the flex parent's default
     stretch) so its text is left-aligned within at rest. Sticky
     only affects the paragraph's LEFT edge clamp during scroll;
     layout stays unchanged at rest. text-wrap: balance retired
     because it can center-wrap short headlines, which would read
     as off-left even at rest. */
  position: sticky;
  left: calc(var(--bp-label-col) + var(--pad-m));
  z-index: 2;
}
.jobs-supports {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.22rem;
}
.jobs-supports li {
  font-size: 0.82rem;
  line-height: 1.4;
  color: var(--ink-soft);
  font-style: italic;
  position: relative;
  padding-left: 0.85rem;
}
.jobs-supports li::before {
  /* Quiet square bullet matching the kit's def-points style. */
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 0.32rem;
  height: 0.32rem;
  background: color-mix(in srgb, var(--accent) 60%, transparent);
}


/* ============================================================
   KEY MOMENTS ROW
   ------------------------------------------------------------
   Moments-of-truth altitude. 1-4 critical events per stage
   that shape recall. Sits between Customer Actions and Customer
   Thoughts -- consequential events orient the more granular
   internal monologue that follows.

   Visual vocabulary: vertical list of items, each with a small
   diamond glyph in a severity-colored circle (matching the
   pain row's per-item severity vocabulary). Most items neutral;
   only critical-success or critical-failure earn color.
   ============================================================ */

.bp-layer-moments .bp-label { background: var(--bg); }
.bp-c-moments {
  display: flex;
  align-items: flex-start;
  padding: var(--pad-s) var(--pad-m);
}
.bp-c-moments.bp-c-empty {
  align-items: center;
  justify-content: center;
}
.moments-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  width: 100%;
}
.moment-item {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  column-gap: 0.55rem;
  font-size: 0.85rem;
  line-height: 1.4;
  color: var(--ink-soft);
  padding-left: 0.1rem;
}
.moment-item::before {
  /* Diamond shape -- rotated square, matches the row label's
     #moment-diamond glyph and the original journey-map vocabulary
     for moments-of-truth. Color flows through .is-* classes below. */
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  transform: rotate(45deg);
  background: var(--moment-color, color-mix(in srgb, var(--ink-muted) 50%, transparent));
  align-self: center;
  flex-shrink: 0;
}
.moment-item.is-neutral {
  --moment-color: color-mix(in srgb, var(--accent) 70%, transparent);
}
.moment-item.is-positive {
  /* Severity colors balanced against neutral so critical doesn't
     visually shout 5x louder than routine moments. Mid-saturation
     mix keeps the row's vocabulary coherent. */
  --moment-color: color-mix(in srgb, var(--hold-edge) 80%, transparent);
  color: var(--ink);
}
.moment-item.is-detractor {
  --moment-color: color-mix(in srgb, var(--flex-edge) 80%, transparent);
  color: var(--ink);
}
.moment-item.is-critical {
  --moment-color: color-mix(in srgb, var(--break-edge) 80%, transparent);
  color: var(--ink);
  font-weight: 500;
}


/* ============================================================
   PHASE BOUNDARIES (added in v0.5+ polish)

   Phase chunking propagates down the grid so the reader's eye
   keeps phase context while scanning vertically. JS marks every
   cell whose data-stage matches a phase end-stage (not the last
   phase) with data-phase-end="true". CSS draws a stronger right
   border on those cells -- 2px, ink-soft, instead of the row's
   1px hairline.

   Same rule applies to stage-heads (so the dividers persist in
   the sticky header clone too). The visual chain reads as: phase
   strip groups columns at the top -> sticky header preserves the
   grouping while scrolled -> grid itself enforces it through
   every row.
   ============================================================ */

/* Stronger divider at phase boundaries -- both live grid cells
   and the stage-head row (which carries through into the sticky
   clone). The hairline still renders by default; this rule
   overrides only at the boundary. */
/* Phase column dividers: when JS marks a cell with data-phase-end="true",
   draw a slightly stronger right border than the row's standard hairline
   to anchor "phase 2 ends here." The divider is structural, not content,
   so it stays quiet -- 1.5px at low opacity reads as "boundary marker"
   without competing with content rails (narrative, loop pills, etc.). */
.bp-row [data-phase-end="true"] {
  border-right: 1.5px solid color-mix(in srgb, var(--ink-muted) 35%, transparent);
}

/* Stages row gets a touch more contrast since it sits at the top of
   the grid and anchors the divider visually for the rest. */
.bp-row.bp-stages [data-phase-end="true"] {
  border-right-color: color-mix(in srgb, var(--ink-muted) 50%, transparent);
}

/* Drop the divider on rows that have their own structural rails:
   narrative cards have accent left/right rails per beat; loop cells
   already had a transparent border. Adding a phase divider on top
   of those creates redundant verticals that fight content. */
.bp-row.bp-layer-narrative [data-phase-end="true"] {
  border-right-color: transparent;
}

/* Dark theme: dividers need slightly more contrast since the
   bg-elev is closer to ink-muted. */
body[data-theme="dark"] .bp-row [data-phase-end="true"] {
  border-right-color: color-mix(in srgb, var(--ink-muted) 50%, transparent);
}
body[data-theme="dark"] .bp-row.bp-stages [data-phase-end="true"] {
  border-right-color: color-mix(in srgb, var(--ink-muted) 65%, transparent);
}
body[data-theme="dark"] .bp-row.bp-layer-narrative [data-phase-end="true"] {
  border-right-color: transparent;
}

/* Column-highlight on hover should still tint cleanly behind the
   stronger phase divider -- the divider sits on the right edge
   so column tint reads underneath without conflict. */


/* =========================================================================
   KIT RAIL -- side navigation for catalog-shape pages
   -------------------------------------------------------------------------
   Persistent left rail that carries page identity, section nav, and
   settings (theme toggle). On opted-in pages the desktop top bar is
   hidden -- the rail handles all three jobs from a single column.

   Active state inherited from the existing .top-nav .nav-link scroll-spy
   selector in app.js, so the rail "just works" without JS changes.

   Currently wired up on patterns.html only (gated by body.page-patterns).
   Other catalog-shape pages can opt in by:
     1. Adding class="page-patterns" to <body> (or a renamed equivalent)
     2. Including the <aside class="kit-rail"> + <div class="page-with-rail">
        wrapper in the markup
     3. Including the .kit-rail-mobile-bar header for the mobile fallback

   Mobile (<= 960px): rail collapses to a horizontal scroller pinned
   below a slim mobile-only top bar. The mobile bar is hidden on
   desktop; the desktop rail is replaced by the mobile scroller plus
   bar combo on narrow viewports.
   ========================================================================= */

/* --- Hide the desktop top bar on opted-in pages.
       The rail carries page identity. The mobile bar (.kit-rail-mobile-bar)
       takes over below 960px. --- */
body.page-patterns .top-bar.kit-rail-mobile-bar { display: none; }

/* --- Rail visible only on opted-in pages --- */
.kit-rail { display: none; }
body.page-patterns .kit-rail { display: flex; }

/* --- Page-with-rail container offsets section content past the rail --- */
.page-with-rail { padding-left: 0; }
body.page-patterns .page-with-rail {
  padding-left: 19rem; /* matches kit-rail width + breathing room */
}

/* --- Rail itself: fixed left column from viewport top.
       Three regions in a flex column: identity (top), nav (middle, scrolls
       if overflow), settings (bottom). Settings are pinned to the bottom
       via margin-top: auto on the settings region. --- */
body.page-patterns .kit-rail {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 18rem;
  flex-direction: column;
  padding: 1.5rem 1rem 1.25rem 1.25rem;
  border-right: 1px solid var(--rule);
  background: var(--bg-elev);
  overflow-y: auto;
  z-index: 5;
}

/* --- Identity block: page name + subtitle. Sits at the top.
       When wrapped in <a class="kit-rail-home"> it becomes a link
       back to the kit's start page, with a subtle home indicator
       on hover. --- */
.kit-rail-identity {
  display: block;
  margin: 0 0.3rem 1.5rem;
  padding-bottom: 1.1rem;
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
  color: inherit;
  position: relative;
}
a.kit-rail-identity { cursor: pointer; }
.kit-rail-title {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 0.2rem;
  letter-spacing: -0.005em;
  line-height: 1.25;
}
.kit-rail-subtitle {
  font-size: 0.74rem;
  color: var(--ink-muted);
  margin: 0;
  line-height: 1.4;
}

/* Home indicator: small house glyph + label. Hidden by default so
   the identity block reads as identity, not as a button. Reveals on
   hover/focus so the affordance is discoverable without competing
   for the eye at rest. */
.kit-rail-home-mark {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.55rem;
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
  opacity: 0;
  transform: translateY(-2px);
  transition: opacity 0.18s ease, transform 0.18s ease, color 0.18s ease;
}
.kit-rail-home:hover .kit-rail-home-mark,
.kit-rail-home:focus-visible .kit-rail-home-mark {
  opacity: 1;
  transform: translateY(0);
  color: var(--accent);
}
.kit-rail-home:hover .kit-rail-title,
.kit-rail-home:focus-visible .kit-rail-title {
  color: var(--accent);
}
.kit-rail-home:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 4px;
}
@media (prefers-reduced-motion: reduce) {
  .kit-rail-home-mark { transition: none; transform: none; }
}

.kit-rail-eyebrow {
  font-size: 0.7rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  margin: 0 0 0.85rem 0.5rem;
}

.kit-rail-nav {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

/* Override .top-nav row layout for the rail -- vertical stack, not horizontal */
body.page-patterns .top-nav.kit-rail-nav { flex-direction: column; gap: 0.15rem; }

/* --- Rail link cell: number + name + sub-line --- */
body.page-patterns .kit-rail-link {
  display: grid;
  grid-template-columns: 1.85rem 1fr;
  align-items: start;
  gap: 0.6rem;
  padding: 0.55rem 0.65rem;
  border-radius: 6px;
  font-size: 0.88rem;
  text-decoration: none;
  color: var(--ink-soft);
  border: 1px solid transparent;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
  /* Reset .nav-link's letter-spacing so the rail reads as
     editorial copy, not eyebrow vocabulary. */
  letter-spacing: 0;
}

.kit-rail-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 50%;
  /* No border at rest -- the number alone reads as a TOC marker,
     not a course-progress chip. The active-state circle below adds
     the chrome only when it's earning its place ("you are here"). */
  border: 1px solid transparent;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--ink-muted);
  background: transparent;
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
  margin-top: 0.05rem;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.kit-rail-num-glyph { padding: 0.25rem; }
.kit-rail-num-glyph svg { width: 0.95rem; height: 0.95rem; }

.kit-rail-text {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

.kit-rail-name {
  font-weight: 600;
  color: var(--ink);
  line-height: 1.25;
  letter-spacing: -0.005em;
}

.kit-rail-sub {
  font-size: 0.76rem;
  color: var(--ink-muted);
  line-height: 1.3;
}

/* --- Hover --- */
body.page-patterns .kit-rail-link:hover {
  background: color-mix(in srgb, var(--accent-soft) 50%, transparent);
  color: var(--ink);
  border-color: transparent;
}
body.page-patterns .kit-rail-link:hover .kit-rail-num {
  color: var(--ink);
  border-color: color-mix(in srgb, var(--ink-muted) 35%, transparent);
}

/* --- Focus --- */
body.page-patterns .kit-rail-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  background: color-mix(in srgb, var(--accent-soft) 50%, transparent);
}

/* --- Active state (driven by initScrollSpy via .nav-active) --- */
body.page-patterns .kit-rail-link.nav-active {
  background: var(--accent-soft);
  color: var(--ink);
  border-color: color-mix(in srgb, var(--accent) 18%, transparent);
}
body.page-patterns .kit-rail-link.nav-active .kit-rail-num {
  background: var(--accent);
  color: var(--bg-elev);
  border-color: var(--accent);
}
body.page-patterns .kit-rail-link.nav-active .kit-rail-name {
  color: var(--ink);
}

/* --- Divider between Part patterns and altitude-shifted Row patterns --- */
.kit-rail-divider {
  display: block;
  height: 1px;
  margin: 0.85rem 0.65rem;
  background: var(--rule);
}

/* --- Altitude-shifted item (Row patterns) reads as a different
       category, not part of the numbered Part sequence. The glyph
       in place of a number signals the altitude shift. --- */
body.page-patterns .kit-rail-link-altitude .kit-rail-name {
  font-size: 0.86rem;
}

/* --- Settings region: theme toggle pinned to bottom of rail.
       margin-top: auto pushes it to the bottom of the flex column.
       Border-top separates it from nav as a different region. --- */
.kit-rail-settings {
  margin: 1.5rem -0.25rem 0;
  padding: 0.85rem 0.4rem 0;
  border-top: 1px solid var(--rule);
  margin-top: auto; /* pin to bottom of rail flex column */
}

/* --- Theme toggle inside the rail: wide pill row instead of the
       compact circle used in the top bar. Three columns: icon
       (current state), label (current state), switch (affordance).
       The switch on the right makes the row scan as interactive --
       without it the row reads as a status label, not a control. --- */
body.page-patterns .kit-rail-theme-toggle {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  width: 100%;
  height: auto;
  padding: 0.55rem 0.7rem;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--ink-soft);
  cursor: pointer;
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 500;
  text-align: left;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
body.page-patterns .kit-rail-theme-toggle:hover {
  background: color-mix(in srgb, var(--accent-soft) 50%, transparent);
  color: var(--ink);
  border-color: transparent;
}
body.page-patterns .kit-rail-theme-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.kit-rail-theme-toggle-icon {
  display: inline-flex;
  width: 1.6rem;
  height: 1.6rem;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--rule);
  background: var(--bg);
  flex-shrink: 0;
  color: var(--ink-soft);
}
body.page-patterns .kit-rail-theme-toggle:hover .kit-rail-theme-toggle-icon {
  border-color: color-mix(in srgb, var(--accent) 25%, var(--rule));
  color: var(--ink);
}

/* Label fills the middle, pushing the switch to the right edge. */
.kit-rail-theme-toggle-label {
  flex: 1;
  min-width: 0;
}

/* Switch indicator: track + sliding thumb. Track tints accent when
   dark mode is active so the affordance also reads as a state. */
.kit-rail-theme-toggle-switch {
  position: relative;
  display: inline-block;
  width: 1.85rem;
  height: 1rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink-muted) 22%, transparent);
  flex-shrink: 0;
  transition: background 0.18s ease;
}
.kit-rail-theme-toggle-switch-thumb {
  position: absolute;
  top: 50%;
  left: 0.15rem;
  width: 0.7rem;
  height: 0.7rem;
  background: var(--bg-elev);
  border-radius: 50%;
  transform: translateY(-50%);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
  transition: left 0.18s ease, background 0.18s ease;
}
body[data-theme="dark"] .kit-rail-theme-toggle-switch {
  background: var(--accent);
}
body[data-theme="dark"] .kit-rail-theme-toggle-switch-thumb {
  left: calc(100% - 0.85rem);
}

@media (prefers-reduced-motion: reduce) {
  .kit-rail-theme-toggle-switch,
  .kit-rail-theme-toggle-switch-thumb { transition: none; }
}

/* Label swap: "Light theme" reads as the current state -- in light mode
   we say "Light theme" (the action would be "switch to dark", carried by
   the icon's moon glyph). In dark mode, "Dark theme". The icon already
   teaches direction; the label states the current state for clarity. */
.theme-toggle-text-light { display: inline; }
.theme-toggle-text-dark { display: none; }
body[data-theme="dark"] .theme-toggle-text-light { display: none; }
body[data-theme="dark"] .theme-toggle-text-dark { display: inline; }

/* --- Mobile: rail collapses to horizontal scroller below mobile-only top bar --- */
@media (max-width: 960px) {
  /* Mobile bar shows up on opted-in pages only at narrow widths */
  body.page-patterns .top-bar.kit-rail-mobile-bar { display: flex; }

  body.page-patterns .page-with-rail { padding-left: 0; }

  body.page-patterns .kit-rail {
    position: sticky;
    top: var(--top-bar-height);
    left: 0;
    right: 0;
    bottom: auto;
    width: 100%;
    height: auto;
    flex-direction: row;
    align-items: center;
    padding: 0.75rem var(--pad-m);
    border-right: 0;
    border-bottom: 1px solid var(--rule);
    overflow-x: auto;
    overflow-y: hidden;
    z-index: 9;
    background: color-mix(in srgb, var(--bg-elev) 88%, transparent);
    -webkit-backdrop-filter: saturate(160%) blur(12px);
    backdrop-filter: saturate(160%) blur(12px);
  }

  /* Mobile: identity + settings hide from the rail; the mobile bar
     hosts identity, and theme toggle migrates to the mobile bar. */
  .kit-rail-identity { display: none; }
  .kit-rail-settings { display: none; }
  .kit-rail-eyebrow { display: none; }

  body.page-patterns .top-nav.kit-rail-nav {
    flex-direction: row;
    gap: 0.4rem;
    flex-wrap: nowrap;
    width: max-content;
  }
  body.page-patterns .kit-rail-link {
    grid-template-columns: auto auto;
    padding: 0.4rem 0.7rem;
    white-space: nowrap;
    gap: 0.4rem;
  }
  .kit-rail-num {
    width: 1.35rem;
    height: 1.35rem;
    font-size: 0.72rem;
  }
  .kit-rail-sub { display: none; }
  .kit-rail-divider {
    width: 1px;
    height: 1.4rem;
    margin: 0 0.3rem;
    background: var(--rule);
  }
}

/* Reduced motion: drop transitions on rail interactions. */
@media (prefers-reduced-motion: reduce) {
  body.page-patterns .kit-rail-link,
  body.page-patterns .kit-rail-theme-toggle { transition: none; }
}


/* =========================================================================
   CATALOG: HOW TO READ EACH CARD
   -------------------------------------------------------------------------
   Reader-orientation block under the catalog hero. Three short labeled
   lines naming what each card carries. Replaces the buried-at-footer
   "How to read this catalog" details, which now serves a different job
   (sources + how-it-grows for builders extending the catalog).

   Editorial column with vertical rule, no card chrome. Reads as part
   of the hero's prose, not a separate component.
   ========================================================================= */

.catalog-howto {
  margin-top: var(--pad-l);
  padding-top: var(--pad-m);
  border-top: 1px solid var(--rule);
  max-width: 60ch;
}

.catalog-howto-eyebrow {
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  margin: 0 0 0.65rem;
}

.catalog-howto-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.catalog-howto-list li {
  display: grid;
  grid-template-columns: 9.5rem 1fr;
  gap: 0.85rem;
  align-items: baseline;
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--ink-soft);
}

.catalog-howto-key {
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.005em;
}

.catalog-howto-value {
  color: var(--ink-soft);
}

@media (max-width: 720px) {
  .catalog-howto-list li {
    grid-template-columns: 1fr;
    gap: 0.15rem;
  }
}


/* --- Anchor-jump offsets adapt to the rail page archetype.
       Desktop: no top bar, so anchors land at viewport top.
       Mobile: still a top bar + sticky rail, so the combined
       offset is the bar height plus a generous rail allowance. --- */
@media (min-width: 961px) {
  html:has(body.page-patterns) { scroll-padding-top: 1.5rem; }
  body.page-patterns .pattern-section { scroll-margin-top: 1.5rem; }
}
@media (max-width: 960px) {
  /* Top bar (4.5rem) + rail (~3.5rem) ~ 8rem combined sticky surface. */
  html:has(body.page-patterns) { scroll-padding-top: 8rem; }
  body.page-patterns .pattern-section { scroll-margin-top: 8rem; }
}


/* --- Back-to-top redundant on rail-led pages.
       The rail's section list is always visible and any section is
       one click from the current scroll position. The BTT button
       was solving the "where am I, how do I get back" problem that
       the rail solves by being persistent. --- */
body.page-patterns .btt { display: none; }


/* --- Mobile bar identity link: same home affordance, compressed
       to fit the bar. Title becomes accent on hover/focus. --- */
.kit-rail-home-mobile {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
  transition: color 0.15s ease;
}
.kit-rail-home-mobile:hover h1,
.kit-rail-home-mobile:focus-visible h1 {
  color: var(--accent);
}
.kit-rail-home-mobile:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}


/* =========================================================================
   KIT-PAGE CHROME
   -------------------------------------------------------------------------
   Affordances that appear ONLY on kit-source pages (template.html,
   example-wmp.html, patterns.html, start-here.html). Gated by
   body.kit-page so they disappear automatically when a builder
   copies a kit page into a project (project drops the class).

   Two pieces:
     - .kit-home-link (top-bar back-to-kit-start affordance, used on
       template.html and example-wmp.html only -- patterns.html has
       its own home link in the rail identity block)
     - .kit-byline (footer byline naming the maintainer + contact;
       lives in the sources sidecar footer of every kit page)
   ========================================================================= */

/* Hidden by default; shown only on kit-source pages.
   Hidden again on start-here itself -- you don't need a home link
   when you're already home. */
.kit-home-link { display: none; }
body.kit-page .kit-home-link {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.65rem 0.3rem 0.55rem;
  margin-right: 0.4rem;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--ink-soft);
  text-decoration: none;
  border-radius: 999px;
  border: 1px solid transparent;
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
body.kit-page.is-start-here .kit-home-link { display: none; }
body.kit-page .kit-home-link svg {
  flex-shrink: 0;
  color: var(--ink-muted);
  transition: transform 0.18s ease, color 0.15s ease;
}
body.kit-page .kit-home-link:hover {
  color: var(--ink);
  background: color-mix(in srgb, var(--accent-soft) 60%, transparent);
}
body.kit-page .kit-home-link:hover svg {
  color: var(--accent);
  transform: translateX(-2px);
}
body.kit-page .kit-home-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  color: var(--ink);
}
@media (max-width: 720px) {
  body.kit-page .kit-home-link span { display: none; }
  body.kit-page .kit-home-link { padding: 0.3rem 0.45rem; }
}
@media (prefers-reduced-motion: reduce) {
  body.kit-page .kit-home-link svg { transition: color 0.15s ease; }
  body.kit-page .kit-home-link:hover svg { transform: none; }
}


/* =========================================================================
   KIT BYLINE
   -------------------------------------------------------------------------
   Single-line maintainer + contact byline that appears in the footer of
   every kit-source page. Gated by body.kit-page so it disappears when
   pages are copied into a project (project credits its own author, not
   the kit's).

   Single source of truth for the maintainer's name + Slack handle.
   Update once here when ownership changes; every kit page picks it up.
   ========================================================================= */

.kit-byline { display: none; }
body.kit-page .kit-byline {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1rem;
  margin-top: var(--pad-m);
  padding-top: var(--pad-s);
  border-top: 1px dashed color-mix(in srgb, var(--rule) 70%, transparent);
  font-size: 0.78rem;
  color: var(--ink-muted);
  line-height: 1.55;
}
.kit-byline-text { color: var(--ink-soft); }
.kit-byline-contact {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  color: var(--ink-soft);
}
.kit-byline-contact svg {
  flex-shrink: 0;
  color: var(--ink-muted);
}
.kit-byline-handle {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.95em;
}
.kit-byline-divider {
  width: 0.25rem;
  height: 0.25rem;
  background: color-mix(in srgb, var(--ink-muted) 50%, transparent);
  border-radius: 50%;
  flex-shrink: 0;
  align-self: center;
}

/* =========================================================================
   PROJECT BYLINE
   -------------------------------------------------------------------------
   Shipped on builder-authored project artifacts. Same visual vocabulary
   as `.kit-byline` (font size, dashed top rule, dot dividers, monospace
   handle), but ALWAYS shown — not gated on body.kit-page. Used to credit
   the project's builder while still acknowledging the kit. Reuses the
   .kit-byline-text, .kit-byline-divider, .kit-byline-contact, and
   .kit-byline-handle helper classes inside the project-byline shell so
   the styling stays in one place.

   Convention encoded in `template.html`: when a builder copies the
   template into their project, they replace the project-byline's name
   and handle with their own (kit attribution survives, builder identity
   gets named). The kit-byline sibling block above gets removed during
   the same step, since project artifacts shouldn't carry the kit's
   maintainer credit.
   ========================================================================= */
.project-byline {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1rem;
  margin-top: var(--pad-m);
  padding-top: var(--pad-s);
  border-top: 1px dashed color-mix(in srgb, var(--rule) 70%, transparent);
  font-size: 0.78rem;
  color: var(--ink-muted);
  line-height: 1.55;
}
/* Children of .project-byline should match the .kit-byline visual
   register exactly. Reusing .kit-byline-text, .kit-byline-divider,
   .kit-byline-contact, .kit-byline-handle inside the project-byline
   shell works for the children, but the parent paragraph's font-size
   must cascade into the strong tags too. Force them to inherit so
   the bolded "CS UX Service Blueprint Kit" stays at the byline's
   small size, not at body register. */
.project-byline strong { font-size: inherit; font-weight: 700; }


/* =========================================================================
   KIT TYPOGRAPHY: eyebrow + thesis
   -------------------------------------------------------------------------
   Promoted from start-here's inline styles. Two-tier title vocabulary
   used across kit-source pages: a small uppercase eyebrow above a
   display-font thesis, with an accent phrase carrying the second beat.

   Family resemblance pattern: every kit-source page that has a hero
   uses these classes. Pages can extend them with their own page-scoped
   modifier (.catalog-thesis, .sh-thesis, etc.) to vary size or treatment
   while keeping the family.

   start-here.html still defines local copies of these classes inline
   to keep its hero self-contained; the inline definitions match this
   block. If they ever drift, the inline copy wins for start-here only.
   ========================================================================= */

.sh-eyebrow {
  display: block;
  font-size: 0.7rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  margin: 0 0 var(--pad-s);
}

.sh-thesis {
  font-family: var(--font-display);
  font-size: clamp(1.8rem, 1.3rem + 1.5vw, 2.6rem);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.015em;
  color: var(--ink);
  margin: 0 0 var(--pad-m);
  /* Hero copy uses two .sh-thesis-line spans, each set to
     display: block. This gives us deterministic two-line layout
     without relying on text-wrap: balance (which treated each
     <br>-separated chunk as something to balance into multiple
     lines, not as a single line to fit). max-width is set on
     each line individually below so the LONGER line drives the
     constraint and the shorter one fits naturally inside it. */
}
.sh-thesis-line {
  display: block;
  /* Each line stays on a single visual line at desktop sizes by
     not wrapping. On narrow viewports it can wrap (max-width
     can't fit the line), and we accept that -- the explicit
     two-line structure still preserves the phrase split. */
  max-width: 100%;
}

.sh-thesis-accent { color: var(--accent); }


/* =========================================================================
   STORYBOARD ROW (placeholder, doctrine-first ship)
   -------------------------------------------------------------------------
   Comic-style panels per stage. Visual carries the felt experience;
   caption underneath is a beat label. SVG placeholder symbols ship in
   defs (#story-1..#story-9); builders replace with <img> when shipping
   real artwork. CSS treats <svg> and <img> equivalently.

   Sits above the narrative row so the panel reads as the moment's
   image, narrative reads as caption. In Stage Slide mode the panel
   becomes the slide's visual hero (see 03-component-library.md
   "Held: candidate new modes -> Stage Slide mode").
   ========================================================================= */

.bp-c-story {
  padding: 0.55rem 0.5rem;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.35rem;
}

.story-panel {
  display: block;
  width: 100%;
  aspect-ratio: 100 / 70;
  background: color-mix(in srgb, var(--ink-muted) 4%, var(--bg-elev));
  border-radius: 4px;
  overflow: hidden;
  color: var(--ink-soft);
}
.story-panel svg,
.story-panel img {
  width: 100%;
  height: 100%;
  display: block;
}
.story-panel img {
  object-fit: cover;
  object-position: center;
}
body[data-theme="dark"] .story-panel {
  background: color-mix(in srgb, var(--ink-muted) 8%, var(--bg-elev));
}

.story-caption {
  font-size: 0.7rem;
  line-height: 1.35;
  color: var(--ink-muted);
  text-align: center;
  padding: 0 0.15rem;
}




/* =========================================================================
   STORYBOARD BUTTON (lightbox trigger)
   -------------------------------------------------------------------------
   Same affordance shape as .ev-thumb-btn (screenshots row): button wraps
   the visual, expand glyph appears on hover, focus ring on keyboard.
   Storyboard panels open in the existing lightbox -- one row, nine
   panels, arrow keys navigate the row.
   ========================================================================= */

.story-btn {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  background: transparent;
  border: 0;
  cursor: zoom-in;
  border-radius: 4px;
  overflow: hidden;
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.story-btn:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.18);
}
.story-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.story-btn .story-panel { margin: 0; }

.story-expand {
  position: absolute;
  top: 0.35rem;
  right: 0.35rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.4rem;
  height: 1.4rem;
  background: color-mix(in srgb, var(--bg-elev) 88%, transparent);
  border-radius: 50%;
  color: var(--ink-soft);
  opacity: 0;
  transition: opacity 0.15s ease;
}
.story-expand svg { width: 0.7rem; height: 0.7rem; }
.story-btn:hover .story-expand,
.story-btn:focus-visible .story-expand { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .story-btn { transition: none; }
  .story-btn:hover { transform: none; }
}


/* =========================================================================
   STAGE SLIDE MODE (rebuilt as fullscreen overlay)
   -------------------------------------------------------------------------
   Lightbox-shaped architecture. Slide mode no longer reflows the grid
   markup; it renders into a dedicated #slide-stage container that lives
   above the normal page (z-index above lightbox would conflict; below
   is fine since they're mutually exclusive). The grid stays exactly as
   authored; the overlay reads from it via JS.

   STATE:
     body[data-view="slide"]       -- mode active, hides page chrome
     #slide-stage [data-active]    -- overlay visible
     #slide-stage [data-stage]     -- which stage's content is shown

   Doctrine: 03-component-library.md "Held: candidate new modes ->
   Stage Slide mode". This implementation honors the doctrine's six
   altitudes (hero / visual / supporting / quote / body / closer)
   but composes them into a clean overlay layout instead of reflowing
   the grid.
   ========================================================================= */

/* Hide page when slide overlay is up */
body[data-view="slide"] {
  overflow: hidden;
}
body[data-view="slide"] .top-bar,
body[data-view="slide"] .skip-link,
body[data-view="slide"] .btt {
  visibility: hidden;
}
/* The blueprint's sticky-header clone is position: fixed and would
   otherwise float above the slide overlay (same z-index range as
   other fixed-position page chrome). Force-hide while slide mode
   is active. */
body[data-view="slide"] .bp-sticky-header {
  display: none !important;
}

/* The overlay container itself */
.slide-stage {
  /* The kit teaches one color language: time flows left-to-right and
     color heats from cool (accent) to warm (flex-edge) to hot
     (break-edge) along the timeline. The active stage's gradient
     position is written to --col-glow on the overlay and inherited
     by the title eyebrow, verb glyph, active rail segment, and
     active phase label. Map mode and slide mode read in the same
     visual language.

     In-content state signals (pain block, breach metrics, critical
     badges) use a separate semantic palette (--state-break for
     breakdown, --flex-edge for friction). Two systems, two jobs:
       --col-glow  -> chrome (timeline position)
       state color -> content (this moment's emotional weight) */
}
.slide-stage {
  position: fixed;
  inset: 0;
  /* Above .bp-sticky-header (z-index: 90) and the top bar (z-index: 100).
     Lightbox lives at 99999 so it can still open over the slide if a
     reader enlarges a screenshot during a presentation. */
  z-index: 200;
  background: var(--bg);
  display: none;
  flex-direction: column;
  overflow: hidden;
}
.slide-stage[data-active="true"] {
  display: flex;
  animation: slide-overlay-in 0.32s ease both;
}
@keyframes slide-overlay-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Top toolbar: 3-column grid (left | center | right). Each zone is
   independently anchored, so right-side controls never shift when
   left-zone or center-zone content changes width.
   Left  : artifact identity + counter (where you are)
   Center: transport (rail relocates here at init)
   Right : settings + exit (theme, deck mode, exit)
*/
.slide-toolbar {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, auto) minmax(0, 1fr);
  align-items: center;
  gap: 1.25rem;
  padding: 0.85rem 1.25rem;
  border-bottom: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  flex-shrink: 0;
}
.slide-toolbar-zone {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  min-width: 0;
}
.slide-toolbar-zone.is-left  { justify-content: flex-start; }
.slide-toolbar-zone.is-center { justify-content: center; }
.slide-toolbar-zone.is-right { justify-content: flex-end; gap: 0.5rem; }
.slide-toolbar-artifact {
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.005em;
  margin-right: 0.5rem;
  padding-right: 0.85rem;
  border-right: 1px solid var(--rule);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 22ch;
}
.slide-toolbar-artifact:empty {
  display: none;
}
.slide-toolbar-counter {
  font-size: 0.74rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
}
.slide-toolbar-counter strong {
  color: var(--ink);
  font-weight: 700;
}
.slide-counter-context {
  /* Quiet context detail: "Slide 4 of 14 · Stage 3" or "Slide 1 of
     14 · Problem". Reader sees position AND role of current slide. */
  font-size: 0.7rem;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--ink-muted);
  font-style: italic;
  font-weight: 400;
}
.slide-toolbar-phase {
  font-size: 0.78rem;
  color: var(--ink-soft);
  letter-spacing: 0.05em;
}
.slide-toolbar-phase::before {
  content: "Phase \00b7 ";
  color: var(--ink-muted);
}
.slide-toolbar-spacer { display: none; } /* retired in 3-column grid */
.slide-toolbar-phase  { display: none; } /* retired -- slide's own phase progress is canonical */
/* Generic icon-style toolbar button -- used for theme toggle and
   exit. Quiet at rest (transparent), warm on hover/focus. Same
   chrome principle as patterns.html's rail toggle: interactive
   earns affordance, doesn't shout. */
.slide-toolbar-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 50%;
  color: var(--ink-soft);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.slide-toolbar-icon-btn:hover {
  background: color-mix(in srgb, var(--ink-muted) 10%, transparent);
  color: var(--ink);
  border-color: color-mix(in srgb, var(--rule) 60%, transparent);
}
.slide-toolbar-icon-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Theme toggle in toolbar: inherit icon-btn chrome; the existing
   .theme-toggle rules show/hide sun/moon glyphs based on body
   data-theme so no new wiring needed. The button binds to
   initTheme() automatically via the .theme-toggle class. */
.slide-toolbar-icon-btn.theme-toggle .theme-toggle-icon {
  width: 16px;
  height: 16px;
}
/* Exit gets a subtle red wash on hover to read as the "leave this
   surface" affordance, distinct from theme toggle which switches
   state. */
.slide-toolbar-icon-btn.slide-toolbar-exit:hover {
  background: color-mix(in srgb, var(--state-break, #c75a5a) 12%, transparent);
  color: color-mix(in srgb, var(--state-break, #c75a5a) 80%, var(--ink));
  border-color: color-mix(in srgb, var(--state-break, #c75a5a) 30%, transparent);
}

/* Slide canvas: the actual content area. Sizes to viewport between
   toolbar and rail so the slide fits without scroll. Internal scroll
   only kicks in when content genuinely exceeds the canvas. */
.slide-canvas {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: stretch;
  justify-content: center;
  padding: 1.5rem 2rem 2rem;
  /* Default: no scroll. Slides should fit. is-overflowing class
     (set by JS when content exceeds canvas) flips to scroll. */
  overflow: hidden;
  /* Restrained scrollbar chrome for the overflow case so the slide
     surface doesn't get a heavy scrollbar competing with the slide
     itself. */
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--ink-muted) 30%, transparent) transparent;
}
.slide-canvas::-webkit-scrollbar { width: 8px; }
.slide-canvas::-webkit-scrollbar-track { background: transparent; }
.slide-canvas::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--ink-muted) 30%, transparent);
  border-radius: 999px;
}
/* Part bookend slides (Problem / Scale / Evidence / Takeaway) carry
   variable content density. Some are short (single recommendation
   aside, a hero stat). Others are dense (Takeaway with multiple
   structural variants: recommendation + intro + resolution pairs +
   scope boundaries + closer). The reliable default is top-aligned
   with vertical scroll: short slides sit at the top with empty
   space below (no scrollbar appears when content fits); long
   slides scroll naturally without clipping the top or bottom.

   The earlier default centered short part-slides vertically and
   relied on a JS-driven `.is-overflowing` class to flip to scroll
   for long ones. That detection had timing fragility (web font
   load, layout settle) and silently clipped dense slides when
   the rAF measurement read a stale layout. Always-scroll is the
   robust default; the `.is-overflowing` class still flags when
   the visual scroll affordance is in play. */
.slide-canvas:has(.slide-content.is-part-slide) {
  align-items: flex-start;
  overflow-y: auto;
}
/* When the canvas is overflowing (per JS detection) the overlay
   surface adds extra bottom padding so the last slide content has
   breathing room below the closer. The detection class still
   applies; CSS just no longer depends on it for basic scroll. */
.slide-canvas.is-overflowing,
.slide-canvas:has(.slide-content.is-part-slide.is-overflowing) {
  padding-bottom: 3rem;
}

/* SLIDE CONTENT -- ANCHORED GRID
   -----------------------------------------------------------
   Every slot has a fixed position. Empty slots dim, they don't
   collapse. Reader scanning between stages knows where every
   piece of content lives -- the layout never shifts. The slot
   inventory is built once at slide-mode init from the artifact's
   row vocabulary; only those slots render. Within an artifact
   the layout is stable across all stages.

   GRID:
     header          (top, full-width)            -- stage title + sub
     visual          (left, tall)                 -- storyboard / screenshot
     quote           (right, top)                 -- customer thoughts
     metrics         (right, mid)                 -- inline metric strip
     body            (full-width below visual)    -- frontstage / backstage / pain
     closer          (right, bottom)              -- insight + recommendation

   The right column scrolls internally when dense (rare); the
   visual + body stay anchored on the left.
*/
/* New shape (text-left / image-right):

     header          (full-width)               -- stage label + title
     metrics         (full-width strip)         -- vital-signs band, anchored top
     left            (editorial column)         -- quote -> body -> closer
     right           (visual evidence column)   -- locked-size visual + optional strip

   The visual is locked to a canonical size per kind regardless of
   whether a screens strip is present. The strip, when present, sits
   below the visual at a fixed height. The visual + strip block
   centers vertically inside the right column so a sparse left
   column doesn't leave the right column floating in dead space
   and a dense left column doesn't push the visual out of view.

   When the artifact has no visual row at all, the right column
   collapses and the left column widens with a max-width cap so
   prose stays readable instead of stretching to full canvas.
*/
.slide-content {
  width: 100%;
  max-width: 1280px;
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "header   header"
    "left     right";
  column-gap: 2.4rem;
  row-gap: 1.1rem;
  animation: slide-content-in 0.28s ease both;
}

.slide-header        { grid-area: header; }
/* Editorial column. Caps line length so dense stages stay readable;
   right column takes whatever space is left. Independently scrollable
   when its content exceeds available height -- the slide header
   (full width, above both columns) and the visual column stay
   visible while the reader scrolls editorial content. The header
   acts as natural sticky context because it lives outside this
   scrollable container. Reader scrolling pain points or insights
   never loses the stage's identity (eyebrow + JOB + title + sub)
   or the visual evidence. */
.slide-left-zone     {
  grid-area: left;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 60ch;
  /* Scroll behavior: when editorial content exceeds the column's
     allocated height, the column scrolls. Padding-right reserves
     room for the scrollbar so it doesn't crowd the prose. */
  overflow-y: auto;
  overflow-x: hidden;
  padding-right: 0.4rem;
  /* Subtle scroll affordance: thin track, accent thumb on hover. */
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--ink-muted) 28%, transparent) transparent;
}
.slide-left-zone::-webkit-scrollbar {
  width: 6px;
}
.slide-left-zone::-webkit-scrollbar-track {
  background: transparent;
}
.slide-left-zone::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--ink-muted) 28%, transparent);
  border-radius: 999px;
}
.slide-left-zone::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--accent) 40%, var(--ink-muted));
}
/* Mask hint at the bottom edge: subtle fade signals "more content
   below" when the column is scrollable. Pure visual; no JS. */
.slide-left-zone {
  mask-image: linear-gradient(
    to bottom,
    black calc(100% - 1.5rem),
    transparent 100%
  );
  -webkit-mask-image: linear-gradient(
    to bottom,
    black calc(100% - 1.5rem),
    transparent 100%
  );
}
/* Reset the mask when scroll position is at the bottom -- no need
   to fade if there's nothing more below. Detected via scrollbar
   end states isn't possible in pure CSS; this is a static visual
   that's correct most of the time. Acceptable tradeoff vs. JS. */
.slide-right-zone {
  grid-area: right;
  min-height: 0;
  /* Flex column instead of grid: phone, optional caption, and strip
     stack naturally without grid-row sizing producing overlap when
     the canvas is shorter than the phone's max height + strip's
     intrinsic height. With grid + auto rows, a 620px phone in a
     700px column left only 80px for the strip's row; the strip's
     content (~120px) overflowed back into the phone above. Flex
     gives us natural stacking with explicit gap between siblings,
     and the phone's height clamps below honor the column's bounds. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 1.4rem;
}
.slide-right-zone > .slide-visual {
  /* Phone (or storyboard) fits the remaining column space when the
     column is shorter than the phone's natural height. The phone's
     own height-clamp (clamp(440px, 70vh, 620px) on the inner frame)
     handles the upper bound; min(0) here lets it shrink if needed. */
  flex-shrink: 1;
  min-height: 0;
}
.slide-right-zone > .slide-visual-caption {
  flex-shrink: 0;
}
.slide-right-zone > .slide-screens-strip {
  /* Strip is small chrome -- never shrink. Phone yields if anything
     does. The flex container's gap (1.4rem above) gives clear
     separation between phone and strip; no margin-top needed. */
  flex-shrink: 0;
}

/* Variant: no visual row in artifact -- right column collapses,
   left column widens to single editorial column with readable cap.
   Stage slides without screenshots use this layout: single column,
   centered in the canvas, comfortable max-width so the editorial
   prose reads as composed rather than left-anchored with empty
   space to the right. When a project later adds screenshots /
   storyboard rows, slide-content drops the `.no-visual` class and
   reverts to the two-column layout.

   Critical: the WHOLE slide-content (header + left-zone) caps at
   the same width and centers as one block, so the title's left
   edge aligns with the body content's left edge. Header alone
   capping at 1fr while left-zone caps at 80ch produces visible
   left-edge mismatch (title at column edge, body indented). */
.slide-content.no-visual {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "header"
    "left";
  max-width: 80ch;
  margin-inline: auto;
}
.slide-content.no-visual .slide-left-zone {
  max-width: none;
  margin: 0;
  align-self: stretch;
}
.slide-content.no-visual .slide-right-zone { display: none; }

@keyframes slide-content-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .slide-content { animation: none; }
  .slide-stage[data-active="true"] { animation: none; }
}

/* Narrow viewports: stack vertically. Reading rhythm: header ->
   metrics -> visual -> left-zone (quote / body / closer). Visual
   moves up under metrics so it stays in the "context band" before
   the editorial prose begins. */
@media (max-width: 900px) {
  .slide-canvas { padding: 1.25rem 1rem 1.5rem; overflow-y: auto; }
  .slide-content {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto;
    grid-template-areas:
      "header"
      "right"
      "left";
    column-gap: 0;
    row-gap: 1.1rem;
  }
  .slide-content.no-visual { grid-template-areas: "header" "left"; }
  .slide-left-zone  { max-width: none; }
  .slide-right-zone {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    gap: 0.85rem;
  }
  .slide-right-zone > .slide-visual,
  .slide-right-zone > .slide-visual-caption,
  .slide-right-zone > .slide-screens-strip {
    grid-row: auto;
    align-self: center;
  }
}

/* HERO (header) */
/* HERO (header) -- the sub line is always rendered (even as a
   non-breaking space placeholder) so children stay anchored stage
   to stage. No min-height needed; the rendered structure does the
   work. */
.slide-header {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}
.slide-hero-eyebrow {
  font-size: 0.66rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--col-glow, var(--accent));
  font-weight: 700;
  margin: 0 0 0.4rem;
  /* Eyebrow color samples the timeline gradient at the active stage
     -- mirrors map mode's left-to-right cool-to-hot color language.
     A late-journey stage's eyebrow reads warmer; an early-journey
     stage's reads cooler. Reader's mental model is consistent across
     map and slide views. */
}
.slide-hero-title {
  font-family: var(--font-display);
  font-size: clamp(2.15rem, 1.4rem + 1.85vw, 3.1rem);
  line-height: 1.04;
  letter-spacing: -0.018em;
  font-weight: 600;
  color: var(--ink);
  margin: 0;
  text-wrap: balance;
  max-width: 32ch;
  display: flex;
  align-items: baseline;
  gap: 0.65rem;
}
.slide-hero-title-text {
  text-wrap: balance;
}
/* Verb glyph next to the slide title -- categorical signal carried
   from map mode (look / tap / scroll / click / speak). Sized to
   align with the title's cap-height so it reads as a sibling, not
   a disconnected icon. */
.slide-hero-verb {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 1.6rem;
  height: 1.6rem;
  color: var(--col-glow, var(--accent));
  /* Lift to align baseline with title cap-height */
  transform: translateY(-0.15em);
}
.slide-hero-verb svg {
  width: 100%;
  height: 100%;
}
.slide-hero-sub {
  font-size: 1.05rem;
  color: var(--ink-soft);
  font-style: italic;
  margin: 0.4rem 0 0;
  line-height: 1.45;
  max-width: 56ch;
  /* Reserve full line height even when sub is absent so the phase
     progress + metrics + editorial column anchor in the same place
     stage to stage. is-placeholder modifier hides the &nbsp; visually
     while preserving its layout footprint. */
  min-height: 1.5em;
}
.slide-hero-sub.is-placeholder {
  visibility: hidden;
}
/* Part slides always have a section-job sub line authored, so the
   placeholder reservation isn't needed -- drop the min-height to
   tighten spacing between the title and sub. */
.slide-content.is-part-slide .slide-hero-sub {
  min-height: 0;
}

/* PHASE ROW (integrated into the rail).
   The rail becomes a two-layer component:
     top: phase eyebrow labels, each spanning the stages it covers
     bottom: existing segment strip (gradient + bookend dots)
   Both rows share the .slide-rail-track parent so phase labels
   align column-perfectly with their stage segments. Bookend Part
   dots get matching gap placeholders in the phases row so the
   columns line up. */
.slide-rail-track {
  display: flex;
  flex-direction: column;
  gap: 0.32rem;
  flex: 1;
  min-width: 0;
}
.slide-rail-phases {
  display: flex;
  align-items: end;
  gap: 0.3rem;
  width: 100%;
  height: 0.85rem;
}
/* Bookend gap: matches the dot's footprint so phase labels start
   where the journey stages start. */
.slide-rail-phase-gap {
  width: 6px;
  flex: 0 0 auto;
}
.slide-rail-phase-spacer {
  display: block;
}
.slide-rail-phase {
  display: flex;
  align-items: end;
  justify-content: center;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  min-width: 0;
  text-align: center;
  /* Quiet at rest -- ink-muted eyebrow. Active phase: full ink + bold,
     rail gradient below tells the journey's emotional shape. */
  transition: opacity 0.18s ease;
}
.slide-rail-phase-label {
  font-size: 0.58rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
  transition: color 0.18s ease, font-weight 0.18s ease;
  line-height: 1;
}
.slide-rail-phase:hover .slide-rail-phase-label {
  color: color-mix(in srgb, var(--col-glow, var(--accent)) 80%, var(--ink));
}
.slide-rail-phase.is-active .slide-rail-phase-label {
  /* Active phase label picks up its own gradient color (sampled at
     the phase's center stage) -- same color logic as map mode's
     phase labels. Bold weight + colored ink reads as "you are in
     this phase" without competing with the rail segments below. */
  color: var(--col-glow, var(--accent));
  font-weight: 700;
}
.slide-rail-phase:focus-visible {
  outline: 2px solid var(--slide-state-color);
  outline-offset: 2px;
  border-radius: 2px;
}
/* When on a Part bookend slide (no phase applies), dim all phase
   labels so the row reads as "phase context not applicable here." */
.slide-rail.is-phase-inactive .slide-rail-phases {
  opacity: 0.4;
}
@media (prefers-reduced-motion: reduce) {
  .slide-rail-phase,
  .slide-rail-phase-label,
  .slide-rail-phases { transition: none; }
}

/* Canvas: the actual content area. No mood-band on the canvas top
   edge -- the rail's per-segment gradient already shows the journey's
   emotional arc statically, and the active stage carries state via
   title eyebrow + pain block (in-content). Chrome would only stack
   redundant lines. */
.slide-canvas {
  position: relative;
}

/* VISUAL -- canonical sizes locked per kind, independent of strip
   presence. Reader sees a phone of consistent size whether the stage
   has a single screen, multi-screen strip, or storyboard panel.

   Two canonical kinds:
     storyboard panel     -> 16/9 landscape, max-width capped
     phone screenshot     -> 9/19.5 portrait, max-height capped

   The .is-portrait modifier swaps the chrome wrapper out so the
   phone-shaped frame can be sized directly. The strip, when present,
   sits beneath the visual at fixed height -- never compresses the
   visual to make room. The right-zone column centers the visual+strip
   block as a unit, so a sparse stage doesn't leave the visual
   floating in dead space.
*/
.slide-visual {
  display: block;
  position: relative;
  /* Storyboard panels (default): 10/7 matches the kit's 100x70
     panel symbols, so they fill the frame with no gray bands.
     Capped at 540px so the panel doesn't dominate the right column
     on dense template stages. The panel is clickable in slide mode
     -- click opens the lightbox via the same wiring map mode uses. */
  width: 100%;
  max-width: min(44vw, 540px);
  aspect-ratio: 10 / 7;
  background: color-mix(in srgb, var(--ink-muted) 4%, var(--bg-elev));
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  box-shadow: 0 8px 32px -10px rgba(0, 0, 0, 0.18);
  flex-shrink: 0;
  cursor: zoom-in;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.slide-visual:not(.is-portrait):not(.is-empty):hover {
  border-color: color-mix(in srgb, var(--accent) 40%, var(--rule));
  box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.22);
}
/* Empty visual placeholder is not zoomable */
.slide-visual.is-empty {
  cursor: default;
}
/* Portrait visual (phone screenshots): the .slide-visual is now a
   transparent positioning container; the actual phone frame inside
   carries the canonical phone size. Height-capped so dense stages
   and sparse stages render at the same phone size. */
.slide-visual.is-portrait {
  background: transparent;
  border: 0;
  box-shadow: none;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: auto;
  width: auto;
  max-width: none;
  /* The portrait wrapper itself is not the click target -- the
     inner phone-shaped frame is. Reset cursor so the surrounding
     transparent area doesn't show zoom-in. */
  cursor: default;
}
.slide-visual.is-portrait > .slide-visual-frame {
  /* Canonical phone frame: 4/5 aspect matches the kit's 80x100 mock
     symbols, so SVG mocks fill the frame edge-to-edge with no gray
     bars top/bottom. Real screenshots (when adopters use real images)
     will object-fit: cover into the same frame.

     Sizing rules (artifact-wide consistency):
     - Aspect-ratio drives proportions (4/5 portrait).
     - Phone height is FIXED per artifact, not per stage. The slide
       overlay carries data-has-strip="true|false" set once at slide-
       mode entry by scanning all stages for multi-screen content.
     - When NO stage has a strip (data-has-strip="false"), all phones
       use the bigger no-strip canonical: clamp(440px, 70vh, 620px).
     - When ANY stage has a strip (data-has-strip="true"), all phones
       use the with-strip canonical: clamp(380px, 60vh, 520px) -- a
       smaller value that leaves vertical room for the strip below
       on the same canvas height.
     - Both sizes apply uniformly across the deck for that artifact.
       A stage with one screen and a stage with three screens render
       at the same phone size; the deck reads as visually consistent.

     Why this beats per-stage shrinking: a deck that switches phone
     size between adjacent slides reads as broken visual rhythm. The
     kit's doctrine is "same phone size across the deck"; honoring
     that means the height is a per-artifact decision, not a per-
     stage decision.

     The min-height floor is removed because both clamps already
     have generous floors (440px / 380px). */
  height: clamp(440px, 70vh, 620px);
  aspect-ratio: 4 / 5;
  background: color-mix(in srgb, var(--ink-muted) 4%, var(--bg-elev));
  border-radius: 22px;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  box-shadow: 0 8px 32px -10px rgba(0, 0, 0, 0.18);
  cursor: zoom-in;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
/* Artifact has at least one stage with a multi-screen strip --
   smaller canonical phone height for the entire deck so the strip
   fits below without overflow on the same canvas height. */
.slide-stage[data-has-strip="true"] .slide-visual.is-portrait > .slide-visual-frame {
  height: clamp(380px, 60vh, 520px);
}
.slide-visual.is-portrait > .slide-visual-frame:hover {
  border-color: color-mix(in srgb, var(--accent) 40%, var(--rule));
  box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.22);
}
.slide-visual.is-portrait > .slide-visual-frame svg,
.slide-visual.is-portrait > .slide-visual-frame img {
  width: 100%;
  height: 100%;
  display: block;
}
.slide-visual svg,
.slide-visual img {
  width: 100%;
  height: 100%;
  display: block;
}
.slide-visual img { object-fit: cover; }
.slide-visual-caption {
  display: block;
  margin: 0.55rem 0 0;
  font-size: 0.85rem;
  color: var(--ink-muted);
  font-style: italic;
  text-align: center;
  flex-shrink: 0;
  max-width: 36ch;
}
/* When a screens strip follows the caption, give the caption a
   little extra room below so it groups with the storyboard above
   instead of bleeding into the strip's eyebrow. */
/* Caption-then-strip case: when a storyboard panel has a caption,
   the caption sits in the row between the visual and the strip.
   The strip then needs less margin-top of its own (the caption row
   handles separation), but still needs a bit so it groups with the
   storyboard above instead of bleeding into the strip's eyebrow.
   1rem gives clear caption-to-strip separation without the full
   2rem the phone-no-caption case needs (no shadow projecting from
   the storyboard). */
.slide-visual-caption + .slide-screens-strip {
  margin-top: 1rem;
}

/* "Screens" thumbnail strip: shown below the visual when this stage
   has multi-screen content. The strip's height is fixed; the visual
   above it is locked to its canonical size and never compresses to
   make room. Reader sees consistent visual across stages with or
   without strip; strip just adds context underneath when present.

   Sits inside the right-zone column, centered horizontally to align
   with the visual above it. Empty padding-top hosts the lifted thumb
   on hover without clipping. */
.slide-screens-strip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
  width: 100%;
  max-width: 320px;
  /* Top padding so the eyebrow has air between it and the visual
     above; bottom padding so the strip's lifted thumb doesn't kiss
     the canvas bottom. */
  padding: 0.4rem 0 0.2rem;
}
.slide-screens-eyebrow {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 0.6rem;
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  margin: 0;
  /* Reserve full line-height so descenders/ascenders don't get
     clipped when the strip sits near a column edge. */
  line-height: 1.4;
  padding: 0.1rem 0;
}
.slide-screens-count {
  font-size: 0.68rem;
  letter-spacing: 0.04em;
  text-transform: none;
  font-style: italic;
  font-weight: 400;
  color: var(--ink-muted);
}
.slide-screens-list {
  list-style: none;
  margin: 0;
  /* Vertical padding reserves room for the thumb's :hover translateY
     so the lifted thumb doesn't get clipped at top or bottom. */
  padding: 0.45rem 0.4rem 0.55rem;
  display: flex;
  justify-content: center;
  /* Gap bumped from 0.6rem to 0.85rem so the .is-current accent
     border + box-shadow doesn't crowd adjacent thumbs. The visual
     breathing room teaches "these are siblings, not stacked." */
  gap: 0.85rem;
  overflow-x: auto;
  overflow-y: visible;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--ink-muted) 30%, transparent) transparent;
  width: 100%;
}
.slide-screens-list::-webkit-scrollbar { height: 6px; }
.slide-screens-list::-webkit-scrollbar-track { background: transparent; }
.slide-screens-list::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--ink-muted) 30%, transparent);
  border-radius: 999px;
}

.slide-screens-thumb {
  position: relative;
  display: block;
  padding: 0;
  background: transparent;
  border: 0;
  cursor: zoom-in;
  flex-shrink: 0;
  transition: transform 0.18s ease;
}
.slide-screens-thumb-frame {
  display: block;
  width: 4.4rem;
  aspect-ratio: 80 / 100;
  background: color-mix(in srgb, var(--ink-muted) 4%, var(--bg-elev));
  border: 1px solid color-mix(in srgb, var(--rule) 80%, transparent);
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 2px 6px -2px rgba(0, 0, 0, 0.12);
  transition: border-color 0.15s ease, box-shadow 0.18s ease;
}
.slide-screens-thumb-frame svg,
.slide-screens-thumb-frame img {
  width: 100%;
  height: 100%;
  display: block;
}
.slide-screens-thumb-frame img { object-fit: cover; object-position: top center; }
.slide-screens-thumb-num {
  /* Number lives INSIDE the thumb's bottom-right corner instead of
     hanging outside the frame. The previous outside-top-right
     position let badges from adjacent thumbs visually collide
     (the right-edge badge of thumb 1 sat at the left edge of thumb 2
     once hover lifts were applied). Inside-bottom-right keeps each
     thumb's chrome contained within its own frame. */
  position: absolute;
  bottom: 0.2rem;
  right: 0.2rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.05rem;
  height: 1.05rem;
  background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
  border: 1px solid color-mix(in srgb, var(--rule) 70%, transparent);
  color: var(--ink-soft);
  border-radius: 999px;
  font-size: 0.6rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  /* Backdrop blur in case the thumb shows screenshot content under
     the badge -- keeps the number legible without a hard background. */
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
.slide-screens-thumb:hover { transform: translateY(-2px); }
.slide-screens-thumb:hover .slide-screens-thumb-frame {
  border-color: color-mix(in srgb, var(--accent) 50%, var(--rule));
  box-shadow: 0 6px 14px -4px rgba(0, 0, 0, 0.2);
}
.slide-screens-thumb:hover .slide-screens-thumb-num {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg-elev);
}
.slide-screens-thumb:focus-visible { outline: none; }
.slide-screens-thumb:focus-visible .slide-screens-thumb-frame {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Current state: the thumb matches the screen currently shown as the
   hero. Reads as "you are here" -- accent border, accent number badge,
   no transform on rest (the stage rest state is the active state). */
.slide-screens-thumb.is-current .slide-screens-thumb-frame {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 18%, transparent),
              0 4px 10px -3px rgba(0, 0, 0, 0.18);
}
.slide-screens-thumb.is-current .slide-screens-thumb-num {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg-elev);
}

@media (prefers-reduced-motion: reduce) {
  .slide-screens-thumb { transition: none; }
  .slide-screens-thumb:hover { transform: none; }
}
/* Empty visual: dimmed placeholder. Reader reads as quietness, not
   as broken content. Sized to match a phone-shaped frame so the
   right column doesn't reflow to a different shape on empty stages. */
.slide-visual.is-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border-style: dashed;
  border-color: color-mix(in srgb, var(--rule) 80%, transparent);
  box-shadow: none;
  width: auto;
  height: clamp(440px, 70vh, 620px);
  aspect-ratio: 4 / 5;
  max-width: none;
  border-radius: 22px;
}
.slide-visual-empty-text {
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
  opacity: 0.55;
  font-weight: 600;
}
@media (max-width: 900px) {
  .slide-visual.is-portrait > .slide-visual-frame { height: clamp(320px, 56vh, 440px); }
  .slide-visual { max-width: min(85vw, 440px); }
  .slide-visual.is-empty { height: clamp(320px, 56vh, 440px); }
}

/* SLOT (generic): every supporting / body / closer block uses the
   same slot vocabulary. The slot eyebrow is the kit's only label
   voice on a slide -- one consistent uppercase tracking treatment
   so eight slot eyebrows on a slide read as one voice, not eight
   competing labels. Empty slots dim; their footprint stays so the
   eye lands in the same place when arrowing through stages. */
.slide-slot {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  min-width: 0;
}
.slide-slot.is-empty {
  opacity: 0.32;
}
.slide-slot.is-empty .slide-slot-body,
.slide-slot.is-empty .slide-quote-list,
.slide-slot.is-empty .slide-metrics,
.slide-slot.is-empty .slide-body-tags { display: none; }

.slide-slot-eyebrow {
  font-size: 0.7rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0;
}
.slide-slot-eyebrow-empty {
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--ink-muted);
  font-weight: 400;
  font-style: italic;
}

/* QUOTE -- the slide's emotional hook. No eyebrow chrome above; the
   italic display-font with accent left-rule is the signal. Quote
   sits at the top of the editorial column so reader meets the
   customer voice before the analysis. Bottom margin separates the
   emotional moment from the observational prose that follows. */
.slide-slot.is-quote {
  min-height: 5.5rem;
  margin: 0.4rem 0 0.8rem;
}
.slide-quote-list {
  list-style: none;
  margin: 0;
  padding: 0.1rem 0 0.1rem 1.15rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  border-left: 3px solid color-mix(in srgb, var(--accent) 55%, transparent);
}
.slide-quote-list li {
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 0.95rem + 0.6vw, 1.5rem);
  line-height: 1.32;
  font-style: italic;
  color: var(--ink);
  margin: 0;
  text-wrap: balance;
}

/* METRICS STRIP -- vital-signs band at the top of the editorial
   column. Each metric is a vertical stat block: label on top
   (uppercase eyebrow), value below (display weight). This is the
   standard presentation pattern -- handles mixed value lengths
   ("Real-time . 180ms" vs "1:30" vs an emoji) without the messy
   wrapping you get from horizontal label+value chips.

   Auto-fit grid handles light inventory (3 chips on WMP) and dense
   (7+ on template) equally -- fewer chips spread to fill the band,
   more chips wrap at consistent intervals. */
.slide-metrics-strip {
  padding: 0.85rem 0;
  border-top: 1px solid color-mix(in srgb, var(--rule) 70%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--rule) 70%, transparent);
  margin-bottom: 0.4rem;
}
.slide-metrics-strip .slide-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 0.85rem 1.5rem;
  align-items: start;
  padding: 0;
  border: 0;
}
.slide-metric {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
  min-width: 0;
  line-height: 1.25;
}
.slide-metric.is-empty { opacity: 0.32; }
.slide-metric-label {
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  flex-shrink: 0;
}
.slide-metric-value {
  font-size: 1.1rem;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  font-variant-numeric: tabular-nums;
  line-height: 1.25;
  font-weight: 600;
  font-family: var(--font-display);
}
/* Glyph-only values (emotion smiley, gate dash, trust marker) need
   real presence in the metric block -- they're the value, not an
   accent next to text. Sized to match the value-text cap-height
   so glyph-only and text-only metrics read at the same scale. */
.slide-metric-value svg {
  width: 1.4rem;
  height: 1.4rem;
  flex-shrink: 0;
  /* Promote stroke color to ink so glyphs read at presentation
     distance regardless of their map-mode default. */
  color: var(--ink);
}
.slide-metric.is-empty .slide-metric-value svg { color: var(--ink-muted); }
.slide-metric.is-breach .slide-metric-value svg { color: color-mix(in srgb, var(--state-break, #c75a5a) 80%, var(--ink)); }
.slide-metric.is-breach .slide-metric-value {
  color: color-mix(in srgb, var(--state-break, #c75a5a) 80%, var(--ink));
}

/* Emotion metric -- richer treatment than other metrics. The glyph
   sits on top in its own row at the same baseline as other metric
   values; the feelings list sits below it as an inline list with
   per-word severity color (matches map mode's .emotion-feeling-*
   palette). Reader sees both the visceral signal (face) and the
   semantic detail ("Frustrated, Helpless") without hijacking the
   strip's vertical rhythm. */
.slide-metric.is-emotion-positive .slide-metric-value {
  color: var(--hold-edge);
}
.slide-metric.is-emotion-anxious .slide-metric-value {
  color: var(--flex-edge);
}
.slide-metric.is-emotion-negative .slide-metric-value,
.slide-metric.is-emotion-frustrated .slide-metric-value,
.slide-metric.is-emotion-angry .slide-metric-value {
  color: var(--break-edge);
}
.slide-metric.is-emotion-neutral .slide-metric-value,
.slide-metric.is-emotion-resigned .slide-metric-value {
  color: var(--ink-muted);
}
/* Glyph SVG inherits via currentColor so the smiley is the same
   color as the value text. */
.slide-metric.is-emotion-positive .slide-metric-value svg,
.slide-metric.is-emotion-anxious .slide-metric-value svg,
.slide-metric.is-emotion-negative .slide-metric-value svg,
.slide-metric.is-emotion-neutral .slide-metric-value svg,
.slide-metric.is-emotion-frustrated .slide-metric-value svg,
.slide-metric.is-emotion-angry .slide-metric-value svg,
.slide-metric.is-emotion-resigned .slide-metric-value svg {
  color: currentColor;
}

/* When emotion has feelings, the value becomes a column: glyph row
   on top + feelings line below. The metric row's align-items: start
   in the strip grid lets this taller cell coexist with single-line
   peers without dragging them down. */
.slide-metric-value:has(.slide-metric-feelings) {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.32rem;
}
.slide-metric-emotion-glyph {
  display: inline-flex;
  align-items: center;
  /* Slide-viewing distance calibration. 1.4rem read as a token at
     slide widths; 2.4rem gives the face room to carry expression
     so the emotion's primary signal lands. Parity with map mode's
     36px default plus a step up for slide reading distance. */
  width: 2.4rem;
  height: 2.4rem;
}
.slide-metric-emotion-glyph svg {
  width: 100%;
  height: 100%;
  color: currentColor;
}
.slide-metric-feelings {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem;
  font-family: var(--font-text, inherit);
  /* Italic at 0.78rem read as squeezed at slide-viewing distance.
     Upright at 0.9rem reads as comfortable supporting detail. */
  font-size: 0.9rem;
  font-weight: 500;
  font-style: normal;
  line-height: 1.3;
  letter-spacing: 0;
}
.slide-metric-feeling {
  /* Per-feeling color picks up map mode's feeling severity. The
     glyph carries the dominant tone; individual words can carry
     finer-grain valence (e.g. stage 5: green "Relieved" + neutral
     "Wary" -- complex feelings that the single glyph can't capture). */
  white-space: nowrap;
}
.slide-metric-feeling.is-negative { color: var(--break-edge); }
.slide-metric-feeling.is-positive { color: var(--hold-edge); }
.slide-metric-feeling.is-neutral  { color: var(--ink-soft); }
.slide-metric-feeling.is-anxious  { color: var(--flex-edge); }
.slide-metric-feeling-sep {
  color: var(--ink-muted);
  opacity: 0.6;
}
.slide-metric-feeling-more {
  font-style: normal;
  font-size: 0.7rem;
  color: var(--ink-muted);
  font-weight: 400;
  letter-spacing: 0.02em;
}

/* Resolution chip dot: the same hold/flex/break dot vocabulary the
   grid-mode resolution row uses, scaled into the slide-metric chip.
   The chip's text carries the resolution name; the dot carries the
   state. Same color tokens, same shape, smaller scale. */
.slide-resolution-dot {
  display: inline-block;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  vertical-align: baseline;
  margin-right: 0.2rem;
  background: var(--ink-muted);
}
.slide-resolution-dot.is-hold { background: var(--hold-edge); }
.slide-resolution-dot.is-flex { background: var(--flex-edge); }
.slide-resolution-dot.is-break { background: var(--break-edge); }

/* Overflow indicator: when the artifact has more metrics than the
   slide cap, this quiet caption names the count without competing
   with the visible chips. Sits inside the strip's bottom border so
   it reads as a footnote on the band, not a separate slide line. */
.slide-metrics-more {
  margin: 0.5rem 0 0;
  font-size: 0.66rem;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
  font-style: italic;
  font-weight: 400;
}

/* BODY (frontstage / backstage / pain): stacked editorial blocks
   inside the left column. Each block is a self-contained unit with
   its own eyebrow, prose, and optional tags. Stacks vertically
   because the left column is narrower than the previous full-width
   row -- prose stays readable, blocks group as one editorial flow. */
.slide-body {
  display: flex;
  flex-direction: column;
  gap: 1.2rem;
}
.slide-body-block { min-height: 0; }
.slide-body-text {
  font-size: 1.05rem;
  line-height: 1.5;
  color: var(--ink);
  margin: 0;
}
.slide-body-block.is-pain .slide-body-text { color: var(--ink); }
.slide-body-block.is-pain {
  /* Pain is the journey's break moment -- give it visual weight as
     the dramatic conclusion of the slide's body. Border + tinted
     background use --state-break (the kit's breakdown semantic
     color) regardless of the stage's timeline position -- pain is
     a content signal about THIS moment, not a chrome signal about
     where in the timeline we are. */
  padding: 0.8rem 1.05rem 0.9rem;
  border-left: 3px solid color-mix(in srgb, var(--state-break, #c75a5a) 60%, transparent);
  background: color-mix(in srgb, var(--state-break, #c75a5a) 5%, transparent);
  border-radius: 0 6px 6px 0;
}
.slide-body-block.is-pain .slide-slot-eyebrow {
  color: color-mix(in srgb, var(--state-break, #c75a5a) 80%, var(--ink-muted));
}
.slide-body-block.is-pain.is-empty {
  /* Empty pain block at this stage -- quieter signal that there's
     no breakdown here. Reads as confirming silence, not as an empty
     slot. */
  padding: 0.45rem 0 0.45rem 1.05rem;
  border-left-color: color-mix(in srgb, var(--ink-muted) 25%, transparent);
  background: transparent;
  border-radius: 0;
  opacity: 0.6;
}

/* Per-item severity list inside the pain block. Mirrors map-mode's
   pc-critical / pc-detractor / pc-ok encoding: critical = red dot,
   detractor = amber dot, ok = green dot. Each item carries its own
   color so a slide with mixed severities reads the gradient (not
   "everything is critical"). */
.slide-pain-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}
.slide-pain-list li {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: 0.65rem;
  font-size: 1.02rem;
  line-height: 1.45;
  color: var(--ink);
  margin: 0;
}
.slide-pain-sev {
  font-size: 0.65rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 700;
  padding: 0.18rem 0.5rem;
  border-radius: 999px;
  white-space: nowrap;
  align-self: center;
  flex-shrink: 0;
  /* Default severity color (used if no class applied) */
  color: var(--ink-muted);
  background: color-mix(in srgb, var(--ink-muted) 10%, transparent);
}
.slide-pain-list li.is-critical .slide-pain-sev {
  color: var(--bg-elev);
  background: var(--state-break, #c75a5a);
}
.slide-pain-list li.is-detractor .slide-pain-sev {
  color: var(--bg-elev);
  background: var(--flex-edge, #d49a3a);
}
.slide-pain-list li.is-ok .slide-pain-sev {
  color: var(--bg-elev);
  background: color-mix(in srgb, var(--accent) 80%, transparent);
}
.slide-pain-text {
  /* No special color -- the badge does the severity work */
}
.slide-body-tags {
  margin-top: 0.4rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.slide-body-tag {
  display: inline-flex;
  align-items: center;
  padding: 0.18rem 0.55rem;
  font-size: 0.72rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: var(--ink-soft);
  background: color-mix(in srgb, var(--ink-muted) 4%, transparent);
}
@media (max-width: 900px) {
  .slide-body-tag { font-size: 0.7rem; }
}

/* CLOSER -- insight + recommendation as anchored stack inside the
   support zone. Each block is its own slot with a reserved min-height
   so empty closer states match populated heights. The recommendation
   block is the slide's only accent moment outside the page indicator. */
.slide-closer {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.7rem;
}
.slide-closer-block {
  padding: 0.85rem 1rem;
  border-radius: 8px;
  min-width: 0;
  min-height: 5rem;
}
.slide-closer-block.is-insight {
  /* Insight is reflective, not dramatic -- no chrome, just a left
     rule that nods at editorial commentary. Pain (above) is the
     loud block; insight (below) is the quiet one. */
  background: transparent;
  border: 0;
  border-left: 2px solid color-mix(in srgb, var(--ink-muted) 35%, transparent);
  padding: 0.4rem 0 0.4rem 1rem;
  border-radius: 0;
}
.slide-closer-block.is-rec {
  background: color-mix(in srgb, var(--accent) 6%, var(--bg-elev));
  border-left: 3px solid var(--accent);
}
.slide-closer-block.is-empty { opacity: 0.32; background: transparent; border: 1px dashed color-mix(in srgb, var(--rule) 80%, transparent); }
.slide-closer-block.is-rec.is-empty { border-left-style: dashed; }
.slide-closer-block .slide-slot-eyebrow { margin-bottom: 0.35rem; }
.slide-closer-block.is-rec .slide-slot-eyebrow { color: var(--accent); }
.slide-closer-text {
  font-size: 1.05rem;
  line-height: 1.45;
  margin: 0;
  color: var(--ink);
}
.slide-closer-block.is-insight .slide-closer-text { color: var(--ink-soft); font-style: italic; }
.slide-closer-block.is-rec .slide-closer-text { font-weight: 500; }

/* PLAYER CHROME -- one surface (top toolbar) carries everything:
   counter, phase, progress segments, prev/next arrows, mode toggle,
   exit. Editorial-tool pattern: chrome lives in the header so the
   canvas is reserved for content. The rail relocates into the
   toolbar at slide-mode init via JS (.slide-rail.is-in-toolbar);
   when relocated, drop the bottom-rail chrome (border, padding) and
   restyle as inline header element. */
.slide-rail {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.85rem 1.5rem 1.25rem;
  border-top: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  flex-shrink: 0;
}
.slide-rail.is-in-toolbar {
  /* Inline in the toolbar's center zone -- no border, no chrome of
     its own. Width sized to read as transport, not a wall. The track
     (phases + strip) sits between prev/next buttons. */
  padding: 0;
  border: 0;
  margin: 0;
  width: clamp(320px, 40vw, 520px);
  gap: 0.55rem;
  align-items: center;
}
.slide-rail-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.85rem;
  height: 1.85rem;
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  border-radius: 50%;
  cursor: pointer;
  color: var(--ink-soft);
  flex-shrink: 0;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.slide-rail.is-in-toolbar .slide-rail-btn {
  width: 1.65rem;
  height: 1.65rem;
  border: 0;
}
.slide-rail-btn:hover {
  background: color-mix(in srgb, var(--ink-muted) 10%, transparent);
  color: var(--ink);
  border-color: var(--rule);
}
.slide-rail-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.slide-rail-btn:disabled { opacity: 0.3; cursor: default; pointer-events: none; }

/* Story-style segment progress: each deck entry is a thin segment.
   Journey stages stretch (they're the artifact's spine). Part
   bookends are fixed-width dots (they're scaffolding -- visible but
   not the spine). The asymmetry encodes narrative hierarchy: stages
   carry the story, Parts frame it.

   Active segment fills accent. Click any segment to jump. Scales
   from 5 to 14+ entries without becoming visually noisy. */
.slide-rail-strip {
  display: flex;
  list-style: none;
  padding: 0;
  margin: 0;
  gap: 0.3rem;
  width: 100%;
  min-width: 0;
  align-items: center;
}
.slide-rail-strip li { display: flex; min-width: 0; }
/* Journey stages stretch to fill */
.slide-rail-strip li:has(.slide-rail-marker[data-type="stage"]) {
  flex: 1 1 0;
}
/* Part bookends are fixed-width markers */
.slide-rail-strip li:has(.slide-rail-marker[data-type="part"]) {
  flex: 0 0 auto;
}

.slide-rail-marker {
  display: block;
  width: 100%;
  height: 4px;
  border: 0;
  padding: 0;
  background: color-mix(in srgb, var(--col-glow, var(--accent)) 55%, transparent);
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.18s ease, height 0.18s ease, opacity 0.18s ease;
  position: relative;
}
.slide-rail-marker:hover {
  background: color-mix(in srgb, var(--col-glow, var(--accent)) 80%, transparent);
}
/* Part bookend markers: small fixed-width dots so they read as
   "different altitude" without text. Bookends sit outside the
   timeline gradient (no per-stage position), so they keep a neutral
   muted color rather than borrowing accent. */
.slide-rail-marker[data-type="part"] {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--ink-muted) 30%, transparent);
}
.slide-rail-marker::before {
  content: "";
  position: absolute;
  inset: -10px 0;
}
.slide-rail-marker[data-type="part"]::before {
  inset: -10px -4px;
}
.slide-rail-marker[data-type="part"]:hover {
  background: color-mix(in srgb, var(--ink-muted) 55%, transparent);
}
/* Active marker: keeps its own gradient color (so the rail's
   left-to-right gradient stays intact), grows in height + saturates
   to full color. Position signal = height delta. Color signal =
   timeline position (unchanged). */
.slide-rail-marker[aria-current="true"] {
  height: 8px;
  background: var(--col-glow, var(--accent));
}
.slide-rail-marker[data-type="part"][aria-current="true"] {
  width: 9px;
  height: 9px;
  background: var(--ink);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--ink-muted) 25%, transparent);
}
.slide-rail-marker:focus-visible {
  outline: 2px solid var(--col-glow, var(--accent));
  outline-offset: 4px;
}
@media (prefers-reduced-motion: reduce) {
  .slide-rail-marker { transition: none; }
}


/* =========================================================================
   PER-STAGE "OPEN IN SLIDES" AFFORDANCE
   -------------------------------------------------------------------------
   The whole .bp-stage-head becomes a click target when slide mode is
   wired up (.is-slide-trigger class added by app.js). A small glyph
   in the corner teaches the affordance; the whole cell is the click
   target so it's easy to hit.

   Quiet at rest -- cursor goes pointer on hover, accent tint on the
   cell, accent fill on the glyph. Same chrome principle: interactive
   earns affordance, but doesn't shout.
   ========================================================================= */

.bp-stage-head.is-slide-trigger {
  cursor: pointer;
  transition: background 0.15s ease;
}
.bp-stage-head.is-slide-trigger:hover {
  background: color-mix(in srgb, var(--accent-soft) 50%, var(--bg));
}
.bp-stage-head.is-slide-trigger:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Decorative glyph in the corner: visible at rest as a quiet hint;
   accents on stage hover. Doesn't intercept its own clicks (the head
   is the trigger). pointer-events: none so the click always lands on
   the head, not the glyph. */
.bp-stage-slide-cue {
  position: absolute;
  top: 0.4rem;
  /* Sit slightly inside the right edge so the phase-divider border
     (which sits on the cell's right edge for phase-end stages) has
     visual room. Without this, the glyph reads as colliding with the
     2px phase divider line. */
  right: 0.55rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  color: var(--ink-muted);
  opacity: 0.5;
  pointer-events: none;
  transition: opacity 0.15s ease, color 0.15s ease;
  z-index: 4;
}
.bp-stage-slide-cue svg { display: block; width: 13px; height: 13px; }

.bp-stage-head.is-slide-trigger:hover .bp-stage-slide-cue {
  opacity: 1;
  color: var(--accent);
}
.bp-stage-head.is-slide-trigger:focus-visible .bp-stage-slide-cue {
  opacity: 1;
  color: var(--accent);
}

/* Sticky-header clones inherit via cloneNode. Clone-side click
   handling is covered by document-level delegation in app.js. */
.bp-sticky-header .bp-stage-slide-cue {
  top: 0.25rem;
  right: 0.4rem;
  width: 1.1rem;
  height: 1.1rem;
}
.bp-sticky-header .bp-stage-slide-cue svg { width: 11px; height: 11px; }

@media (prefers-reduced-motion: reduce) {
  .bp-stage-head.is-slide-trigger { transition: none; }
  .bp-stage-slide-cue { transition: opacity 0.15s ease, color 0.15s ease; }
}


/* =========================================================================
   SLIDE TOOLBAR DECK-MODE TOGGLE
   -------------------------------------------------------------------------
   Compact segmented control: "All" / "Journey". Same affordance as
   before, less width so the right-zone fits theme + exit + mode
   without crowding. Sits in the right zone of the toolbar grid.
   ========================================================================= */

.slide-toolbar-deck-mode {
  display: inline-flex;
  align-items: center;
  background: color-mix(in srgb, var(--ink-muted) 6%, transparent);
  border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  border-radius: 999px;
  padding: 2px;
}
.slide-toolbar-mode {
  padding: 0.28rem 0.7rem;
  background: transparent;
  border: 0;
  border-radius: 999px;
  font-family: inherit;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  font-weight: 600;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}
.slide-toolbar-mode:hover { color: var(--ink); }
.slide-toolbar-mode[aria-pressed="true"] {
  background: var(--bg-elev);
  color: var(--ink);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.slide-toolbar-mode:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Below ~720px the deck-mode toggle hides entirely -- keyboard
   shortcuts (1-9) and arrow nav still work; visible toggle returns
   on wider screens. */
@media (max-width: 720px) {
  .slide-toolbar-deck-mode { display: none; }
}


/* Strip part-segment differentiation handled inline above via
   .slide-rail-marker[data-type="part"]. The legacy
   .slide-rail-marker-part class still gets added to the button by
   JS for backward compatibility, but story-style segments don't
   render text -- the bar shape is universal. */


/* =========================================================================
   PART SLIDES
   -------------------------------------------------------------------------
   Bookend slides use a single-column layout (no visual zone, no
   support cluster). Hero header at top, the Part's primary content
   composed below. Each Part composer emits a marker class
   (.is-part1, .is-part2, .is-part4, .is-part5) for variant-specific
   styling if needed.
   ========================================================================= */

.slide-content.is-part-slide {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto;
  grid-template-areas:
    "header"
    "body";
  max-width: 1100px;
  /* Inherit alignment from the parent canvas. The canvas decides
     whether to top-align (Part bookend default for variable-height
     content) or center (stage slides with predictable size). The
     earlier `align-self: center` override forced inner centering
     regardless of canvas alignment, which pushed the top of dense
     Part 5 slides off-screen. Auto lets the parent's `align-items`
     win. */
  align-self: auto;
  height: auto;
  row-gap: 1.6rem;
}
.slide-content.is-part-slide .slide-header { grid-area: header; }
.slide-part-body {
  grid-area: body;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  min-height: 0;
}

/* Part 1 hero stat variant */
.slide-hero-stat {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: flex-start;
  padding: 1.5rem 0;
}
.slide-hero-stat-num {
  font-family: var(--font-display);
  font-size: clamp(3rem, 2rem + 4vw, 5.5rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--accent);
  margin: 0;
  font-variant-numeric: tabular-nums;
}
.slide-hero-stat-claim {
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 1rem + 0.6vw, 1.75rem);
  line-height: 1.25;
  font-weight: 600;
  color: var(--ink);
  margin: 0;
  text-wrap: balance;
  max-width: 32ch;
}
.slide-hero-stat-detail {
  font-size: 1rem;
  color: var(--ink-soft);
  margin: 0;
  line-height: 1.55;
  max-width: 60ch;
}

/* Part 1 pillar / bet cards.
   Flex + per-card max-width via :has() for last-row balance.
   1 card: full width. 2 cards: 50/50. 3 cards: 33% each.
   4 cards: 2x2. 5 cards: 3+2 (1-3 at 33%, 4-5 at 50%). 6+: 33% all.
   Mirrors the patterns-grid model so all three text-slide card
   archetypes share one balanced-layout vocabulary. */
.slide-bet-row {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
}
.slide-bet-item {
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
  /* Basis matches the 33% cap so 4 cards never pack onto one row.
     Without this, a 15rem basis lets 4 cards fit on wide slides
     before wrapping, defeating the 3+2 intent. */
  flex: 1 1 calc(33% - 1.25rem);
  max-width: calc(33% - 1.25rem);
  padding: 1.15rem 1.3rem;
  background: color-mix(in srgb, var(--ink-muted) 3%, var(--bg-elev));
  border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  border-radius: 8px;
}
/* 1 or 2 cards: full or 50/50. */
.slide-bet-row:has(> .slide-bet-item:nth-child(2):last-child) .slide-bet-item,
.slide-bet-row:has(> .slide-bet-item:only-child) .slide-bet-item {
  max-width: calc(50% - 1.25rem / 2);
}
/* 4 cards: 2x2. */
.slide-bet-row:has(> .slide-bet-item:nth-child(4):last-child) .slide-bet-item {
  max-width: calc(50% - 1.25rem / 2);
}
/* 5 cards: 1-3 at 33%, 4-5 lift to 50%. */
.slide-bet-row:has(> .slide-bet-item:nth-child(5):last-child) > .slide-bet-item:nth-child(4),
.slide-bet-row:has(> .slide-bet-item:nth-child(5):last-child) > .slide-bet-item:nth-child(5) {
  max-width: calc(50% - 1.25rem / 2);
}
.slide-bet-num {
  /* Eyebrow bump from 0.7 to 0.8. At slide-viewing distance 0.7
     read as decorative chrome; 0.8 reads as a scannable label. */
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
}
.slide-bet-name {
  font-family: var(--font-display);
  font-size: 1.15rem;
  line-height: 1.25;
  font-weight: 600;
  color: var(--ink);
  margin: 0;
}
.slide-bet-why {
  font-size: 0.9rem;
  color: var(--ink-soft);
  line-height: 1.6;
  margin: 0;
}

/* Part 1 diagnostic line */
.slide-diagnostic {
  display: inline-block;
  padding: 0.5rem 1rem;
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-size: 0.95rem;
  color: var(--ink);
  margin: 0;
}
.slide-diagnostic strong {
  color: var(--accent);
  font-weight: 600;
}

/* Part 2 KPI strip */
.slide-kpi-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 1.5rem;
  padding: 1rem 0;
}
.slide-kpi {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.slide-kpi-num {
  font-family: var(--font-display);
  font-size: clamp(2rem, 1.5rem + 1.5vw, 3rem);
  line-height: 1;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.slide-kpi-label {
  font-size: 0.95rem;
  color: var(--ink);
  font-weight: 500;
}
.slide-kpi-sub {
  font-size: 0.78rem;
  color: var(--ink-muted);
  font-style: italic;
}

/* Part 4 pattern cards.
   Flex layout with per-card targeting for last-row balance. The
   challenge: "5 cards as 3+2" requires cards 1-3 at 33% AND cards
   4-5 at 50% in the same grid. Per-card max-width via
   :nth-child(N):has(grid with exactly 5 cards) achieves this.
   See styles.css comment block on .patterns-grid for reasoning. */
.slide-patterns-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
}
.slide-pattern-card {
  /* Flex-basis matches the 33% cap so 4 cards never pack onto
     row 1. Earlier basis of 15rem let 4 cards fit on wide slides
     before wrapping, defeating the 3+2 intent. */
  flex: 1 1 calc(33% - 1.25rem);
  max-width: calc(33% - 1.25rem);
  padding: 1.1rem 1.25rem;
  background: color-mix(in srgb, var(--ink-muted) 3%, var(--bg-elev));
  border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
.slide-patterns-grid .slide-pattern-glyph {
  width: 1.25rem;
  height: 1.25rem;
}
/* Density relief on text-heavy slides. The earlier overrides
   actively shrank pattern card text below baseline (body 0.82
   vs base 0.9, example 0.74 vs base 0.82) to fit a 3+2 layout.
   At slide viewing distance the result read as cramped paragraph
   walls. Restore body to 0.9, example to 0.82, bump grid name
   to 1.05, loosen line-heights for reading comfort. The 3+2
   layout still holds via the flex-basis correction above. */
.slide-patterns-grid .slide-pattern-name {
  font-size: 1.05rem;
}
.slide-patterns-grid .slide-pattern-body {
  font-size: 0.9rem;
  line-height: 1.6;
}
.slide-patterns-grid .slide-pattern-example {
  font-size: 0.82rem;
  line-height: 1.55;
}
/* 1, 2 cards: each at 50% (pair-up). 3 cards: 33% each (one row).
   4 cards: 2x2 (50% each). 5 cards: 3+2 (1-3 at 33%, 4-5 at 50%).
   6+ cards: 33% all (dense 3-per-row). */
.slide-patterns-grid:has(> .slide-pattern-card:nth-child(2):last-child) .slide-pattern-card,
.slide-patterns-grid:has(> .slide-pattern-card:only-child) .slide-pattern-card {
  max-width: calc(50% - 1.25rem / 2);
}
.slide-patterns-grid:has(> .slide-pattern-card:nth-child(4):last-child) .slide-pattern-card {
  max-width: calc(50% - 1.25rem / 2);
}
/* 5 cards: lift cap on cards 4 and 5 only so the second row's two
   cards each fill half the row. Cards 1-3 stay at the base 33%. */
.slide-patterns-grid:has(> .slide-pattern-card:nth-child(5):last-child) > .slide-pattern-card:nth-child(4),
.slide-patterns-grid:has(> .slide-pattern-card:nth-child(5):last-child) > .slide-pattern-card:nth-child(5) {
  max-width: calc(50% - 1.25rem / 2);
}
.slide-pattern-glyph {
  display: inline-flex;
  width: 1.6rem;
  height: 1.6rem;
  color: var(--accent);
}
.slide-pattern-glyph svg { width: 100%; height: 100%; }
.slide-pattern-num {
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
}
.slide-pattern-name {
  font-family: var(--font-display);
  font-size: 1.1rem;
  line-height: 1.3;
  font-weight: 600;
  color: var(--ink);
  margin: 0;
}
.slide-pattern-body {
  font-size: 0.9rem;
  color: var(--ink-soft);
  line-height: 1.5;
  margin: 0;
}
.slide-pattern-example {
  font-size: 0.82rem;
  color: var(--ink-muted);
  font-style: italic;
  margin: 0;
}

/* Part 5 recommendation + roadmap.
   Tightened so the recommendation reads as the slide's thesis
   without dominating the slide. Earlier padding 1.1rem 1.25rem
   plus clamp upper 1.5rem made the block visually overpower the
   guarantees / roadmap cards below. Pull padding to 1rem 1.15rem
   and clamp upper to 1.35rem; the recommendation still leads, the
   downstream cards now share the slide's presence. */
.slide-recommendation {
  padding: 1rem 1.15rem;
  background: color-mix(in srgb, var(--accent) 6%, var(--bg-elev));
  border-left: 3px solid var(--accent);
  border-radius: 8px;
}
.slide-recommendation-eyebrow {
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 700;
  margin: 0 0 0.5rem;
}
.slide-recommendation-text {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 0.9rem + 0.4vw, 1.35rem);
  line-height: 1.35;
  font-weight: 500;
  color: var(--ink);
  margin: 0;
  text-wrap: balance;
}

/* Roadmap. Same flex + :has() 3+2 pattern as bet-row and
   patterns-grid. Earlier auto-fit minmax(15rem, 1fr) packed 4+1
   at slide widths for 5-card sets; flex with per-card max-width
   forces 1-3 at 33% and 4-5 at 50%. */
.slide-roadmap {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
}
.slide-roadmap-phase {
  flex: 1 1 calc(33% - 1.25rem);
  max-width: calc(33% - 1.25rem);
  padding: 1.15rem 1.3rem;
  background: color-mix(in srgb, var(--ink-muted) 3%, var(--bg-elev));
  border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  border-radius: 8px;
}
/* 1 or 2 cards. */
.slide-roadmap:has(> .slide-roadmap-phase:nth-child(2):last-child) .slide-roadmap-phase,
.slide-roadmap:has(> .slide-roadmap-phase:only-child) .slide-roadmap-phase {
  max-width: calc(50% - 1.25rem / 2);
}
/* 4 cards: 2x2. */
.slide-roadmap:has(> .slide-roadmap-phase:nth-child(4):last-child) .slide-roadmap-phase {
  max-width: calc(50% - 1.25rem / 2);
}
/* 5 cards: 1-3 at 33%, 4-5 lift to 50%. */
.slide-roadmap:has(> .slide-roadmap-phase:nth-child(5):last-child) > .slide-roadmap-phase:nth-child(4),
.slide-roadmap:has(> .slide-roadmap-phase:nth-child(5):last-child) > .slide-roadmap-phase:nth-child(5) {
  max-width: calc(50% - 1.25rem / 2);
}
.slide-roadmap-phase-eyebrow {
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  margin: 0 0 0.4rem;
}
.slide-roadmap-phase-name {
  font-family: var(--font-display);
  font-size: 1.05rem;
  line-height: 1.3;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 0.3rem;
}
.slide-roadmap-phase-body {
  font-size: 0.9rem;
  color: var(--ink-soft);
  line-height: 1.6;
  margin: 0;
}


/* Part 5 resolution pairs in slide mode.
   Compressed two-column variant of the map-mode .resolution-pair.
   Each pair sits as one row: numbered head, current state on the
   left, an inline arrow, future state on the right, meta beneath.
   Calibrated for slide viewing distance. */
.slide-resolution-pairs {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}
.slide-resolution-pair {
  display: grid;
  grid-template-columns: auto 1fr 1.6rem 1fr;
  align-items: stretch;
  gap: 0.85rem;
  padding: 0.9rem 1.1rem;
  background: color-mix(in srgb, var(--ink-muted) 3%, var(--bg-elev));
  border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  border-radius: 8px;
}
.slide-resolution-pair-num {
  font-size: 0.8rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  align-self: center;
  font-variant-numeric: tabular-nums;
  min-width: 1.5rem;
}
.slide-resolution-pair-col {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
}
/* Slide-mode label: the column's state name as a small-caps eyebrow
   colored by state (break-edge for the current column, hold-edge
   for the future column). No leading dot; the color discipline plus
   the column's bg tint already encode the state. */
.slide-resolution-pair-label {
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 700;
}
.slide-resolution-pair-col.is-current .slide-resolution-pair-label {
  color: var(--break-edge);
}
.slide-resolution-pair-col.is-future .slide-resolution-pair-label {
  color: var(--hold-edge);
}
.slide-resolution-pair-text {
  font-size: 0.95rem;
  color: var(--ink);
  line-height: 1.5;
  margin: 0;
}
.slide-resolution-pair-name {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 0.15rem;
  letter-spacing: -0.005em;
}
.slide-resolution-pair-arrow {
  align-self: center;
  font-size: 1.4rem;
  color: var(--ink-muted);
  line-height: 1;
  text-align: center;
  user-select: none;
}
.slide-resolution-pair-meta {
  grid-column: 2 / -1;
  margin: 0.25rem 0 0;
  padding-top: 0.55rem;
  border-top: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  font-size: 0.78rem;
  color: var(--ink-muted);
  line-height: 1.4;
  letter-spacing: 0.01em;
}
.slide-resolution-pair-meta strong {
  color: var(--ink-soft);
  font-weight: 600;
}

/* Stack on narrow widths so each pair reads top-to-bottom rather
   than left-to-right. Same fallback shape as the map-mode pair. */
@media (max-width: 720px) {
  .slide-resolution-pair {
    grid-template-columns: auto 1fr;
  }
  .slide-resolution-pair-arrow { display: none; }
  .slide-resolution-pair-meta { grid-column: 1 / -1; }
}


/* Part 5 scope boundaries in slide mode.
   Smaller register than the resolution pairs above. Sits as a
   tinted aside with an eyebrow + intro + numbered constraint list.
   Earns its place when the artifact's ask is substantial enough
   that the reader needs to know what scope does NOT cover. */
.slide-scope-boundaries {
  padding: 1rem 1.15rem;
  background: color-mix(in srgb, var(--ink-muted) 3%, var(--bg-elev));
  border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  border-radius: 8px;
}
.slide-scope-boundaries-eyebrow {
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  margin: 0 0 0.4rem;
}
.slide-scope-boundaries-intro {
  font-size: 0.95rem;
  color: var(--ink);
  line-height: 1.5;
  margin: 0 0 0.7rem;
  max-width: 70ch;
}
.slide-scope-boundaries-list {
  margin: 0;
  padding-left: 1.4rem;
  display: grid;
  gap: 0.5rem;
}
.slide-scope-boundaries-list li {
  font-size: 0.88rem;
  color: var(--ink-soft);
  line-height: 1.5;
  max-width: 70ch;
}
.slide-scope-boundaries-list li strong {
  color: var(--ink);
  font-weight: 600;
}


/* Section intro text in slide mode. Body register that bridges
   from the recommendation aside to the structural variant below
   (resolution pairs, roadmap, etc). Carries the framing prose
   that anchors the cards' argument. Reads as one beat between
   the thesis aside and the structured content. */
.slide-section-intro-text {
  font-size: 1.05rem;
  color: var(--ink);
  line-height: 1.55;
  margin: 0;
  max-width: 70ch;
  text-wrap: pretty;
}


/* Diagnosis bridge components in slide mode.
   The bridge between Evidence and Takeaway has its own slide
   subsequence (diagnosis-a, diagnosis-b, diagnosis-c) when the
   artifact uses the 6-Part variant. Each sub-slide carries one
   beat: the architectural diagnosis, the cost-of-inaction analysis,
   the investment imperative plus the failed-prior-attempt signal. */
.slide-diagnosis-statement {
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 0.95rem + 0.6vw, 1.5rem);
  line-height: 1.5;
  font-weight: 500;
  color: var(--ink);
  margin: 0;
  max-width: 70ch;
  text-wrap: pretty;
  letter-spacing: -0.005em;
}
.slide-diagnosis-statement strong {
  color: var(--accent);
  font-weight: 600;
}
.slide-cost-table {
  width: 100%;
  border-collapse: collapse;
  margin: 0;
  font-size: 0.88rem;
}
.slide-cost-table thead th {
  text-align: left;
  padding: 0.5rem 0.85rem;
  background: color-mix(in srgb, var(--ink-muted) 4%, var(--bg-elev));
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  border-bottom: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
}
.slide-cost-table tbody th,
.slide-cost-table tbody td {
  text-align: left;
  padding: 0.7rem 0.85rem;
  border-bottom: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  vertical-align: top;
  line-height: 1.45;
}
.slide-cost-table tbody tr:last-child th,
.slide-cost-table tbody tr:last-child td { border-bottom: 0; }
.slide-cost-table tbody th {
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  width: 14ch;
}
.slide-cost-table tbody td:first-of-type {
  font-weight: 600;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.slide-cost-table tbody td:last-child {
  color: var(--ink-soft);
}
.slide-investment-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.85rem;
}
.slide-investment-item {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.85rem 1rem;
  background: color-mix(in srgb, var(--ink-muted) 3%, var(--bg-elev));
  border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  border-radius: 8px;
}
.slide-investment-item-addresses {
  font-size: 0.65rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 700;
  margin: 0;
}
.slide-investment-item-name {
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  margin: 0;
  color: var(--ink);
  line-height: 1.3;
}
.slide-investment-item-cost {
  font-size: 0.82rem;
  color: var(--ink-soft);
  line-height: 1.5;
  margin: 0;
}
.slide-diagnostic-signal {
  padding: 0.85rem 1rem;
  background: color-mix(in srgb, var(--break-bg) 25%, var(--bg-elev));
  border-left: 3px solid var(--break-edge);
  border-radius: 0 8px 8px 0;
}
.slide-diagnostic-signal-eyebrow {
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--break-edge);
  font-weight: 700;
  margin: 0 0 0.4rem;
}
.slide-diagnostic-signal-claim {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 0.6rem;
  line-height: 1.4;
}
.slide-diagnostic-signal-list {
  margin: 0;
  padding-left: 1.4rem;
  display: grid;
  gap: 0.35rem;
}
.slide-diagnostic-signal-list li {
  font-size: 0.85rem;
  color: var(--ink-soft);
  line-height: 1.45;
}
.slide-diagnostic-signal-list li strong {
  color: var(--ink);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Stack investment grid on narrow slide widths. */
@media (max-width: 720px) {
  .slide-investment-grid { grid-template-columns: 1fr; }
}


/* Section closer in slide mode. Display-register triplet or
   capper at the bottom of a Part bookend slide. Calibrated for
   slide viewing distance, max-width keeps the line from running
   the full slide width. */
.slide-section-closer {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 0.85rem + 0.5vw, 1.3rem);
  line-height: 1.4;
  font-weight: 500;
  color: var(--ink);
  letter-spacing: -0.005em;
  max-width: 65ch;
  margin: 0.8rem 0 0;
  text-wrap: balance;
}


.slide-fallback-body {
  font-size: 1rem;
  color: var(--ink-soft);
  line-height: 1.55;
  max-width: 65ch;
  margin: 0;
}


/* Part 3 journey intro slide -- arc preview ("Stage 1 -> Stage N")
   that anchors what's coming. The next N slides are the stages
   themselves; this slide is the "we're entering the journey" beat. */
.slide-journey-intro {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  padding-top: 0.75rem;
  margin-top: 0.5rem;
  border-top: 1px solid color-mix(in srgb, var(--rule) 70%, transparent);
}
.slide-journey-intro-eyebrow {
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  margin: 0;
}
.slide-journey-intro-arc {
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 0.95rem + 0.5vw, 1.45rem);
  line-height: 1.3;
  color: var(--ink);
  margin: 0;
}
.slide-journey-intro-arc span { font-weight: 600; }


/* =========================================================================
   SLIDE MODE: Jobs to be done eyebrow line + Key moments slot
   Added v0.6 polish pass after the parity audit caught two live
   template rows the slide composer wasn't reading.

   - Jobs eyebrow: ink-soft single line above the slide title, repeats
     across every slide of a job's span (intent persists). Visual
     restraint matters here -- the title is the slide's primary message;
     the job is supporting context. Smaller weight, muted color.

   - Key moments slot: editorial-column slot between metrics strip and
     customer-thought quote. Severity diamond per item maps to the kit's
     hold/flex/break vocabulary. Most moments neutral; only critical
     events earn color.
   ========================================================================= */

/* Jobs to be done -- eyebrow line above the slide title.
   Sits between the stage eyebrow and the title. Visual restraint is
   load-bearing: it stays clearly subordinate to the title so the
   slide's primary message keeps its weight. */
.slide-hero-job {
  font-size: 0.92rem;
  line-height: 1.35;
  color: var(--ink-soft);
  margin: 0 0 0.55rem;
  max-width: 50ch;
  font-style: italic;
  /* No tabular-nums, no transform -- this reads as voice, not chrome.
     The italic stems from the row's first-person framing in grid
     mode (jobs are the actor's voice). Mirrors the convention. */
}
.slide-hero-job-label {
  font-style: normal;
  font-weight: 600;
  font-size: 0.66rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--col-glow, var(--accent));
  margin-right: 0.35rem;
  /* Eyebrow-style "JOB:" prefix picks up the col-glow gradient like
     other chrome eyebrows on the slide. Teaches "this line is the
     intent, distinct from the title below." */
}
.slide-hero-job-text {
  /* Italic carried from parent .slide-hero-job */
}

/* Key moments slot -- editorial column between metrics and quote.
   Three altitudes adjacent: metrics (measurable cost) -> moments
   (consequential events) -> thought (inner voice). Reader's eye walks
   the column; each block earns its slot. */
.slide-slot.is-moments {
  margin: 0 0 1.05rem;
  padding: 0.5rem 0;
  /* No background, no border -- moments are part of the editorial
     flow, not a separate surface. The list items carry their own
     visual weight via severity color. */
}
.slide-slot.is-moments .slide-slot-eyebrow {
  margin-bottom: 0.45rem;
}
.slide-moments-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.slide-moments-list li {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.6rem;
  align-items: baseline;
  font-size: 0.93rem;
  line-height: 1.4;
  color: var(--ink);
}
.slide-moment-diamond {
  display: inline-block;
  width: 0.55rem;
  height: 0.55rem;
  transform: rotate(45deg);
  /* Neutral diamond: more present than ink-muted at 55% so the row
     scans as a list with severity markers, not as a list with
     optional dots. Severity classes below override fill for
     positive / detractor / critical events. */
  background: color-mix(in srgb, var(--ink-soft) 80%, transparent);
  flex-shrink: 0;
  align-self: center;
}
.slide-moments-list li.is-positive .slide-moment-diamond {
  background: color-mix(in srgb, #5fa367 80%, var(--ink));
}
.slide-moments-list li.is-detractor .slide-moment-diamond {
  background: color-mix(in srgb, #d49a3a 80%, var(--ink));
}
.slide-moments-list li.is-critical .slide-moment-diamond {
  background: color-mix(in srgb, #c75a5a 80%, var(--ink));
}
/* Critical moments earn a touch more weight in the text too -- not
   loud, just enough that a reader scanning the column registers the
   severity without reading every word. */
.slide-moments-list li.is-critical .slide-moment-text {
  color: color-mix(in srgb, var(--ink) 85%, var(--state-break, #c75a5a));
  font-weight: 500;
}
.slide-moment-text {
  /* Default styling -- neutral and detractor inherit from li */
}



/* =========================================================================
   MD VIEWER (in-page lightbox-style document reader)
   -------------------------------------------------------------------------
   Centered dialog overlay for reading kit doctrine docs (changelog,
   doctrine, conformance, etc.) without leaving the current page.
   Backdrop dims the page; reading column sits centered with comfortable
   line length and the kit's typography. Theme-aware via the kit's
   token system.

   Reusing the kit's existing :where(a) link baseline, headings via
   --font-display, color via --ink / --ink-soft / --accent. The
   viewer-specific rules below add chrome (overlay positioning, dialog
   shape, close button, scroll behavior) and reading-column spacing.
   ========================================================================= */

.md-viewer {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--pad-l);
  /* Animate in via opacity + scale on the dialog */
  opacity: 0;
  transition: opacity 0.18s ease;
}
.md-viewer[data-active="true"] {
  opacity: 1;
}
.md-viewer[hidden] {
  display: none;
}
/* Lock body scroll while the viewer is open so wheel events
   don't bleed through to the page underneath. */
body.md-viewer-open {
  overflow: hidden;
}

.md-viewer-backdrop {
  position: absolute;
  inset: 0;
  /* Translucent over the page so the reader can still see the
     page underneath -- they're still on start-here, not navigated
     somewhere else. Stronger blur + lighter dim keeps depth
     without occluding context. */
  background: color-mix(in srgb, var(--bg) 70%, transparent);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  cursor: pointer;
}

/* Dialog: centered reading surface with a max-width capped for
   comfortable line length. Tall (90vh) so long docs have room
   without forcing scroll on the body underneath. */
.md-viewer-dialog {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 780px;
  max-height: 90vh;
  background: var(--bg-elev);
  border: 1px solid var(--rule);
  border-radius: 10px;
  box-shadow: 0 18px 60px -16px rgba(0, 0, 0, 0.32),
              0 4px 14px -4px rgba(0, 0, 0, 0.2);
  overflow: hidden;
  transform: scale(0.985);
  transition: transform 0.22s ease;
}
.md-viewer[data-active="true"] .md-viewer-dialog {
  transform: scale(1);
}

.md-viewer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--pad-m);
  padding: 0.7rem 0.8rem 0.7rem 1.1rem;
  border-bottom: 1px solid var(--rule);
  background: color-mix(in srgb, var(--bg) 60%, var(--bg-elev));
  flex-shrink: 0;
}
.md-viewer-head-left {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
  flex: 1;
}
.md-viewer-head-actions {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  flex-shrink: 0;
}
/* "MD" badge: tells visitors they're inside a markdown viewer,
   not a separate page. Tinted accent so it reads as a category
   chip, paired with the title. */
.md-viewer-badge {
  display: inline-flex;
  align-items: center;
  padding: 0.15rem 0.5rem;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.68rem;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 0.04em;
  flex-shrink: 0;
}
.md-viewer-titles {
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
  min-width: 0;
}
.md-viewer-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
/* Last-updated date: small ink-muted line under the title.
   Tells readers (and Kiro) when the doc was last touched
   without making them scroll to the bottom. Hidden when the
   doc has no last-updated marker. */
.md-viewer-meta-date {
  margin: 0;
  font-size: 0.7rem;
  color: var(--ink-muted);
  letter-spacing: 0.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.md-viewer-meta-date[hidden] {
  display: none;
}
.md-viewer-back,
.md-viewer-close,
.md-viewer-mode,
.md-viewer-external {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  padding: 0.4rem 0.55rem;
  background: transparent;
  border: 0;
  cursor: pointer;
  color: var(--ink-soft);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  border-radius: 6px;
  transition: color 0.15s ease, background 0.15s ease;
  text-decoration: none;
}
.md-viewer-back:hover,
.md-viewer-close:hover,
.md-viewer-mode:hover,
.md-viewer-external:hover {
  color: var(--ink);
  background: color-mix(in srgb, var(--ink-muted) 10%, transparent);
}
.md-viewer-back:focus-visible,
.md-viewer-close:focus-visible,
.md-viewer-mode:focus-visible,
.md-viewer-external:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.md-viewer-back[hidden] {
  display: none;
}
.md-viewer-close {
  width: 2.2rem;
  height: 2.2rem;
  padding: 0;
  flex-shrink: 0;
}
.md-viewer-external {
  width: 2.2rem;
  height: 2.2rem;
  padding: 0;
  flex-shrink: 0;
}
/* Mode toggle: shows the "raw" icon at rest (the action it
   triggers); shows the "rendered" icon when in raw mode (so the
   button telegraphs "switch back"). The icons swap via the
   aria-pressed state. */
.md-viewer-mode {
  flex-shrink: 0;
}
.md-viewer-mode .md-viewer-mode-icon-raw,
.md-viewer-mode[aria-pressed="true"] .md-viewer-mode-icon-rendered {
  display: inline-flex;
}
.md-viewer-mode .md-viewer-mode-icon-rendered,
.md-viewer-mode[aria-pressed="true"] .md-viewer-mode-icon-raw {
  display: none;
}
.md-viewer-mode[aria-pressed="true"] {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.md-viewer-mode-label {
  font-size: 0.72rem;
  letter-spacing: 0.03em;
}

/* Hide the back-button label on narrow viewports; keep the
   icon. Same compress pattern the close+external use. */
@media (max-width: 540px) {
  .md-viewer-back span,
  .md-viewer-mode-label {
    display: none;
  }
}

/* Reading body: scrollable, generous padding, comfortable line
   height for long-form prose. Outline only on focus (keyboard
   tabbed in) so mouse users don't see a focus ring on the body. */
.md-viewer-body {
  flex: 1;
  overflow-y: auto;
  padding: var(--pad-l) 2.4rem;
  line-height: 1.7;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--ink-muted) 28%, transparent) transparent;
}
.md-viewer-body:focus { outline: none; }
.md-viewer-body:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -3px;
  border-radius: 4px;
}
.md-viewer-body::-webkit-scrollbar { width: 8px; }
.md-viewer-body::-webkit-scrollbar-track { background: transparent; }
.md-viewer-body::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--ink-muted) 28%, transparent);
  border-radius: 999px;
}

/* Loading + error states. */
.md-viewer-loading,
.md-viewer-error {
  color: var(--ink-muted);
  font-size: 0.95rem;
  text-align: center;
  padding: var(--pad-xl) 0;
}
.md-viewer-error code {
  background: color-mix(in srgb, var(--ink-muted) 12%, transparent);
  padding: 0.1rem 0.35rem;
  border-radius: 3px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.92em;
}

/* Markdown content typography. Reuses the kit's display font for
   headings and body font for prose. Spacing tuned for long-form
   reading (more generous than start-here's section bodies). */
.md-viewer-body h1 {
  font-family: var(--font-display);
  font-size: clamp(1.7rem, 1.3rem + 1.2vw, 2.25rem);
  font-weight: 600;
  letter-spacing: -0.018em;
  line-height: 1.15;
  margin: 0 0 var(--pad-m);
  color: var(--ink);
  text-wrap: balance;
}
.md-viewer-body h2 {
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 1rem + 0.7vw, 1.55rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.2;
  margin: var(--pad-l) 0 var(--pad-s);
  padding-top: var(--pad-m);
  border-top: 1px solid color-mix(in srgb, var(--rule) 70%, transparent);
  color: var(--ink);
}
.md-viewer-body h2:first-of-type {
  border-top: 0;
  padding-top: 0;
  margin-top: 0;
}

/* Collapsible H2 sections inside the viewer.
   The applyHeadingCollapse() post-processor wraps each H2 + its
   following content in <details open class="md-viewer-section">.
   The original H2's styling (border-top, padding, font register)
   moves to the <details> element so the visual rhythm of the
   document is preserved -- just with a clickable chevron that
   collapses the section.

   Default state is open; readers collapse what they want to
   skim past. Native disclosure means keyboard + AT just work. */
.md-viewer-section {
  margin: var(--pad-l) 0 var(--pad-s);
  padding-top: var(--pad-m);
  border-top: 1px solid color-mix(in srgb, var(--rule) 70%, transparent);
}
.md-viewer-section:first-of-type {
  border-top: 0;
  padding-top: 0;
  margin-top: 0;
}
.md-viewer-section-summary {
  /* Chevron leads, heading follows. Disclosure affordance at
     the start of the row -- the universal collapsible-tree
     pattern. Cursor + keyboard activation come from native
     <summary>. */
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: baseline;
  gap: 0.55rem;
  padding: 0;
  margin: 0;
  border-radius: 4px;
  transition: color 0.15s ease;
}
.md-viewer-section-summary::-webkit-details-marker {
  display: none;
}
.md-viewer-section-summary::marker {
  display: none;
}
.md-viewer-section-summary:hover .md-viewer-section-heading,
.md-viewer-section-summary:hover .md-viewer-section-chevron {
  color: var(--accent);
}
.md-viewer-section-summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}
.md-viewer-section-heading {
  /* Inherit the original h2 register exactly; this is the
     heading the reader sees, just inside a summary now. */
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 1rem + 0.7vw, 1.55rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.2;
  margin: 0;
  padding: 0;
  border: 0;
  color: var(--ink);
  flex: 1 1 auto;
  min-width: 0;
  transition: color 0.15s ease;
}
/* Chevron: leads the heading. Points right at rest, rotates
   down when [open] -- direction maps to "expands downward."
   Sized to read at the heading's optical weight (slightly
   larger than the right-side version had to compensate for
   the heading's display-font scale). */
.md-viewer-section-chevron {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  color: var(--ink-muted);
  transform: rotate(-90deg);
  transition: transform 0.18s ease, color 0.15s ease;
  /* Tiny optical adjustment: align chevron baseline with the
     heading's cap-height instead of the heading's baseline,
     since chevrons live in their own visual band. */
  position: relative;
  top: -0.15em;
}
.md-viewer-section-chevron svg {
  width: 100%;
  height: 100%;
}
.md-viewer-section[open] > .md-viewer-section-summary .md-viewer-section-chevron {
  transform: rotate(0deg);
}
/* When collapsed, dim the heading slightly so the open sections
   read as the reader's current focus and collapsed ones read as
   "set aside for now." Subtle -- the heading still scans, just
   doesn't compete. */
.md-viewer-section:not([open]) .md-viewer-section-heading {
  color: var(--ink-soft);
}
@media (prefers-reduced-motion: reduce) {
  .md-viewer-section-chevron { transition: color 0.15s ease; }
}
.md-viewer-body h3 {
  font-family: var(--font-display);
  font-size: 1.08rem;
  font-weight: 600;
  margin: var(--pad-m) 0 0.5rem;
  color: var(--ink);
}
.md-viewer-body h4 {
  font-size: 0.74rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
  margin: var(--pad-m) 0 0.5rem;
}
.md-viewer-body p {
  margin: 0 0 var(--pad-s);
  color: var(--ink-soft);
  font-size: 1rem;
}
.md-viewer-body p strong { color: var(--ink); }
.md-viewer-body ul,
.md-viewer-body ol {
  margin: 0 0 var(--pad-s);
  padding-left: 1.5rem;
  color: var(--ink-soft);
}
.md-viewer-body li {
  margin-bottom: 0.45rem;
  font-size: 1rem;
}
.md-viewer-body li strong { color: var(--ink); }
.md-viewer-body blockquote {
  margin: var(--pad-m) 0;
  padding: 0.4rem 0 0.4rem var(--pad-m);
  border-left: 2px solid var(--accent);
  color: var(--ink-soft);
  font-size: 1rem;
}
.md-viewer-body blockquote strong { color: var(--ink); }
.md-viewer-body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.92em;
  padding: 0.1rem 0.35rem;
  background: color-mix(in srgb, var(--ink-muted) 12%, transparent);
  border-radius: 3px;
  color: var(--ink);
}
.md-viewer-body hr {
  margin: var(--pad-l) 0;
  border: 0;
  border-top: 1px solid var(--rule);
}
.md-viewer-body table {
  width: 100%;
  border-collapse: collapse;
  margin: var(--pad-m) 0;
  font-size: 0.92rem;
}
.md-viewer-body th,
.md-viewer-body td {
  text-align: left;
  vertical-align: top;
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid var(--rule);
}
.md-viewer-body th {
  font-weight: 700;
  color: var(--ink);
  border-bottom: 2px solid color-mix(in srgb, var(--rule) 90%, var(--ink));
  font-size: 0.85rem;
}
.md-viewer-body td { color: var(--ink-soft); }
.md-viewer-body em { font-style: italic; }
/* Inline links inherit the kit's :where(a) baseline (accent color,
   subtle underline, theme-aware). No additional rules needed. */

/* Raw markdown view: <pre> with the source text. Same body
   padding so the layout doesn't jump when toggling. Monospace
   font for code-like reading; preserves whitespace and line
   breaks. Selectable so a reader can copy the source. */
.md-viewer-body.is-raw {
  padding-top: var(--pad-m);
}
.md-viewer-raw {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.86rem;
  line-height: 1.55;
  color: var(--ink-soft);
  white-space: pre-wrap;
  word-break: break-word;
  margin: 0;
  padding: 0;
  background: transparent;
}

/* Reduced motion: drop the scale animation, keep opacity. */
@media (prefers-reduced-motion: reduce) {
  .md-viewer { transition: opacity 0.12s ease; }
  .md-viewer-dialog,
  .md-viewer[data-active="true"] .md-viewer-dialog {
    transform: none;
    transition: none;
  }
}

/* Mobile: dialog goes near-fullscreen for readability on
   phones; padding tightens. */
@media (max-width: 720px) {
  .md-viewer { padding: 0; }
  .md-viewer-dialog {
    max-width: 100%;
    max-height: 100vh;
    height: 100vh;
    border-radius: 0;
    border: 0;
  }
  .md-viewer-body { padding: var(--pad-m) var(--pad-m); }
}
