/* =============================================================================
 * kc-lesson-beats.css — one beat per screen, and the motion that carries it
 * =============================================================================
 *
 * WHAT THIS FILE IS FOR
 *
 * A concept step is an ARGUMENT built in four to eight beats. The runner used
 * to render `(p.beats || []).slice(0, reveal)` into one growing <ol>, so every
 * press of "Reveal the next part" re-listed everything already read and the
 * page got longer underneath the member. Measured signed in against the local
 * stack on `les_muscle_strength / s1_organ` (6 beats, 1280x900): the card grew
 * 340 -> 966px, the document 900 -> 1461px, and the primary button travelled
 * 685 -> 1309px — away from the pointer about to press it.
 *
 * `_renderConcept` now emits ONE beat. That fixes the pile. It also takes
 * something away: with the earlier paragraphs gone, nothing on screen says they
 * still count. So this file is not a page transition. It is six gestures, and
 * five of them exist to say KEPT, NOT DISCARDED.
 *
 * -----------------------------------------------------------------------------
 * THE THREE LAWS
 * -----------------------------------------------------------------------------
 *
 * 1. WHAT DID NOT CHANGE DOES NOT MOVE. The eyebrow, the h2 and the key terms
 *    are constant across every screen of a step and they are the member's
 *    anchor that this is still one explanation. Only what sits under
 *    `[data-enter]` animates, and `_renderConcept` writes `data-enter` ONLY on
 *    a render where the beat itself changed. `_setView` removes every child
 *    node and re-inserts fresh HTML on every render (kc-lesson-v2.js), so
 *    element identity never survives — a transition between beats is
 *    impossible and the entrance has to be a mount-fired @keyframes. Without
 *    Law 1 an unrelated re-render would replay the entrance of a paragraph
 *    the member is already reading.
 *
 * 2. THE TEXT LANDS FIRST; NOTHING SECONDARY MOVES UNTIL IT IS STILL.
 *    Emphasis arriving WITH a sentence competes with reading it. Every
 *    secondary gesture — the end cap, the takeaway's rule, the prediction ring
 *    — is delayed by `--kc-b-read`, which is `calc(var(--motion-slow) * 2)`:
 *    400ms in `focused`, 640ms in `vivid`, 0ms in `clear`.
 *
 * 3. EVERY DURATION AND DELAY IS A MULTIPLE OF THE REGISTER'S OWN TOKENS.
 *    Not one raw millisecond appears below. `--motion-fast/base/slow/step` are
 *    all `0ms !important` in `clear` (kc-theme.css:647-649) and under
 *    `prefers-reduced-motion: reduce` (kc-theme.css:2085-2088), so the whole
 *    vocabulary collapses to nothing by ARITHMETIC rather than by an override
 *    someone could forget. There is no "faster version" of any gesture here.
 *
 * -----------------------------------------------------------------------------
 * THE STATIC-EQUIVALENT DISCIPLINE, AND THE BASE-STATE INVARIANT
 * -----------------------------------------------------------------------------
 *
 * `clear` and `prefers-reduced-motion` are the SAME mechanism, which is the one
 * thing worth knowing before writing a line of this file. Both set
 * `animation: none !important` on every descendant of `[data-presentation]` —
 * `clear` at kc-theme.css:2001-2010, `reduce` at kc-theme.css:2090-2097. They
 * do not slow an animation and they do not shorten it; they DELETE it. An
 * element whose resting appearance lives only in a keyframe therefore
 * disappears in the high-legibility register and for any member who asked for
 * less motion. So:
 *
 *     EVERY @keyframes BELOW DEPARTS FROM THE BASE STATE AND RETURNS TO IT.
 *     `from` is the offset; `to` is `transform: none` or absent.
 *
 * That single rule makes all three degradations correct at once: `animation:
 * none` leaves the element at its finished appearance, a 0ms duration lands on
 * it on the first frame, and a full run travels to it. It is checkable by
 * reading — every `to` below is `transform: none`, `opacity: 0` for the ghost,
 * or absent.
 *
 * Where a gesture carried information, the static case carries it ANOTHER WAY,
 * never by running the same animation faster:
 *
 *   G1 CARRY   which part this is       -> the rail, and "Part 3 of 5" in words
 *   G2 THREAD  one continuous argument  -> the accent rule, simply drawn
 *   G3 FILE    the last beat was kept   -> the pip it folded into, permanent
 *   G4 KEEP    that part is now filed   -> pip fill AND pip thickness
 *   G5 MARK    this is the close        -> the end cap and the heavy rule, drawn
 *   G6 COUNT   the number advanced      -> it is a number; it is text
 *
 * G3 is the only gesture with no static form, and it is the only one whose
 * meaning is fully carried by something else already on screen (G4's pip). It
 * is therefore NOT RENDERED AT ALL in `clear` or under `reduce` — absent, not
 * shortened.
 *
 * COLOUR IS NEVER ALONE. The rail's three pip states differ in FILL and in
 * THICKNESS, and `.kc-v2-beat-count` says the same thing in words. Switch the
 * hue channel off and the rail is still readable.
 *
 * NO PARTICLES, IN ANY REGISTER. `vivid`'s larger motion budget is spent on
 * amplitude and stagger (`--kc-b-rise` 1.1em against .8em, `--motion-step` 40ms
 * against 0ms), never on decorative objects. Every gesture states a fact about
 * the argument, which is why every one of them has a static form.
 *
 * NO NAGGING MECHANICS. The rail is progress WITHIN one explanation. It resets
 * at every step and cannot be carried between sessions. It is not a streak, not
 * a daily goal and not a cap, and nothing here introduces one.
 *
 * -----------------------------------------------------------------------------
 * SCOPE AND LOAD ORDER
 * -----------------------------------------------------------------------------
 *
 *   SCOPE. Every selector is `.kc-v2-beatline` or a descendant of it, or
 *   `.kc-v2-runner[data-beat-screen]`. The video step's `.kc-v2-fallback-beats`
 *   also emits `.kc-v2-beat` and is not inside a beatline, so it is untouched
 *   by construction. Nothing is scoped to `.kc-v2-route`, deliberately — see
 *   BOTH SURFACES below.
 *
 *   ORDER. Load AFTER kc-lesson-uplift.css, which styles `.kc-v2-route
 *   .kc-v2-beat` at (0,2,0). `.kc-v2-beatline .kc-v2-beat` is also (0,2,0) and
 *   wins on source order alone. In center.html that is one line, immediately
 *   after `/kc-lesson-uplift.css`:
 *
 *       <link rel="stylesheet" href="/kc-lesson-beats.css">
 *
 *   BOTH SURFACES. kc-boot.js redirects every viewport <= 767px wide to /m,
 *   and m.html mounts the same <kc-lesson-runner> while loading only
 *   kc-theme.css and kc-lesson-v2.css — NOT kc-uplift.css. The
 *   `--acc / --ink1 / --ink2 / --ink3 / --track / --plate / --rule / --inset`
 *   vocabulary does not exist there at all (verified: 0 declarations in
 *   kc-theme.css). So EVERY colour below is written
 *   `var(--uplift-token, var(--theme-token))`. Both resolve, neither is a
 *   literal, and no colour is declared in this file.
 *
 *   The runner also appends this stylesheet itself if the host page has not
 *   linked it (`ensureBeatStyles()` in kc-lesson-v2.js). That is not belt and
 *   braces: `.kc-v2-beat-spacer` is an ordinary <p> until section 2 hides it,
 *   so a page that mounted the runner without this file would render every
 *   beat at once — the exact defect the change removes.
 * ========================================================================== */


