/* ==========================================================================
   concepts.css — the public concept library:
     /concepts        topic shelves in teaching order (views/concepts-index)
     /concepts/:slug  the player page (views/concept-single)

   The library sits on the site's machined-cream INSTRUMENT system
   (instrument.css tokens, per the Claude Design canvas "Concept Player ·
   Instrument"): aluminum-panel ground, the plate as a black-headed
   instrument on the desk, the ACTIVE beat reading in a recessed cream well,
   a tally-mark beatbar. Scene concepts keep The Display's dark bezel — a
   black module on the panel is the house idiom (.in-blackmodule), so the
   flagship object reads native here.

   The two figure skins are NEVER merged (lts-scene.css:10-21): nothing in
   this file touches .iqv-plate internals or .lts-scene internals. This file
   is the room, the reading track and the shelf furniture only.

   Progression grammar: the unit is the BEAT. The beatbar is tally marks and
   a count — no play triangle, no timeline, no timestamps, ever.
   ========================================================================== */

.concepts-page {
   --cx-ground: var(--in-panel, #E9E8E3);       /* aluminum panel */
   --cx-panel: var(--in-panel, #E9E8E3);        /* module faces (with --cx-lift) */
   --cx-panel2: var(--in-panel2, #DFDDD7);
   --cx-edge: var(--in-hairline, #C9C7C0);      /* hairlines */
   --cx-well: var(--in-well, #F8F7F4);          /* recessed reading surfaces */
   --cx-well-edge: var(--in-well-edge, #D6D4CD);
   --cx-ink: var(--in-ink, #232320);
   --cx-ink2: var(--in-ink2, #45443E);
   --cx-ink3: var(--in-ink3, #67665F);
   --cx-ink4: var(--in-ink4, #94938B);
   --cx-accent: var(--in-orange, #E55A12);
   --cx-accent-soft: var(--in-orange-edge, #E5C4A4);
   --cx-walked: var(--in-gr-text, #256E4C);     /* completion green, ink tier */
   --cx-lift: var(--in-lift, 0 1px 0 rgba(255,255,255,.7) inset, 0 10px 24px -16px rgba(35,35,32,.35));
   --cx-inset: var(--in-inset, inset 0 2px 4px rgba(35,35,32,.09));
   /* The pinned walk's vertical budget, as two numbers the whole player reads.
      --cx-head is the sticky site header the stage column must clear (the
      player overwrites it with the measured height - the header is 72.5px
      desktop, 65px mobile, and neither is a number this file should own).
      --cx-read is the strip left below the column for the active beat to read
      in; it is roughly one beat card, and it is the ONLY knob that trades
      reading room against figure size.
      Raised on 2026-09-04 when the scene's housing thinned (scale.ts `bezel`,
      44 -> 16 canvas units): the rim used to be 13% of the figure's height on
      screen and is now 5%, so the strip takes the 8% that was housing and the
      WELL on screen stays the height it was - 40px at 1366x768, 48px at
      1440x900 (measured), which is why the floor rose by 40 and the vh term by
      6: a flat +50 would have shrunk the well on short laptops. Before that it
      was clamp(190px, 21vh, 320px). */
   --cx-head: 72px;
   --cx-read: clamp(230px, 27vh, 370px);
   /* --cx-fig-h is the height left for a SCENE's stage inside the pinned column.
      The player MEASURES it (concept-player.js measure()) and writes the result
      on the player element, which wins over this. This is what the same rules
      read on the FIRST PAINT, before that script has run: the column's cap
      minus its own furniture - 16px of column padding, the figure's two 1px
      borders, the 48px strip on the figure's bottom edge (a fixed height, see
      .concept-figstrip), and the two pixels measure() drops (a floor, and the
      header's half pixel rounding up). Exact at 1440x900; the measured value
      then moves nothing, where the old 46vh guess moved the figure by
      hundreds of pixels. The 240px floor is measure()'s own. */
   --cx-fig-h: max(240px, calc(100vh - var(--cx-head) - var(--cx-read) - 68px));
   --cx-fig-h: max(240px, calc(100svh - var(--cx-head) - var(--cx-read) - 68px));
   background: var(--in-panel, #E9E8E3);
}

main.concepts-single,
main.concepts-index,
main.concepts-sequence {
   background: var(--cx-ground);
   color: var(--cx-ink);
   font-family: var(--in-sans, 'Instrument Sans', sans-serif);
   padding: 44px 20px 80px;
}
main.concepts-single > * {
   max-width: 880px;
   margin-left: auto;
   margin-right: auto;
}
/* Wider than the reading column a single concept gets: the library needs three
   topic cards abreast, and a spine row carries a thumbnail, a name, a line and
   a call to action on one line. */
main.concepts-index > *,
main.concepts-sequence > * {
   max-width: 1080px;
   margin-left: auto;
   margin-right: auto;
}

/* --------------------------------------------------------------- crumbs -- */
.concepts-crumbs {
   font-family: var(--in-mono, monospace);
   font-size: .78rem;
   color: var(--cx-ink3);
   display: flex;
   flex-wrap: wrap;
   align-items: baseline;
   gap: 8px;
   margin-bottom: 26px;
}
/* Without wrap the flex children SHRINK instead, and a long category name
   breaks inside its own crumb - "System Design / API" over "Concepts / Design". */
.concepts-crumbs > * { flex: none; }
.concepts-crumbs a { color: var(--cx-ink2); text-decoration: none; }
.concepts-crumbs a:hover { color: var(--cx-accent); }

/* --------------------------------------------------- the opening statement */
.concept-open { margin-bottom: 34px; }
.concept-kicker {
   font-family: var(--in-mono, monospace);
   font-size: .74rem;
   letter-spacing: .12em;
   text-transform: uppercase;
   color: var(--cx-ink3);
   margin: 0 0 14px;
}
.concept-open h1 {
   font-size: clamp(1.9rem, 4.6vw, 3rem);
   line-height: 1.08;
   letter-spacing: -.015em;
   margin: 0 0 12px;
   color: var(--cx-ink);
}
.concept-thesis {
   font-size: clamp(1.08rem, 2.2vw, 1.3rem);
   line-height: 1.5;
   color: var(--cx-ink2);
   max-width: 32em;
   margin: 0 0 14px;
}
.concept-open-meta {
   font-family: var(--in-mono, monospace);
   font-size: .78rem;
   color: var(--cx-ink3);
   margin: 0;
   display: flex;
   align-items: center;
   gap: 10px;
   flex-wrap: wrap;
}
.concept-walked-chip {
   color: var(--cx-walked);
   border: 1px solid currentColor;
   border-radius: 999px;
   padding: 2px 10px;
   font-size: .72rem;
}

/* ------------------------------------------------------------ the stage -- */
/* The stage pins only when the driver goes live (.is-live) — the no-JS floor
   is a normal document. The walk is STACKED at every width: the figure gets
   the full content berth pinned at the top (wide trees, comparisons and
   sequences need horizontal room far more than height), and the prose reads
   in a centered column beneath it — the same geometry the study-guide
   scrollytelling proved. Tall plates pan INSIDE the pinned region; the
   player nudges the lit stage into view (revealNow). The plate keeps its
   natural internals (never touched); z-index stays under both fullscreen
   paths (.iqv-overlay / .lts-scene.is-fullscreen at 9999).

   .has-js is the same look one step EARLIER. The view stamps it on the player
   with an inline script the moment the element is parsed (walk mode only, see
   views/concept-single.ejs), so the first paint already has the pinned column,
   the sized figure and the walk's beat rhythm. Without it the page painted the
   no-JS floor - full-width figure with its caption list, prose at essay rhythm
   - and re-laid itself out when concept-player.js ran at DOMContentLoaded.
   Every rule that describes the live LOOK matches :is(.is-live, .has-js); the
   rules that need the player's own work (the strip, the active beat) stay on
   .is-live, each with a stand-in under .has-js:not(.is-live). No JS means no
   stamp, and the floor is exactly what it was. */
.concept-stage { position: relative; }
.concept-stage .lts-scene { margin: 0 auto; }

/* The player earns a wider berth than the 880px prose column. */
main.concepts-single > .concept-player:is(.is-live, .has-js) { max-width: 1240px; }
.concept-player:is(.is-live, .has-js) .concept-stagecol {
   position: sticky;
   /* BELOW the header, not under it. The header is sticky at z-index 45 and
      this column at 3, so a `top: 0` pinned the column *behind* it and the
      figure's whole top band - eyebrow, title, first row - was covered. */
   top: var(--cx-head);
   z-index: 3;
   background: var(--cx-ground);
   /* Whatever the viewport has left once the header and the reading strip are
      paid for. The old flat 56vh measured from the top of the WINDOW, so the
      header ate 72px of it and what remained was ~44vh of visible figure. */
   max-height: calc(100vh - var(--cx-head) - var(--cx-read));
   /* svh, not dvh: this region is PINNED, and the figure inside it is sized to
      it by measurement. On dvh a retracting mobile URL bar grows the cap after
      the measurement was taken, and the next one to shrink it crops the figure
      again. The small viewport is the one that always holds. */
   max-height: calc(100svh - var(--cx-head) - var(--cx-read));
   /* hidden, not auto: the wheel over a tall pinned plate must drive the WALK
      (the page), never pan the plate - a manual pan leaves the figure's top
      scrolled out of the pinned region, and revealNow won't heal it while the
      lit rows are still in view. The region is still a scroll container, so
      the player's own scrollTo/scrollTop panning (revealNow) keeps working;
      free exploration lives in fullscreen. */
   overflow-y: hidden;
   padding: 8px 0;
}
.concept-player:is(.is-live, .has-js) .concept-beats { padding: 10vh 0 32vh; }

/* A SCENE has a fixed canvas, so it must FIT the pinned region rather than be
   cropped by it: a 1200x660 scene at the full 1240px column is 683px tall,
   which no laptop viewport can show alongside a reading strip. The player
   measures the region and publishes two customs - --cx-fig-h, the height left
   for the STAGE once the figure's own strip is paid for, and --cx-ar, the
   scene's aspect ratio - and the WIDTH is derived from them here, so the figure
   is as large as the region allows and is always whole. `calc(H * 1200 / 660)`:
   the ratio substitutes as a division, the trick lts-scene.css already uses
   with --lts-ar in fullscreen.
   THE STRIP'S HEIGHT MUST NOT DEPEND ON THE FIGURE'S WIDTH, or this derivation
   feeds itself: chrome decides the figure's width, so anything in the chrome
   that WRAPS when the figure narrows leaves less height, which narrows it
   again. The rail of chapter pills this replaced did exactly that and walked a
   1280x800 window down to the floor in one load. Every cell of the strip is
   nowrap and the chapter title ellipsises, so its height is width-invariant by
   construction and the measuring pass settles in one go.
   PLATES are deliberately excluded - they are tall by nature, they pan inside
   the region (revealNow), and shrinking one to fit would make its code
   unreadable. The 660px floor is the other half of the same guarantee: the viz
   runtime reflows a scene to its phone layout below 640px of container, so a
   figure that could shrink past that would swap its own aspect ratio out from
   under this rule. */
.concept-player:is(.is-live, .has-js)[data-medium="scene"] .iq-visual-figure {
   width: min(100%, max(660px, calc(var(--cx-fig-h, 46vh) * var(--cx-ar, 1.82))));
   margin-inline: auto;
}
/* The backstop, and the reason the region can never be overrun: where the
   column's own width wins (phones, where 100% beats the derived width), the
   stage is capped by HEIGHT instead and the scene letterboxes inside it - the
   svg is width/height 100% over a viewBox, so it meets rather than crops. The
   empty columns take the scene's own bezel colour, exactly as fullscreen does
   with the same trade (lts-scene.css, "ONE surface"). Never in fullscreen:
   there the figure is fixed to the viewport and owns its own geometry. */
.concept-player:is(.is-live, .has-js)[data-medium="scene"] .lts-scene:not(.is-fullscreen) {
   background: var(--sc-bezel, var(--in-panel, #E9E8E3));
}
.concept-player:is(.is-live, .has-js)[data-medium="scene"] .lts-scene:not(.is-fullscreen) .lts-scene-stage {
   max-height: var(--cx-fig-h, none);
}
@media (max-width: 720px) {
   /* Phones read in a shorter strip - the compact canvas is near-square, so
      trading strip for figure buys much less width there. */
   .concepts-page {
      --cx-read: clamp(150px, 18vh, 260px);
      /* The phone strip is two rows (67px, see the phone .concept-figstrip
         rule): 16 + 67 + 2 + 2 = 87px of furniture. */
      --cx-fig-h: max(240px, calc(100vh - var(--cx-head) - var(--cx-read) - 87px));
      --cx-fig-h: max(240px, calc(100svh - var(--cx-head) - var(--cx-read) - 87px));
   }
}

/* --------------------------------------------------- the figure's strip -- */
/* The walk's chrome rides the figure's OWN bottom edge: chapter on the left,
   the transient cue in the middle, beat count and mark on the right. What it
   replaced - chapter pills, tally marks, a licence line and the cue, stacked
   under the figure - cost 103px of the pinned column AND sat below its clamp
   on every viewport, so no reader ever saw it.

   Built by concept-player.js INSIDE .lts-scene, which is why every colour here
   is an --in-* token and never a --cx-*: a custom property resolves where it is
   DECLARED, so --cx-ink3 (declared on .concepts-page) is baked to the page's
   daylight ink and cannot follow the dark figure's re-scoping. --in-* is
   re-declared on .lts-scene[data-scene-theme="dark"] (scene-stage.css, "Chrome
   seam"), so these rules flip with the figure for free.

   Three columns, not a flex row: the cue fades in place when the walk starts
   (opacity, never display) so the chapter and the count never shift under it. */
.concept-figstrip {
   display: grid;
   grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
   align-items: center;
   gap: 14px;
   /* ONE height whatever the cells hold. The tallest cell is the Listen pill,
      and it arrives AFTER measure() has run (the manifest is fetched): before
      this the strip grew 9px when it landed, under a figure already sized to
      the shorter strip. It is also what lets the first-paint stand-in below
      match this box exactly rather than by estimate. Sized to the pill. */
   box-sizing: border-box;
   min-height: 48px;
   padding: 9px 16px 10px;
   border-top: 1px solid var(--in-hairline, #C9C7C0);
   background: var(--in-well, #F8F7F4);
   font-family: var(--in-sans, 'Instrument Sans', system-ui, sans-serif);
   font-size: 13px;
   line-height: 1.4;
   color: var(--in-ink3, #67665F);
}
.cfs-chapter {
   min-width: 0;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
   color: var(--in-ink2, #45443E);
}
.cfs-chapter b {
   font-family: var(--in-mono, 'Fragment Mono', ui-monospace, monospace);
   font-weight: 400;
   color: var(--in-orange, #E55A12);
   margin-right: 9px;
}
.cfs-meta {
   justify-self: end;
   display: flex;
   align-items: baseline;
   gap: 14px;
   white-space: nowrap;
}
.cfs-count {
   font-family: var(--in-mono, 'Fragment Mono', ui-monospace, monospace);
   font-variant-numeric: tabular-nums;
}
.cfs-count b { color: var(--in-ink, #232320); font-weight: 400; }
/* The short form of the licence line the strip replaced - the long one is its
   title attribute, and the page footer carries the year. */
.cfs-mark {
   font-family: var(--in-mono, 'Fragment Mono', ui-monospace, monospace);
   font-size: 11px;
   opacity: .62;
}

/* A plate keeps its strip OUTSIDE its card: .iqv-plate carries a CSS zoom and
   its own padding, and host chrome inside it would inherit both. Sticky to the
   bottom of the pinned region instead, so a tall plate panning behind it
   (revealNow) never carries the count out of view. */
.concept-figstrip.is-detached {
   position: sticky;
   bottom: 0;
   border-top: 0;
   background: var(--cx-ground);
   padding: 10px 2px 0;
   /* Not the fixed 48px: a plate derives nothing from this strip's height (it
      pans, it is never sized to the region), and 20px more ground under it
      would only eat into the plate's visible area. */
   min-height: 0;
}

/* The scroll cue: the walk's one affordance hint, and in the stepped tier its
   only instruction. Fades once the reader has moved, but keeps its cell. */
.concept-cue {
   justify-self: center;
   text-align: center;
   font-family: var(--in-mono, 'Fragment Mono', ui-monospace, monospace);
   font-size: 11.5px;
   white-space: nowrap;
   transition: opacity .6s ease;
}
.concept-player.is-started .concept-cue { opacity: 0; }

/* The no-walk floor's licence line - the strip carries it once the player is
   live (see views/concept-single.ejs). A scene's stand-in strip (below) takes
   its place from the first paint; a plate's strip is the detached one under
   the card with no stand-in, so there the licence line - about the same
   height, and always the thing that strip replaced - stays until the player
   builds it. */
.concepts-single .iq-visual-license {
   color: var(--cx-ink3);
   font-size: .72rem;
   margin: 8px 0 0;
   text-align: center;
}
.concept-player.is-live .iq-visual-license,
.concept-player.has-js[data-medium="scene"] .iq-visual-license { display: none; }

/* The scene's own caption list - every step's text, the figure's whole content
   when JavaScript never comes - never shows under the walk: the beats ARE the
   narration. lts-scene.css hides the list only once the runtime has MOUNTED,
   which is a chunk fetch after the page has painted; left to that, a reader got
   the poster with the whole list stacked under it inside the dark figure for a
   second, and measure() sized the stage against that stack (it fell to its
   240px floor, in a figure a third of its final width). Gated on the walk being
   driven, so the no-JS floor keeps the list. */
.concept-player:is(.is-live, .has-js) .lts-scene-caption { display: none; }

/* The strip's stand-in between the first paint and the player building the
   real one (concept-player.js buildStrip, at DOMContentLoaded): the same box
   with nothing in it, so the figure's bottom edge is already where it will
   stay and the column's height - which the reading band is measured from -
   never moves. The same fixed box as .concept-figstrip, so the two match by
   construction. Scenes only; a plate's strip sits outside its card. */
.concept-player.has-js:not(.is-live)[data-medium="scene"] .lts-scene::after {
   content: '';
   display: block;
   box-sizing: border-box;
   min-height: 48px;
   padding: 9px 16px 10px;
   border-top: 1px solid var(--in-hairline, #C9C7C0);
   background: var(--in-well, #F8F7F4);
}

.concept-chapter-pane[hidden] { display: none !important; }

/* Sequence plates emit their stepper chrome BARE (not inside the [hidden]
   reveal wrapper). Under the player's external-chrome scope the beats ARE
   the chrome, so the native strip hides here — and reappears automatically
   in fullscreen, where the overlay moves the plate out of this scope. */
[data-iqv-external-chrome] .iqv-narr,
[data-iqv-external-chrome] .iqv-stepbar { display: none; }

/* ------------------------------------------------------------- the walk -- */
.concept-beats { padding: 26px 0 30px; }
/* The measure: 38em at 1.2rem is about 770px and 73 characters a line - the
   wide end of the 45-75 reading range, chosen so the card reads as a
   deliberate wide column under a 1240px stage rather than a strip (it was
   34em/1.13rem, ~650px). Not the stage's width: at 1240px a line runs 140
   characters, which readers lose on the way back to the margin, and a beat
   collapses to two or three lines the band can no longer tell apart. */
.concept-beat {
   max-width: 38em;
   margin: 0 auto;
   padding: 11vh 20px;
   font-size: 1.2rem;
   line-height: 1.72;
   color: var(--cx-ink);
   /* Same box on every beat so the active well below never shifts layout. */
   border: 1px solid transparent;
   border-radius: 9px;
}
.concept-beat p { margin: 0 0 1em; }
.concept-beat p:last-child { margin-bottom: 0; }
/* Dimming is gated on .is-live so essay/bare/no-JS floors read at full
   strength; .42 keeps the neighbours legible as context, not veiled. The
   ACTIVE beat reads in a recessed cream well — the Instrument idiom for
   "the surface you are working on". */
.concept-player:is(.is-live, .has-js) .concept-beat {
   opacity: .42;
   transition: opacity .25s ease, background .25s ease;
   /* Live rhythm: the well hugs the prose; the breathing room between beats
      moves to margins so the recess reads as a card, not a corridor. */
   padding-top: 18px;
   padding-bottom: 18px;
   margin-top: 9vh;
   margin-bottom: 9vh;
}
/* Before the player lights a beat itself (activate(0), at DOMContentLoaded)
   the first one is lit for it - the same beat, so nothing flickers. */
.concept-player:is(.is-live, .has-js) .concept-beat.is-active,
.concept-player.has-js:not(.is-live) .concept-beats > .concept-beat:first-child {
   opacity: 1;
   background: var(--cx-well);
   border-color: var(--cx-well-edge);
   box-shadow: var(--cx-inset);
}
/* Essay and bare modes: normal essay rhythm, no scroll theatre. */
.concept-player:not(.is-live):not(.has-js) .concept-beat { padding: 0 0 1.6em; }

/* ----------------------------------------------------------- the payoff -- */
/* The quiet ledger: primary questions are numbered and keep their captions;
   supporting ones compress to one title row each (their captions stay in the
   admin - on the page nine identical-weight blocks read as a wall). */
.concept-reach {
   border-top: 1px solid var(--cx-edge);
   margin-top: 30px;
   padding-top: 34px;
}
.concept-reach-kicker {
   font-family: var(--in-mono, monospace);
   font-size: .74rem;
   letter-spacing: .12em;
   text-transform: uppercase;
   color: var(--cx-ink3);
   margin: 0 0 8px;
}
.concept-reach h2 {
   font-size: 1.3rem;
   margin: 0 0 6px;
   color: var(--cx-ink);
}
.concept-reach-sub {
   font-size: 1.02rem;
   margin: 28px 0 4px;
   color: var(--cx-ink2);
}
.concept-question-list {
   list-style: none;
   margin: 0;
   padding: 0;
}
.concept-question-list li {
   padding: 14px 0;
   border-bottom: 1px solid var(--cx-edge);
}
.concept-question-list a {
   color: var(--cx-ink);
   font-weight: 600;
   text-decoration: none;
}
.concept-question-list a:hover { color: var(--cx-accent); }
.concept-question-primary { display: flex; gap: 14px; }
.concept-qnum {
   font-family: var(--in-mono, monospace);
   font-size: .82rem;
   color: var(--cx-accent);
   padding-top: 2px;
}
.concept-question-main { flex: 1 1 auto; min-width: 0; }
/* Supporting rows: single line, lighter rule, lighter link weight. */
.concept-question-compact li {
   padding: 11px 0;
   border-bottom: 1px solid var(--cx-panel2);
}
.concept-question-compact a {
   font-weight: 500;
   font-size: .94rem;
}
.concept-reach-more {
   font-family: var(--in-mono, monospace);
   font-size: .78rem;
   color: var(--cx-ink3);
   margin: 14px 0 0;
}

/* --------------------------------------------------------- teaching order */
/* Full-width bands, Next first: a lone Next card in a half-width grid column
   looked stranded whenever there was no Previous. */
.concept-seq {
   display: flex;
   flex-direction: column;
   gap: 14px;
   margin-top: 36px;
}
.concept-seq-band {
   display: flex;
   align-items: center;
   justify-content: space-between;
   gap: 24px;
   background: var(--cx-panel);
   border: 1px solid var(--cx-edge);
   border-radius: 10px;
   box-shadow: var(--cx-lift);
   padding: 16px 20px;
   text-decoration: none;
   color: var(--cx-ink);
}
.concept-seq-band:hover { border-color: var(--cx-accent-soft); }
.concept-seq-info { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.concept-seq-cta {
   font-size: .9rem;
   font-weight: 600;
   color: var(--cx-accent);
   white-space: nowrap;
}
.concept-seq-dir {
   display: block;
   font-family: var(--in-mono, monospace);
   font-size: .72rem;
   color: var(--cx-ink3);
   margin-bottom: 6px;
}
.concept-seq-band strong { font-size: 1rem; }
.concept-seq-line {
   display: block;
   color: var(--cx-ink2);
   font-size: .85rem;
   margin-top: 4px;
}

/* ---------------------------------------------------------- related rail -- */
.concept-related { margin-top: 34px; }
.concept-related h2 { font-size: 1.1rem; margin: 0 0 12px; }
.concept-related-grid {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
   gap: 12px;
}
.concept-related-card {
   display: block;
   background: var(--cx-panel);
   border: 1px solid var(--cx-edge);
   border-radius: 10px;
   box-shadow: var(--cx-lift);
   padding: 13px 15px;
   text-decoration: none;
   color: var(--cx-ink);
}
.concept-related-card:hover { border-color: var(--cx-accent-soft); }
.concept-related-line {
   display: block;
   color: var(--cx-ink2);
   font-size: .85rem;
   margin-top: 5px;
}
.concept-related-meta {
   display: block;
   font-family: var(--in-mono, monospace);
   font-size: .72rem;
   color: var(--cx-ink3);
   margin-top: 8px;
}

.concepts-back { margin-top: 38px; }
.concepts-back a {
   color: var(--cx-ink2);
   text-decoration: none;
   font-size: .9rem;
}
.concepts-back a:hover { color: var(--cx-accent); }

/* ==========================================================================
   The LIBRARY (/concepts, /concepts/category/:id): hero, resume, category
   pills, and a grid of TOPIC cards. It lists sequences, never ideas - the
   corpus is heading for hundreds of concepts across 449 topics, and a page
   that lists every one of them stops working long before that.
   ========================================================================== */
.concepts-hero { padding: 6px 0 4px; }
.concepts-hero .concepts-kicker {
   font-family: var(--in-mono, monospace);
   font-size: .74rem;
   letter-spacing: .12em;
   text-transform: uppercase;
   color: var(--cx-ink3);
   margin: 0 0 12px;
}
.concepts-hero h1 {
   font-size: clamp(1.8rem, 4.2vw, 2.7rem);
   letter-spacing: -.015em;
   line-height: 1.1;
   margin: 0 0 10px;
}
.concepts-hero-sub {
   color: var(--cx-ink2);
   max-width: 38em;
   line-height: 1.55;
   margin: 0 0 10px;
}
.concepts-hero-meta {
   font-family: var(--in-mono, monospace);
   font-size: .78rem;
   color: var(--cx-ink3);
   margin: 0;
   display: flex;
   gap: 14px;
   flex-wrap: wrap;
}

/* Shared progress bar. Width is set by concepts-progress.js, never by the
   server - completion must not enter cached HTML. */
.concepts-bar {
   display: block;
   height: 4px;
   border-radius: 999px;
   background: var(--cx-panel2);
   overflow: hidden;
}
.concepts-bar > span {
   display: block;
   width: 0;
   height: 100%;
   border-radius: 999px;
   background: var(--cx-walked);
}

/* ------------------------------------------------------------- resume ---- */
/* The reason to come back: a half-finished sequence. Hidden until the client
   finds one, so it never renders as an empty promise. */
.concepts-resume {
   display: flex;
   align-items: center;
   gap: 22px;
   margin-top: 24px;
   padding: 16px 20px;
   background: var(--cx-well);
   border: 1px solid var(--cx-well-edge);
   border-radius: 12px;
   box-shadow: var(--cx-inset);
   text-decoration: none;
   color: var(--cx-ink);
}
.concepts-resume[hidden] { display: none; }
.concepts-resume:hover { border-color: var(--cx-accent-soft); }
.concepts-resume-body { display: flex; flex-direction: column; gap: 5px; min-width: 0; flex: 1 1 auto; }
.concepts-resume-kicker {
   font-family: var(--in-mono, monospace);
   font-size: .72rem;
   color: var(--cx-ink3);
}
.concepts-resume-where { font-size: 1.05rem; font-weight: 600; }
.concepts-resume-name { color: var(--cx-ink2); font-size: .88rem; }
.concepts-resume-go { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; flex: none; }
.concepts-resume-go .concepts-bar { width: 150px; }
.concepts-resume-cta { font-size: .9rem; font-weight: 600; color: var(--cx-accent); white-space: nowrap; }

/* --------------------------------------------------------- category strip */
/* Pills are anchors to real category pages (the Learn-sibling idiom), so this
   strip needs no JS and no active-state syncing - the server renders it. */
.concepts-strip {
   display: flex;
   align-items: center;
   gap: 12px;
   flex-wrap: wrap;
   margin-top: 26px;
   padding-bottom: 16px;
   border-bottom: 1px solid var(--cx-edge);
}
.concepts-strip-label {
   font-family: var(--in-mono, monospace);
   font-size: .72rem;
   letter-spacing: .12em;
   text-transform: uppercase;
   color: var(--cx-ink4);
}
.concepts-strip-row { display: flex; gap: 8px; flex-wrap: wrap; }
.concepts-pill {
   display: inline-flex;
   align-items: center;
   gap: 7px;
   border: 1px solid var(--cx-edge);
   border-radius: 999px;
   background: var(--cx-panel);
   box-shadow: var(--cx-lift);
   padding: 6px 14px;
   font-size: .88rem;
   color: var(--cx-ink2);
   text-decoration: none;
}
.concepts-pill:hover { border-color: var(--cx-accent-soft); color: var(--cx-ink); }
.concepts-pill.is-active {
   background: var(--cx-well);
   border-color: var(--cx-accent-soft);
   box-shadow: var(--cx-inset);
   color: var(--cx-ink);
   font-weight: 600;
}
.concepts-pill-count {
   font-family: var(--in-mono, monospace);
   font-size: .72rem;
   color: var(--cx-ink4);
}

/* ------------------------------------------------------------ topic cards */
.concepts-group { margin-top: 40px; }
/* No category header to sit under: the first row carries its own space. */
.concepts-group.is-bare { margin-top: 26px; }
.concepts-group-name {
   font-family: var(--in-mono, monospace);
   font-size: .74rem;
   font-weight: 400;
   letter-spacing: .12em;
   text-transform: uppercase;
   color: var(--cx-ink3);
   margin: 0;
   padding-bottom: 8px;
   border-bottom: 1px solid var(--cx-edge);
   display: flex;
   align-items: baseline;
   gap: 12px;
   flex-wrap: wrap;
}
.concepts-group-meta { color: var(--cx-ink4); font-size: .7rem; }
.concepts-topic-grid {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(288px, 1fr));
   align-items: start;
   gap: 14px;
   margin-top: 16px;
}
.topic-card {
   display: flex;
   flex-direction: column;
   gap: 12px;
   background: var(--cx-panel);
   border: 1px solid var(--cx-edge);
   border-radius: 14px;
   box-shadow: var(--cx-lift);
   padding: 15px 16px 14px;
   text-decoration: none;
   color: var(--cx-ink);
   transition: border-color .15s ease, transform .15s ease;
}
.topic-card:hover { border-color: var(--cx-accent-soft); transform: translateY(-2px); }
.topic-card-head { display: flex; align-items: center; gap: 11px; }
.topic-card-mark {
   width: 30px;
   height: 30px;
   flex: none;
   border-radius: 8px;
   background: var(--cx-accent);
   color: #fff;
   display: flex;
   align-items: center;
   justify-content: center;
}
.topic-card-mark svg { width: 17px; height: 17px; }
/* The finished state changes the MARK, not just a chip: it is the only status
   on the card that needs no reading. */
.topic-card-tick { display: none; }
.topic-card.is-complete .topic-card-mark { background: var(--cx-walked); }
.topic-card.is-complete .topic-card-mark svg:not(.topic-card-tick) { display: none; }
.topic-card.is-complete .topic-card-tick { display: block; }
.topic-card-title { min-width: 0; }
.topic-card-title b { display: block; font-size: 1.02rem; line-height: 1.25; font-weight: 600; }
.topic-card-n {
   display: block;
   font-family: var(--in-mono, monospace);
   font-size: .72rem;
   color: var(--cx-ink3);
   margin-top: 3px;
}
/* Every thumb is a 1200x630 share card - title row on top, figure below - so
   a tile holds that ratio instead of a fixed height. A fixed 56px was a
   near-whole poster while three tiles split the row (~102px wide each) and a
   6:1 band sliced through the title once one figure had the row to itself.
   Row height now follows the count (three ~53px, one = the whole poster), so
   the grid top-aligns its cards: a short card ends at its own bar rather than
   stretching to a tall neighbour and hollowing out. */
.topic-card-fan { display: flex; gap: 6px; }
.topic-card-fan img {
   flex: 1 1 0;
   min-width: 0;
   width: 100%;
   height: auto;
   aspect-ratio: 1200 / 630;
   object-fit: cover;
   border-radius: 7px;
   border: 1px solid var(--cx-well-edge);
   background: var(--cx-well);
}
.topic-card-foot {
   display: flex;
   align-items: center;
   gap: 10px;
   font-family: var(--in-mono, monospace);
   font-size: .72rem;
   color: var(--cx-ink4);
}
.topic-card-foot .concepts-bar { flex: 1 1 auto; }
.topic-card-state.is-walked { color: var(--cx-walked); }

/* ==========================================================================
   ONE SEQUENCE (/concepts/topic/:id) — drawn as a spine, because a grid of
   equal cards cannot show order and order is the whole product here.
   ========================================================================== */
.seq-head {
   display: flex;
   align-items: center;
   gap: 16px;
   padding-bottom: 20px;
   border-bottom: 1px solid var(--cx-edge);
}
.seq-mark {
   width: 44px;
   height: 44px;
   flex: none;
   border-radius: 11px;
   background: var(--cx-accent);
   color: #fff;
   display: flex;
   align-items: center;
   justify-content: center;
}
.seq-mark svg { width: 22px; height: 22px; }
.seq-head-text { min-width: 0; flex: 1 1 auto; }
.seq-head h1 { font-size: clamp(1.5rem, 3.4vw, 2.1rem); letter-spacing: -.015em; margin: 0; }
.seq-head-meta {
   font-family: var(--in-mono, monospace);
   font-size: .76rem;
   color: var(--cx-ink3);
   margin: 6px 0 0;
   display: flex;
   gap: 14px;
   flex-wrap: wrap;
}
.seq-walked { color: var(--cx-walked); }
.seq-walked[hidden] { display: none; }
.seq-head-cta { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 12px; }
.seq-head-bar { width: 170px; }
.seq-head-bar[hidden] { display: none; }
.seq-start {
   background: var(--cx-ink);
   color: var(--cx-well);
   border-radius: 9px;
   padding: 11px 19px;
   font-size: .9rem;
   font-weight: 600;
   text-decoration: none;
   white-space: nowrap;
}
.seq-start:hover { background: var(--cx-ink2); color: var(--cx-well); }

.seq-spine {
   position: relative;
   list-style: none;
   margin: 26px 0 0;
   padding: 0 0 0 52px;
}
/* The rule is drawn PER STEP, in two halves, so it needs no measurement: the
   node's centre is a fixed offset in this stylesheet, and a run between two
   nodes is coloured by whether the step ABOVE it is done. A single measured
   fill (the first cut) went stale the moment anything reflowed - a window
   resize or a late webfont swap left the green stopping 66px short of its
   node, with nothing to recompute it. */
.seq-step { position: relative; margin-bottom: 11px; }
.seq-step::before,
.seq-step::after {
   content: "";
   position: absolute;
   left: -37px;          /* node centre: left -52px + half of 32px */
   width: 2px;
   background: var(--cx-panel2);
}
.seq-step::before { top: 0; height: 38px; }               /* step top -> node centre */
.seq-step::after { top: 38px; bottom: -11px; }            /* node centre -> next step */
/* The rule starts at the first node and ends at the last - no stubs. */
.seq-step:first-child::before { display: none; }
.seq-step:last-child::after { display: none; }
/* A walked step colours the run below its node - its own lower half plus the
   next step's upper half - so the green covers exactly the walked prefix...
   ...except below the LAST walked node, where nothing beyond it is walked. The
   client tags that one step rather than measuring anything, so the rule still
   survives a resize or a late webfont swap. */
.seq-step.is-done::after { background: var(--cx-walked); }
.seq-step.is-done + .seq-step::before { background: var(--cx-walked); }
.seq-step.is-last-done::after { background: var(--cx-panel2); }
.seq-step.is-last-done + .seq-step::before { background: var(--cx-panel2); }
.seq-node {
   position: absolute;
   left: -52px;
   top: 22px;
   width: 32px;
   height: 32px;
   border-radius: 999px;
   background: var(--cx-panel);
   border: 2px solid var(--cx-panel2);
   display: flex;
   align-items: center;
   justify-content: center;
   font-family: var(--in-mono, monospace);
   font-size: .78rem;
   color: var(--cx-ink3);
}
.seq-node-tick { display: none; width: 15px; height: 15px; }
.seq-step.is-done .seq-node {
   background: var(--cx-walked);
   border-color: var(--cx-walked);
   color: #fff;
}
.seq-step.is-done .seq-node-n { display: none; }
.seq-step.is-done .seq-node-tick { display: block; }
.seq-step.is-live .seq-node { border-color: var(--cx-accent); color: var(--cx-accent); background: var(--cx-well); }

.seq-card {
   display: flex;
   align-items: center;
   gap: 16px;
   padding: 13px 15px;
   background: var(--cx-panel);
   border: 1px solid var(--cx-edge);
   border-radius: 12px;
   box-shadow: var(--cx-lift);
   text-decoration: none;
   color: var(--cx-ink);
}
.seq-card:hover { border-color: var(--cx-accent-soft); }
.seq-step.is-live .seq-card {
   background: var(--cx-well);
   border-color: var(--cx-accent-soft);
   box-shadow: var(--cx-inset);
}
.seq-thumb {
   width: 132px;
   height: 74px;
   flex: none;
   object-fit: cover;
   border-radius: 8px;
   border: 1px solid var(--cx-well-edge);
   background: var(--cx-well);
}
.seq-body { min-width: 0; flex: 1 1 auto; }
.seq-name { display: block; font-size: 1rem; line-height: 1.3; font-weight: 600; }
.seq-line { display: block; color: var(--cx-ink2); font-size: .86rem; line-height: 1.5; margin-top: 5px; }
.seq-meta {
   display: flex;
   gap: 12px;
   flex-wrap: wrap;
   font-family: var(--in-mono, monospace);
   font-size: .7rem;
   color: var(--cx-ink3);
   margin-top: 9px;
}
.seq-go {
   flex: none;
   align-self: center;
   font-size: .86rem;
   font-weight: 600;
   color: var(--cx-accent);
   white-space: nowrap;
}

/* ----------------------------------------------------------------- floors */
@media (max-width: 720px) {
   main.concepts-single, main.concepts-index, main.concepts-sequence { padding: 30px 14px 60px; }
   .concept-beat { font-size: 1.04rem; padding: 9vh 0; }
   /* The strip has no room for three things at 390px: the chapter and the
      count are the two that carry state, so the mark drops (the stage keeps
      its own imprint) and the cue drops with it - scrolling is not a gesture a
      phone reader needs taught. The STEPPED tier is the exception: there the
      cue is the only instruction, so it takes a second row, permanently, and
      the geometry stays still when it fades. */
   .concept-figstrip {
      grid-template-columns: minmax(0, 1fr) auto;
      gap: 4px 10px;
      padding: 8px 12px 9px;
      font-size: 12.5px;
      /* Two rows in two columns: chapter and cue-cell on the first, the count
         on the second. Fixed at the voiced height (a 27.5px Listen pill on row
         one) for the same two reasons as the 48px desktop floor - the pill
         lands after measure(), and the first-paint stand-in below copies this
         box. The stepped tier's cue row is taller still and simply exceeds it. */
      min-height: 67px;
   }
   .concept-player.has-js:not(.is-live)[data-medium="scene"] .lts-scene::after {
      min-height: 67px;
      padding: 8px 12px 9px;
   }
   .cfs-mark { display: none; }
   .concept-cue { display: none; }
   .concept-player.is-stepped .concept-cue {
      display: block;
      grid-column: 1 / -1;
      grid-row: 2;
      justify-self: start;
      text-align: left;
   }
   .concept-seq-band { flex-direction: column; align-items: flex-start; gap: 8px; }
   /* The library: one card per row, and the resume block stacks so its bar and
      CTA keep full-width thumb targets. */
   .concepts-topic-grid { grid-template-columns: minmax(0, 1fr); }
   .concepts-resume { flex-direction: column; align-items: stretch; gap: 14px; }
   .concepts-resume-go { align-items: stretch; }
   .concepts-resume-go .concepts-bar { width: auto; }
   /* The spine keeps its rule on mobile but pulls in tight, and each step
      becomes thumb-over-text so the name never squeezes to two words. */
   .seq-head { flex-wrap: wrap; }
   .seq-head-cta { width: 100%; align-items: stretch; }
   .seq-head-bar { width: auto; }
   .seq-start { text-align: center; }
   .seq-spine { padding-left: 34px; }
   /* Node centre moves with the smaller node: left -34px + half of 22px. */
   .seq-step::before, .seq-step::after { left: -24px; }
   .seq-step::before { height: 25px; }
   .seq-step::after { top: 25px; }
   .seq-node { left: -34px; top: 14px; width: 22px; height: 22px; font-size: .68rem; }
   .seq-node-tick { width: 12px; height: 12px; }
   .seq-card { flex-wrap: wrap; gap: 12px; padding: 12px; }
   .seq-thumb { width: 100%; height: auto; aspect-ratio: 1200 / 630; }
   .seq-go { align-self: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
   .concept-card, .concept-beat, .concept-cue {
      transition: none !important;
   }
   /* Reduced motion gets the CLICK-STEPPED walk (.is-stepped): every state
      change is reader-initiated and instant. The iqv veil/step transitions
      only ever fire here (question pages never arm under reduced motion),
      so they are silenced in this scope. */
   [data-iqv-external-chrome] [data-iqv-stage],
   [data-iqv-external-chrome] .iqv-msg,
   [data-iqv-external-chrome] .iqv-veiled {
      transition: none !important;
   }
}

/* ─── Listening ─────────────────────────────────────────────────────────────
   The walk read aloud. The control lives in the figure's own bottom strip
   beside the cue, and it is a PLAY/PAUSE and nothing else: this page's rule is
   that progression is by beat, never by clock, so there is no scrubber and no
   timestamp - the "n / 20" already in the strip is the position indicator.
   Everything here inherits the strip's --in-* tokens, which .lts-scene
   re-scopes for the dark stage, so one rule set serves both skins. */
.cfs-cuewrap {
   display: flex;
   align-items: center;
   gap: 12px;
   min-width: 0;
}
.concept-listen {
   display: inline-flex;
   align-items: center;
   gap: 7px;
   flex: none;
   padding: 4px 11px 4px 9px;
   border: 1px solid var(--in-hairline, #C9C7C0);
   border-radius: 999px;
   background: var(--in-paper, #FFFDF8);
   color: var(--in-ink2, #45443E);
   font: inherit;
   font-size: 12.5px;
   cursor: pointer;
   transition: border-color .15s ease, color .15s ease;
}
/* display:inline-flex above outranks the UA / Tailwind-preflight [hidden]
   rule (same specificity, this sheet loads later), so the player's
   `listen.hidden = true` did nothing and every walk showed a dead Play
   button until the manifest said otherwise. Restated with the attribute
   so hidden means hidden. */
.concept-listen[hidden] { display: none; }
.concept-listen:hover { border-color: var(--in-orange, #E55A12); color: var(--in-orange, #E55A12); }
.concept-listen:focus-visible { outline: 2px solid var(--in-orange, #E55A12); outline-offset: 2px; }
.concept-listen[aria-pressed="true"] { border-color: var(--in-orange, #E55A12); color: var(--in-orange, #E55A12); }
.cl-glyph { font-size: 10px; line-height: 1; }
.cl-total { opacity: .65; font-variant-numeric: tabular-nums; }

/* The spoken sentence, then the spoken word. Sentence-first is deliberate:
   it survives a transcript that heard something slightly different, so the
   reader never loses their place even where a single word drifts. */
.concept-beat .cl-w { transition: color .12s ease; }
.concept-beat .cl-w.is-said {
   /* Derived from the real accent rather than a hardcoded cream tint, so the
      wash follows whichever skin re-scopes --in-orange. The rgba line is the
      fallback for engines without color-mix, and is the light-column value. */
   background: rgba(229, 90, 18, .07);
   background: color-mix(in srgb, var(--in-orange, #E55A12) 9%, transparent);
}
.concept-beat .cl-w.is-saying { color: var(--in-orange, #E55A12); }

@media (prefers-reduced-motion: reduce) {
   .concept-listen, .concept-beat .cl-w { transition: none; }
}