/* =============================================================================
 * 1. CONSTANTS — six, none of them a colour
 * -----------------------------------------------------------------------------
 * Declared on the bare `[data-presentation]` as well as `:root` so a preview
 * fragment carrying the attribute recomputes `--kc-b-read` against its OWN
 * register rather than inheriting the page's — the same reason kc-theme.css
 * lists both selectors in its reduce block.
 *
 *   --kc-b-rise   how far the incoming beat travels, in em so it tracks the
 *                 member's OS text size instead of undoing it
 *   --kc-b-fold   the scale the outgoing beat folds to as it is filed
 *   --kc-b-fold-y how far it travels toward the rail (negative = up, toward it)
 *   --kc-b-kept   the strength a filed beat's pip keeps
 *   --kc-b-read   Law 2: the delay after which a sentence has been readable
 *   --kc-b-lead   the within-arrival stagger; 0 in focused, one step in vivid
 * ========================================================================== */
:root,
[data-presentation] {
  --kc-b-rise:   .8em;
  --kc-b-fold:   .82;
  --kc-b-fold-y: -1.6em;
  --kc-b-kept:   .45;
  --kc-b-read:   calc(var(--motion-slow) * 2);
  --kc-b-lead:   var(--motion-step);

  /* THE ARRIVAL IS ITS OWN DURATION, and a much longer one than the rest of the
     product. --motion-base is 140ms, tuned for a control acknowledging a press:
     correct there, and far too quick for a paragraph, which the eye has to find
     and start reading rather than merely notice. A press is over before you
     look; an arrival has to be watchable. Derived, never literal -- the reduce
     block zeroes --motion-base, so these zero with it. */
  --kc-b-in:     calc(var(--motion-base) * 2.6);
  --kc-b-out:    calc(var(--motion-base) * 2);
  --kc-b-thread: calc(var(--motion-slow) * 2.2);
  /* Expo-out. It covers most of the distance early and settles rather than
     stopping, so the text is readable well before the motion finishes -- the
     opposite trade to --ease, which is symmetrical and reads as mechanical over
     a long duration. Deliberately NOT an overshoot curve: text that bounces
     past its resting place and comes back has to be re-fixated to read. */
  --ease-beat:   cubic-bezier(.16, 1, .3, 1);
}
:root[data-presentation="vivid"],
[data-presentation="vivid"] { --kc-b-rise: 1.1em; --kc-b-fold: .76; --kc-b-fold-y: -2em;
  --kc-b-in: calc(var(--motion-base) * 1.8); --kc-b-out: calc(var(--motion-base) * 1.5); }

/* `clear` is the high-legibility register and its promise is that nothing moves
   unless the reader moved it. The amplitudes go to zero here as well as the
   durations, so that even if a future rule forgets a guard there is nothing
   left for it to travel. --kc-b-kept rises because `clear`'s groove is a solid
   mid-grey rather than a low alpha, and .45 over it separates less. */
:root[data-presentation="clear"],
[data-presentation="clear"] { --kc-b-rise: 0em; --kc-b-fold: 1; --kc-b-fold-y: 0em; --kc-b-kept: .7; }


/* =============================================================================
 * 2. THE STAGE — what stops the primary control moving
 * -----------------------------------------------------------------------------
 * THE MEASUREMENT THAT FORCED THIS. Published beats run 112 to 702 characters;
 * rendered, that is two to nine lines, 51px to 230px on the desktop card. One
 * beat per screen on its own therefore trades a page that grows for a footer
 * button that JUMPS: measured up to 345px of travel between two consecutive
 * screens of one step on desktop, 581px on the phone. That is a new defect, not
 * a fixed one.
 *
 * THE FIX IS NOT AN ESTIMATE. Every beat of the step is emitted into the same
 * grid cell — the current one inside `.kc-v2-beat-holder`, the rest as
 * `.kc-v2-beat-spacer`. A grid cell is as tall as its tallest item, so the
 * stage is exactly as tall as the tallest beat IN THIS STEP, measured by the
 * browser at the member's own text size, in their own language, at their own
 * window width. No character-count heuristic can be right at all four of those
 * at once, and a `min-height` in `vh` would fail tests/stretch.test.js's rule
 * against elements rendered far beyond their natural content height.
 *
 * The spacers are `visibility: hidden`, which removes them from the
 * accessibility tree, from find-in-page and from selection, and they carry
 * `aria-hidden` as well. `.kc-v2-beat` therefore appears EXACTLY ONCE in the
 * DOM on every screen — which is the assertion tests/lessonbeats.test.js pins.
 *
 * The takeaway screen keeps the same floor rather than collapsing it: the
 * takeaway and its boundary are usually taller than one beat, so the cell grows
 * naturally, and where they are shorter the footer still does not move. */
.kc-v2-beatline {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 16px 0 0;
}

.kc-v2-beat-stage {
  display: grid;
  align-content: start;
  /* Not `stretch`: nothing in the cell wants to be as tall as the cell, and
     leaving them stretched makes every item report the ROW height, which hides
     exactly the kind of one-line difference this reservation exists to absorb. */
  align-items: start;
}

.kc-v2-beat-stage > * { grid-area: 1 / 1; }

/* The live screen sits above the ghost, never under it: a dying sentence must
   never occlude a live one. */
.kc-v2-beat-holder,
.kc-v2-beatline .kc-v2-beat { position: relative; z-index: 1; }

.kc-v2-beat-holder:focus,
.kc-v2-beatline .kc-v2-beat:focus { outline: none; }
.kc-v2-beat-holder:focus-visible,
.kc-v2-beatline .kc-v2-beat:focus-visible {
  outline: var(--bw-2) solid var(--acc-ring);
  outline-offset: 6px;
  border-radius: var(--r-12);
}

/* The spacer and the live beat must be the SAME BOX or the reservation is a
   guess. Every property that affects height is set once, for both. */
.kc-v2-beatline .kc-v2-beat,
.kc-v2-beatline .kc-v2-beat-spacer {
  max-width: 68ch;
  margin: 0;
  padding-inline-start: 16px;
  border-inline-start: 0;
  font-size: var(--fs-15);
  font-weight: 400;
  line-height: 1.7;
  text-wrap: pretty;
  /* THE ONE PROPERTY THAT HAD TO BE FOUND BY MEASURING. kc-lesson-uplift.css
     styles the visible beat with the `font:` SHORTHAND, which resets
     font-variant-numeric to `normal`; the spacer matches no such rule and
     inherited the page's `tabular-nums slashed-zero`. Tabular figures are
     wider, so a paragraph containing "140,000" and "17" wrapped to five lines
     hidden and six lines visible AT THE SAME WIDTH — and because the spacers
     are what set the stage height, that one-line disagreement moved the
     primary control by 25px on exactly one screen of `s3_sarcopenia`.
     Declaring it here makes the two boxes identical whatever else in the
     cascade does or does not match them, on either surface. */
  font-variant-numeric: normal;
}

.kc-v2-beatline .kc-v2-beat { color: var(--ink2, var(--text-2)); }

.kc-v2-beat-spacer {
  visibility: hidden;
  z-index: 0;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}


/* =============================================================================
 * 3. THE RAIL — where the beats you have read are kept
 * -----------------------------------------------------------------------------
 * One bar per screen of the step. This is the whole answer to "where did my
 * paragraphs go": they are the filled bars to the left of the current one.
 *
 * THE FILL IS ONE PAINT AT TWO STRENGTHS, NOT TWO TOKENS. A first pass used
 * `--acc-quiet` for `kept` and `--track` for `remaining`; rendered in the
 * default register those resolve to a 7% accent alpha and an 18% neutral, so a
 * beat the member had READ drew FAINTER than one they had not. Ordering two
 * different tokens by luminance would have to hold across nine
 * register/accent combinations times two schemes; ordering ONE token by alpha
 * holds by construction. So the groove is `--track` and the fill is `--acc`
 * over it, at `--kc-b-kept` when filed and at full strength when current.
 *
 *     remaining   groove only        --bw-2 thick
 *     kept        --acc at .45       --bw-4 thick   <- still thick: still counts
 *     current     --acc at 1         --bw-4 thick
 *
 * KEPT AND CURRENT SHARE A THICKNESS DELIBERATELY. Only the strength steps back
 * when a beat is filed, so the rail says "read, no longer the focus" rather
 * than "shrunk". Greyscale-safe: kept and remaining differ by thickness, kept
 * and current by value.
 *
 * The rail is `aria-hidden`. `.kc-v2-beat-count` is its accessible form and is
 * never hidden, so assistive technology gets the position as a sentence rather
 * than as eight unlabelled spans.
 * ========================================================================== */
.kc-v2-beat-rail {
  display: flex;
  gap: 4px;
  align-items: center;
  margin: 0;
  padding: 0;
  list-style: none;
}

.kc-v2-beat-pip {
  position: relative;
  flex: 1 1 0;
  min-width: 10px;
  height: var(--bw-2);
  border-radius: var(--r-999);
  background: var(--track, var(--border-strong));
}

.kc-v2-beat-pip[data-state="kept"],
.kc-v2-beat-pip[data-state="current"] { height: var(--bw-4); }

.kc-v2-beat-pip[data-state="kept"]::after,
.kc-v2-beat-pip[data-state="current"]::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--acc, var(--blue-600));
  transform-origin: 0 50%;
}

.kc-v2-beat-pip[data-state="kept"]::after { opacity: var(--kc-b-kept); }

/* G4 KEEP — the pip that just stopped being current.
   MEANS: that part is filed. It does not shrink and it does not vanish; only
   its strength steps back, which is the smallest change that can mean "read".
   DURATION --motion-base · EASING --ease · DELAY --kc-b-lead
   STATIC: the pip is already painted at --kc-b-kept and --bw-4. Nothing is
   lost — the animation shows the MOMENT of the change, never the change. */
.kc-v2-beatline[data-enter] .kc-v2-beat-pip[data-just-kept="true"]::after {
  animation: kcBeatKeep var(--motion-base) var(--ease) var(--kc-b-lead) both;
}

/* THE RAIL ADVANCING — the newly current pip draws in from its leading edge.
   MEANS: the argument moved one place forward. Carried by the position
   indicator rather than by a separate object, so it costs no new element and
   cannot be mistaken for decoration.
   DURATION --motion-base · EASING --ease · DELAY --kc-b-lead
   STATIC: the pip is filled. */
.kc-v2-beatline[data-enter] .kc-v2-beat-pip[data-state="current"]::after {
  animation: kcBeatTake var(--motion-base) var(--ease) var(--kc-b-lead) both;
}

@keyframes kcBeatKeep { from { opacity: 1; } }
@keyframes kcBeatTake { from { transform: scaleX(0); } to { transform: none; } }

/* G6 COUNT — the number advancing.
   MEANS: an increment. --ease-press is the only overshoot in this file and it
   is spent here, because a counter that ticks slightly past and settles reads
   as mechanical rather than decorative. It reverses on `back`, which is what
   makes the direction of travel unambiguous.
   DURATION --motion-fast · EASING --ease-press · DELAY 0
   STATIC: it is a number, written in words. tabular-nums keeps "Part 9 of 12"
   from shifting the sentence as the digits change. */
.kc-v2-beat-count {
  margin: 0;
  color: var(--ink3, var(--text-3));
  font-size: var(--fs-12);
  font-weight: 650;
  letter-spacing: .06em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

.kc-v2-beatline[data-enter="forward"] .kc-v2-beat-count {
  animation: kcBeatCountUp var(--motion-fast) var(--ease-press) both;
}
.kc-v2-beatline[data-enter="back"] .kc-v2-beat-count {
  animation: kcBeatCountDown var(--motion-fast) var(--ease-press) both;
}

@keyframes kcBeatCountUp   { from { opacity: 0; transform: translateY(.4em); } to { transform: none; } }
@keyframes kcBeatCountDown { from { opacity: 0; transform: translateY(-.4em); } to { transform: none; } }


/* =============================================================================
 * 4. THE BEAT — G1 CARRY and G2 THREAD
 * -----------------------------------------------------------------------------
 * G1 CARRY. The beat rises into place along the axis reading already travels, a
 * fraction of a line, not a slide from off-screen. A horizontal slide is the
 * grammar of a NEW PAGE; this is the next line of the same one — and
 * `.kc-v2-card` is `overflow: hidden`, so a horizontal translate would be
 * clipped at the card edge and read as a glitch. Opacity reaches 1 at 70% of
 * the duration so no word is ever read while it is still moving.
 *   DURATION --motion-base · EASING --ease · DELAY --kc-b-lead
 *   STATIC: the beat is simply there, and the rail says which one it is.
 *
 * G2 THREAD. kc-lesson-uplift.css calls the accent rule "the sequence: what
 * makes three separately-revealed paragraphs read as one taught thread". One
 * beat per screen destroys that thread — so the rule is promoted from a border
 * to a drawn bar and BECOMES the motion. Going forward it grows DOWNWARD from
 * the top, from exactly where the previous beat's rule ended, so it reads as
 * one line continuing rather than a new line starting. Going back it grows up.
 *   DURATION --motion-slow · EASING --ease · DELAY 0 (it leads)
 *   STATIC: the full-height accent rule the design already had. The rule's
 *   PRESENCE was always the sequence signal; only its drawing is the motion.
 * ========================================================================== */
.kc-v2-beatline .kc-v2-beat::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: var(--bw-2);
  border-radius: var(--r-999);
  background: var(--acc, var(--blue-600));
  transform-origin: 50% 0;
}

.kc-v2-beatline[data-enter="forward"] .kc-v2-beat {
  animation: kcBeatInUp var(--kc-b-in) var(--ease-beat) var(--kc-b-lead) both;
}
.kc-v2-beatline[data-enter="back"] .kc-v2-beat {
  animation: kcBeatInDown var(--kc-b-in) var(--ease-beat) var(--kc-b-lead) both;
}

.kc-v2-beatline[data-enter="forward"] .kc-v2-beat::before {
  animation: kcBeatThread var(--kc-b-thread) var(--ease-beat) both;
}
.kc-v2-beatline[data-enter="back"] .kc-v2-beat::before {
  transform-origin: 50% 100%;
  animation: kcBeatThread var(--kc-b-thread) var(--ease-beat) both;
}

@keyframes kcBeatInUp {
  0%   { opacity: 0; transform: translateY(var(--kc-b-rise)); clip-path: inset(0 0 100% 0); }
  55%  { opacity: 1; }
  100% { opacity: 1; transform: none; clip-path: inset(0 0 0 0); }
}
@keyframes kcBeatInDown {
  0%   { opacity: 0; transform: translateY(calc(-1 * var(--kc-b-rise))); clip-path: inset(100% 0 0 0); }
  55%  { opacity: 1; }
  100% { opacity: 1; transform: none; clip-path: inset(0 0 0 0); }
}
@keyframes kcBeatThread { from { transform: scaleY(.02); } to { transform: none; } }


/* =============================================================================
 * 5. G3 FILE — the beat you just read folding into the rail
 * -----------------------------------------------------------------------------
 * THE ONE GESTURE THIS FILE EXISTS FOR. Replacing a paragraph with the next one
 * is, on its own, indistinguishable from DELETING it. So the outgoing beat does
 * not blink out: it shrinks and travels toward the rail, and lands as the pip
 * that steps back to --kc-b-kept at the same moment. The paragraph BECAME the
 * mark. Kept, not discarded — shown rather than asserted.
 *
 * IT COSTS NO EXTRA MARKUP. The outgoing beat is already in the DOM as the
 * height-reserving spacer for its own index (section 2), so `_renderConcept`
 * marks that one spacer `data-ghost="true"` and nothing is cloned, stashed or
 * measured in JavaScript.
 *
 *   DURATION --motion-slow · EASING --ease · DELAY 0, so it overlaps the
 *   incoming beat's arrival — the overlap is what makes it a handoff rather
 *   than two separate events.
 *
 * BASE STATE IS `opacity: 0`, NOT `opacity: 1`. Under `animation: none` this
 * ghost would otherwise be a permanent duplicate paragraph frozen over the live
 * one. The base state is the FINISHED state, per the invariant at the top.
 *
 * STATIC EQUIVALENT: none, and none is needed. "That beat is kept" is carried
 * permanently by the pip it folds into, which is on screen in every register.
 * In `clear` and under `reduce` the ghost is not drawn at all — absent, not
 * accelerated — and section 9 removes it from the box tree rather than leaving
 * an invisible paragraph-shaped thing lying over a live one.
 * ========================================================================== */
.kc-v2-beatline[data-enter] .kc-v2-beat-spacer[data-ghost="true"] {
  color: var(--ink3, var(--text-3));
  /* THE RULE IS AN INSET SHADOW, NOT A BORDER — and this was measured, not
     preferred. A border on the ghost changes its content width by --bw-2,
     which is enough to reflow a paragraph onto one more line; because the
     ghost is one of the spacers that SETS the stage height, that reflow grew
     the cell and moved the primary control by 26px on desktop and 63px on the
     phone, on exactly the beats that happened to sit near a line boundary.
     A box-shadow draws the same 2px and takes part in no layout at all. */
  box-shadow: inset var(--bw-2) 0 0 0 var(--acc, var(--blue-600));
  transform-origin: 0 0;
  animation: kcBeatFile var(--motion-slow) var(--ease) both;
}

.kc-v2-beatline[data-enter="back"] .kc-v2-beat-spacer[data-ghost="true"] {
  --kc-b-fold-y: 1.6em;
}

/* `visibility` is carried BY THE KEYFRAME, not by the rule above. The spacer's
   resting state is `visibility: hidden` (section 2) and that is also this
   animation's finished state, so the base-state invariant holds with nothing
   added: under `animation: none` the ghost is not merely transparent, it is
   out of the box tree — no selectable duplicate paragraph, nothing for
   find-in-page to land on. The 99% stop makes the flip explicit rather than
   leaning on the special interpolation rule for `visibility`. */
@keyframes kcBeatFile {
  from { visibility: visible; opacity: 1; transform: none; }
  99%  { visibility: visible; }
  to   { visibility: hidden; opacity: 0; transform: translateY(var(--kc-b-fold-y)) scale(var(--kc-b-fold)); }
}


/* =============================================================================
 * 6. THE PREDICTION, CARRIED THROUGH THE STEP
 * -----------------------------------------------------------------------------
 * Six of the 22 published concept steps open with a prediction, and the moment
 * it was committed it vanished — which is the wrong moment to lose it, because
 * the beats that follow exist to test it. It is one line, identical on every
 * screen of the step, so it costs the primary control no movement.
 *
 * ARRIVAL, first beat only: it descends from above, from where the prediction
 * form just was, so it reads as the same object moving rather than a new one
 * appearing. DURATION --motion-slow · EASING --ease · DELAY 0.
 *
 * G5 MARK, last screen: an inset ring at --acc arrives at --kc-b-read, AFTER
 * the closing sentence has been readable. It means "now compare what you
 * predicted with what you have just read", and it must not arrive while that
 * sentence is being read. `box-shadow`, not `border-width`, so nothing reflows.
 *   STATIC: the ring is simply painted on the last screen and absent on the
 *   others, so the distinction survives with nothing moving.
 * ========================================================================== */
.kc-v2-beat-pledge {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: baseline;
  margin: 0;
  padding: 8px 12px;
  /* AN EDGE AS WELL AS A FILL. `--plate` is very close to the card ground in
     `clear`/solid, where the chip disappeared into it. The rule is the channel
     that does not depend on the two grounds differing — kc-interaction.css's
     "colour is never alone". */
  border: var(--bw-1) solid var(--rule, var(--border-strong));
  border-radius: var(--r-12);
  background: var(--plate, var(--surface-2));
  color: var(--ink2, var(--text-2));
  font-size: var(--fs-13);
  line-height: 1.55;
}

.kc-v2-beat-pledge-label {
  color: var(--ink3, var(--text-3));
  font-size: var(--fs-12);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.kc-v2-beatline[data-beat-pos="first"][data-enter="forward"] .kc-v2-beat-pledge {
  animation: kcBeatCarry var(--motion-slow) var(--ease) both;
}

.kc-v2-beatline[data-beat-pos="last"] .kc-v2-beat-pledge {
  box-shadow: inset 0 0 0 var(--bw-2) var(--acc, var(--blue-600));
}
.kc-v2-beatline[data-beat-pos="last"][data-enter] .kc-v2-beat-pledge {
  animation: kcBeatCompare var(--motion-slow) var(--ease) var(--kc-b-read) both;
}

@keyframes kcBeatCarry   { from { opacity: 0; transform: translateY(calc(-1 * var(--kc-b-rise))); } to { transform: none; } }
@keyframes kcBeatCompare { from { box-shadow: inset 0 0 0 0 var(--acc, var(--blue-600)); } }


/* =============================================================================
 * 7. G5 MARK — the two late gestures that close the argument
 * -----------------------------------------------------------------------------
 * THE END CAP. On the last beat the thread gets a foot: a short perpendicular
 * tick where the rule ends. It means "the thread stops here" and it is the only
 * warning the member gets that the next press leaves the teaching. It arrives
 * at --kc-b-read, after the closing sentence has been readable, because a mark
 * that says "this is the end" is worthless if it lands before the end is read.
 *   STATIC: the tick is drawn, in every register. The last beat looks different
 *   from the others whether or not anything moved.
 *
 * THE TAKEAWAY'S RULE. The takeaway is not one more beat, it is the argument
 * closing, so it does not rise: the same thread that ran down every beat runs
 * down the conclusion, at --bw-4 instead of --bw-2. Same vocabulary, heavier. A
 * conclusion consolidates what is on screen; it does not arrive from somewhere
 * else, so nothing travels.
 *   STATIC: the heavy accent edge is painted on the panel.
 * ========================================================================== */
.kc-v2-beatline[data-beat-pos="last"] .kc-v2-beat::after {
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  width: 14px;
  height: var(--bw-2);
  border-radius: var(--r-999);
  background: var(--acc, var(--blue-600));
  transform-origin: 0 50%;
}
.kc-v2-beatline[data-beat-pos="last"][data-enter] .kc-v2-beat::after {
  animation: kcBeatCap var(--motion-base) var(--ease) var(--kc-b-read) both;
}

.kc-v2-beatline[data-beat-role="takeaway"] .kc-v2-takeaway { position: relative; overflow: hidden; }
.kc-v2-beatline[data-beat-role="takeaway"] .kc-v2-takeaway::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: var(--bw-4);
  background: var(--acc, var(--blue-600));
  transform-origin: 50% 0;
}
.kc-v2-beatline[data-beat-role="takeaway"][data-enter] .kc-v2-takeaway::before {
  animation: kcBeatResolve var(--motion-slow) var(--ease) var(--kc-b-read) both;
}

/* The takeaway and its boundary are one landing, so they stack as one block
   rather than two asides with the stage's gap between them. */
.kc-v2-beatline[data-beat-role="takeaway"] .kc-v2-takeaway,
.kc-v2-beatline[data-beat-role="takeaway"] .kc-v2-boundary { margin-top: 0; }
.kc-v2-beatline[data-beat-role="takeaway"] .kc-v2-boundary { margin-top: 12px; }

@keyframes kcBeatCap     { from { transform: scaleX(0); } to { transform: none; } }
@keyframes kcBeatResolve { from { transform: scaleY(.02); } to { transform: none; } }


/* =============================================================================
 * 8. THE HEADER, WHILE A BEAT IS ON SCREEN — and "Previous part"
 * -----------------------------------------------------------------------------
 * WHY THE HEADER IS IN THIS FILE AT ALL. Splitting the beats does not by itself
 * produce "a new screen": measured signed in, `.kc-v2-top` is 250px and the
 * card starts 432px down the page, so at 1280x720 a ONE-BEAT screen is already
 * 836px of document — it scrolls before a single paragraph has been read. The
 * lesson's h1 and its provenance line are byte-identical on all thirty-odd
 * screens of a lesson and are the largest reclaimable block.
 *
 * WHAT THIS DOES AND DOES NOT DO. It puts the exit and the h1 on ONE ROW and
 * sets the h1 at meta scale, reclaiming ~100px. The h1 SURVIVES — one h1 per
 * view — it is subordinate, not removed.
 *
 * IT IS GATED ON HEIGHT, DELIBERATELY. `@media (max-height: 940px)` is the
 * range where the vertical budget is the binding constraint (laptops, and the
 * phone shell). Above it the masthead kc-lesson-uplift.css designed is left
 * exactly as it is, because there the screen already fits and shrinking a title
 * would be a design opinion rather than a fix. It is also scoped to
 * `[data-beat-screen]`, which `render()` sets only while a concept step is
 * being read — so this never touches the other ten step kinds.
 *
 * PREVIOUS PART is the control the old design did not need. The growing <ol>
 * had one accidental virtue: the earlier paragraphs stayed on screen, so
 * re-reading cost nothing. One beat per screen takes that away, and it must be
 * given back or the change is a net loss for anyone who lost the thread. This
 * is also why `data-enter="back"` exists at all: the reverse of every gesture
 * above is defined, so going back is the same explanation running backwards
 * rather than an unexplained jump.
 * ========================================================================== */
.kc-v2-beat-back {
  min-height: var(--target-min, 44px);
  padding-inline: 12px;
  border: 0;
  border-radius: var(--r-999);
  background: transparent;
  color: var(--ink3, var(--text-3));
  font-size: var(--fs-13);
  font-weight: 650;
  cursor: pointer;
}
.kc-v2-beat-back:hover { color: var(--ink1, var(--text)); }
.kc-v2-beat-back:focus-visible {
  outline: var(--bw-2) solid var(--acc-ring);
  outline-offset: var(--bw-2);
}

/* The current step's dot fills by beat. `--kc-beat-fraction` is set on the
   element by render(), the same mechanism `.kc-v2-xp-fly` already uses for
   `--kc-v2-fly-x/y`. The dot is the only thing in the header that moves as the
   member reads, and it moves because something really changed. */
.kc-v2-step-dot[data-beat-fill] { position: relative; overflow: hidden; }
.kc-v2-step-fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: var(--kc-beat-fraction, 0%);
  border-radius: inherit;
  background: var(--ink1, var(--text));
  opacity: .55;
}
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-presentation="clear"]) .kc-v2-step-fill {
    transition: width var(--motion-base) var(--ease);
  }
}

@media (max-height: 940px) {
  .kc-v2-runner[data-beat-screen] .kc-v2-top {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: baseline;
    column-gap: 14px;
    margin-bottom: 14px;
    padding-bottom: 12px;
  }
  .kc-v2-runner[data-beat-screen] .kc-v2-top > * { grid-column: 1 / -1; }
  .kc-v2-runner[data-beat-screen] .kc-v2-back { grid-column: 1; grid-row: 1; }
  .kc-v2-runner[data-beat-screen] h1.kc-v2-title {
    grid-column: 2;
    grid-row: 1;
    /* kc-lesson-v2.css caps the display title at `max-width: 30ch`, which is a
       measure for a 38px display face. At meta scale on one row that cap is
       what truncates a perfectly short lesson name. */
    max-width: none;
    margin: 0;
    font-size: var(--fs-15);
    font-weight: 600;
    line-height: 1.3;
    letter-spacing: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .kc-v2-runner[data-beat-screen] .kc-v2-meta { grid-row: 2; margin-top: 6px; }
  .kc-v2-runner[data-beat-screen] .kc-v2-section-progress { margin-top: 12px; }
  .kc-v2-runner[data-beat-screen] .kc-v2-step-progress { margin-top: 10px; }
}


/* =============================================================================
 * 9. `clear`, REDUCED MOTION, FORCED COLOURS, RTL
 * -----------------------------------------------------------------------------
 * Both registers delete animation outright, so every gesture above is already
 * correct: each one lands on its base state, and its base state is its finished
 * state. TWO THINGS STILL HAVE TO BE SAID EXPLICITLY.
 *
 * The ghost's base state is `opacity: 0`, which is right, but a
 * paragraph-shaped invisible box lying over the live paragraph is still a thing
 * that exists. Both cases return it to `visibility: hidden` — absent, not
 * accelerated.
 *
 * The reduce block does not lean on kc-theme.css's, because that one is scoped
 * to descendants of `[data-presentation]` and there is no guarantee the
 * attribute is on the document of every page that mounts this runner. This file
 * degrades on its own terms, on both surfaces.
 * ========================================================================== */
:root[data-presentation="clear"] .kc-v2-beatline .kc-v2-beat-spacer,
[data-presentation="clear"] .kc-v2-beatline .kc-v2-beat-spacer {
  visibility: hidden !important;
  opacity: 0 !important;
}

@media (prefers-reduced-motion: reduce) {
  .kc-v2-beatline .kc-v2-beat-spacer {
    visibility: hidden !important;
    opacity: 0 !important;
  }
  .kc-v2-beatline,
  .kc-v2-beatline *,
  .kc-v2-beatline *::before,
  .kc-v2-beatline *::after,
  .kc-v2-step-fill {
    animation: none !important;
    transition: none !important;
  }
}

/* `transform-origin` takes no logical keyword, so the three origins that mean
   "the leading edge" are restated for RTL. */
[dir="rtl"] .kc-v2-beat-pip[data-state="kept"]::after,
[dir="rtl"] .kc-v2-beat-pip[data-state="current"]::after,
[dir="rtl"] .kc-v2-beatline[data-beat-pos="last"] .kc-v2-beat::after { transform-origin: 100% 50%; }
[dir="rtl"] .kc-v2-beatline[data-enter] .kc-v2-beat-spacer[data-ghost="true"] { transform-origin: 100% 0; }

@media (forced-colors: active) {
  .kc-v2-beat-pip { border: var(--bw-1) solid CanvasText; background: Canvas; }
  .kc-v2-beat-pip[data-state="kept"]::after    { background: GrayText; opacity: 1; }
  .kc-v2-beat-pip[data-state="current"]::after { background: Highlight; }
  .kc-v2-beatline .kc-v2-beat::before,
  .kc-v2-beatline[data-beat-pos="last"] .kc-v2-beat::after,
  .kc-v2-beatline[data-beat-role="takeaway"] .kc-v2-takeaway::before { background: CanvasText; }
  .kc-v2-step-fill { background: Highlight; opacity: 1; }
  .kc-v2-beatline[data-beat-pos="last"] .kc-v2-beat-pledge { outline: var(--bw-2) solid Highlight; outline-offset: 0; }
}

/* The phone card is narrower, so the same beat runs to more lines and the pips
   have less room. Nothing else changes: the vocabulary is identical on /m.
 *
 * THE FOOTER STAYS A ROW ON THE PHONE, and only on a beat screen. kc-lesson-
 * v2.css turns `.kc-v2-footer` into a stretched COLUMN at this width, which is
 * right for a step whose two controls are both full-size commitments. Here the
 * second control is a quiet "Previous part" that is ABSENT on the first screen
 * and present on every screen after it, so a column footer grew by 58px the
 * moment it appeared — measured on m.html at 390x844, the primary button moved
 * from 820px to 757px between part 1 and part 2. A row keeps the footer one
 * height whether the back control is there or not. */
@media (max-width: 680px) {
  .kc-v2-beat-pip { min-width: 8px; }
  .kc-v2-beatline { gap: 10px; }

  .kc-v2-runner[data-beat-screen] .kc-v2-footer {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
  }
  .kc-v2-runner[data-beat-screen] .kc-v2-footer .kc-v2-primary { flex: 1 1 auto; }
  .kc-v2-runner[data-beat-screen] .kc-v2-beat-back { flex: 0 0 auto; padding-inline: 4px; }
}
