/* TASK-90/122/136/139 — Light case: the onboarding scene. A pinned camera
   move across both flows (`case-light-scene.js`), replacing the filmstrip
   TASK-90 shipped and the walkthrough before that. Built and tuned on the
   `_t122-story.*` bench (TASK-122/126/136) and ported into the case here once
   Pavlo picked all seven readings — the losing branches of each switch went
   with the tuner, so what is below is the one reading each dial has, not a
   mode among several.

   Everything the camera writes is a custom property or an inline transform.
   No CSS transition sits on the plane: its position is written every frame by
   the filter in `case-light-scene.js`, and easing on top of that reads as
   rubber. */

.st {
  /* Composition constants, Pavlo's own off the tuner (TASK-122/126/136 — see
     TASK-122-HANDOFF.md §5 for what each one does and why it sits where it
     does). Fixed now that all seven readings are picked; `case-light-scene.js`
     reads the same four numbers for its own layout maths, so a change here has
     to be made there too. */
  --st-frame-h: 92vh;      /* the stage box */
  --st-fill: 0.76;         /* screen height inside the stage at camera 1× */
  --st-gap: 32px;
  --st-seam: 700px;        /* the breath between the two flows */
  --st-pad: var(--sp-6);
  --st-calm-lift: 8px;     /* pulls the closing caption off the row of screens */
  /* One source for the width of the stage. The indicator has to line up with
     the plate and cannot live inside it (the plate crops what leaves it), so
     without this the width would be written twice and drift apart the first
     time one of them moved. */
  --st-w: min(1310px, calc(100% - 2 * var(--gutter)));
  --st-flat-w: clamp(96px, 15vw, 150px);   /* a screen in the plain fallback */
  --st-ink: var(--text);
  --st-ink-soft: var(--text-muted);
  --st-edge: var(--rule-frame);
}

/* The rail is only scroll length: its height is written by JS from the number
   of beats, so adding a beat never means editing a number here. */
.st__rail { position: relative; }

.st__pin { display: grid; align-content: center; }

.st.is-live .st__pin { position: sticky; top: 0; height: 100vh; }

/* No plate under the scene and no line around it (Pavlo, 03.08): the chapter
   already stands on its own ground (`.chapter--paper`), and a second, greyer
   rectangle on top of it framed the camera move as a window to look through.
   Without it the screens arrive out of the chapter's own paper. The box is
   still a box — it crops what leaves it — it just no longer draws itself. */
.st__frame {
  position: relative;
  width: var(--st-w);
  margin-inline: auto;
  color: var(--st-ink);
}

.st.is-live .st__frame { height: var(--st-frame-h); overflow: hidden; }

/* The camera plane. `transform-origin: 0 0` keeps the maths honest: the JS
   places a strip coordinate in the middle of the stage, and an origin anywhere
   else would add a second unknown to every frame. */
.st.is-live .st__plane {
  position: absolute;
  inset: 0 auto 0 0;
  display: flex;
  align-items: center;
  width: max-content;
  height: 100%;
  gap: var(--st-gap);
  transform-origin: 0 0;
  transition: none;
  will-change: transform;
}

.st__cell { flex: 0 0 auto; margin: 0; }
.st.is-live .st__cell { height: calc(var(--st-frame-h) * var(--st-fill)); }

.st__cell img {
  display: block;
  width: 100%;
  height: auto;
  border: var(--rule-hair) solid var(--st-edge);
  background: var(--white);
}

.st.is-live .st__cell img { width: auto; height: 100%; }

.st__seam { flex: 0 0 var(--st-seam); align-self: stretch; }

/* ── the intro frame (TASK-136 §1.1) ──────────────────────────────────────
   The chapter opens on a frame its text carries. The screens are in it, but
   small and driving in from the right, so the left of the stage is a clear
   field for the opening: kicker, the chapter word on its rule, then the number
   and the bet set as two columns hanging off that rule.

   Its width is capped rather than run to the stage padding, and the cap is
   load-bearing: the row travels left through this segment while the text is
   fading, so anything wider is printed over by the screens on the way in.

   Opacity is written by JS off the head of the timeline, so the frame costs
   the scene no scroll length of its own — a frame that holds still while the
   reader scrolls is a dwell, and this scene has none. */

.st.is-live .st__intro {
  position: absolute;
  top: var(--st-pad);
  bottom: var(--st-pad);
  left: var(--st-pad);
  display: grid;
  align-content: center;
  width: min(46%, 600px, calc(var(--st-intro-max, 600px) - var(--st-pad) - var(--sp-6)));
  pointer-events: none;
  will-change: opacity;
}

.st__intro-kicker {
  margin: 0 0 var(--sp-3);
  font-family: var(--font-label);
  font-size: var(--fs-caption);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--st-ink-soft);
}

/* Weight and its own floor carry the mass here, not size alone: the word sits
   on a bold rule and everything else hangs under it.

   `--ws-caps` is not decoration next to `--tr-caps`, it is the other half of
   the same decision: negative tracking is added to the space character too, so
   without it the words of a bold narrow caps setting close up. */
.st__intro-word {
  margin: 0;
  padding-bottom: var(--sp-3);
  border-bottom: var(--rule-bold) solid var(--st-ink);
  font-family: var(--font-condensed);
  font-size: var(--fs-h1);
  font-weight: var(--wt-bold);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-caps);
  word-spacing: var(--ws-caps);
  text-transform: uppercase;
  color: var(--st-ink);
}

/* The number in the margin, the sentence in the column — the field is working
   space here, not a border (editorial rules §3.4). */
.st__intro-body {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-6);
  margin-top: var(--sp-5);
}

.st__intro-lede { align-self: end; }

.st__intro-lede p {
  max-width: 44ch;
  margin: 0;
  font-size: var(--fs-small);
  line-height: var(--lh-text);
  color: var(--st-ink);
}

.st__intro-lede p + p { margin-top: var(--sp-2); }

/* The conversion number, and the one element in the scene allowed to change
   size between frames: it is the headline here and evidence two frames later,
   so it arrives large and takes its place among the other two figures when the
   context frame explains it. */
.st__fig {
  margin: 0;
  font-family: var(--font-condensed);
  font-size: var(--fs-h2);
  font-weight: var(--wt-bold);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-mega);
  color: var(--st-ink);
  font-variant-numeric: tabular-nums;
}

.st__fig span {
  display: block;
  margin-top: var(--sp-2);
  font-family: var(--font-label);
  font-size: var(--fs-caption);
  font-weight: var(--wt-reg);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--st-ink-soft);
}

/* ── annotations ─────────────────────────────────────────────────────────── */

.st.is-live .st__notes { position: absolute; inset: 0; pointer-events: none; }

.st.is-live .st__note {
  position: absolute;
  max-width: 34ch;
  opacity: 0;
  will-change: opacity, transform;
}

.st.is-live .st__note[data-pos="tl"] { top: var(--st-pad); left: var(--st-pad); }
.st.is-live .st__note[data-pos="tr"] { top: var(--st-pad); right: var(--st-pad); text-align: right; }
.st.is-live .st__note[data-pos="bl"] { bottom: var(--st-pad); left: var(--st-pad); }
.st.is-live .st__note[data-pos="br"] { bottom: var(--st-pad); right: var(--st-pad); text-align: right; }

.st__note p { margin: 0; font-size: var(--fs-small); line-height: var(--lh-text); color: var(--st-ink); }
.st__note p + p { margin-top: var(--sp-3); }

/* Qualified by the block on purpose: a bare `.st__note-label` loses to
   `.st__note p` on specificity and the label silently comes out at body size. */
.st__note .st__note-label {
  font-family: var(--font-label);
  font-size: var(--fs-caption);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--st-ink-soft);
}

.st__note .st__num {
  font-family: var(--font-condensed);
  font-size: var(--fs-h3);
  font-weight: var(--wt-bold);
  letter-spacing: var(--tr-mega);
  line-height: var(--lh-tight);
  font-variant-numeric: tabular-nums;
}

.st__note .st__num span { color: var(--st-ink-soft); }

/* The hypotheses: the upper band of the wide shot, three columns across the
   full stage. Not a corner block like the rest — three items set as a corner
   note would either wrap into a column of runts or push the row off centre. */
.st.is-live .st__note--top {
  top: var(--st-pad);
  right: var(--st-pad);
  left: var(--st-pad);
  max-width: none;
}

.st__hyps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sp-5);
  margin: var(--sp-3) 0 0;
  padding: 0;
  list-style: none;
  counter-reset: hyp;
}

.st__hyps li {
  max-width: 34ch;
  padding-top: var(--sp-2);
  border-top: var(--rule-hair) solid var(--st-edge);
  font-size: var(--fs-small);
  line-height: var(--lh-text);
  color: var(--st-ink);
}

.st__hyps li::before {
  counter-increment: hyp;
  content: counter(hyp, decimal-leading-zero);
  display: block;
  margin-bottom: var(--sp-1);
  font-family: var(--font-condensed);
  font-weight: var(--wt-bold);
  letter-spacing: var(--tr-label);
  color: var(--st-ink-soft);
}

/* The context frame (TASK-136 §1.3). Unlike every other note it is not a
   corner block or a band: it stands either side of the screen the camera is
   pushing in on, and the two gutters it lives in change width every frame as
   that screen grows. So the widths are not written here — JS measures the
   screen's actual edges and hands them over as `--st-hole-l` / `--st-hole-r`.

   The row parks off centre (развилка 2, «right» — Pavlo's pick) and the whole
   block stands in the clear ground to its left, keeping the intro frame's
   spine. Two columns inside that block: the story is prose and the funnel is
   figures, and a ledger set into running text loses both. */
.st.is-live .st__note--ctx {
  top: var(--st-pad);
  right: auto;
  bottom: var(--st-pad);
  left: var(--st-pad);
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  align-content: center;
  gap: var(--sp-6);
  width: calc(var(--st-hole-l, 50%) - var(--st-pad) - var(--sp-7));
  max-width: 74ch;
}

/* One step down from the hypotheses, and the reason is reading order rather
   than the amount of text: this block says why the number was read as low,
   and the block that later stands in the same scene is the argument itself. */
.st.is-live .st__note--ctx p { font-size: var(--fs-caption); }
.st.is-live .st__note--ctx .st__ctx-story p + p { margin-top: var(--sp-4); }

/* Three figures on hairlines, in the order the funnel moved: what the app
   converted before there was anything to buy, what it converted at launch, and
   where the baseline had drifted by the time the redesign was measured. The
   middle one is the number the intro frame opened with — it is at full ink and
   a step larger, the other two carry it. */
.st__ctx-fig {
  margin: 0;
  padding-top: var(--sp-2);
  border-top: var(--rule-hair) solid var(--st-edge);
}

.st__ctx-fig + .st__ctx-fig { margin-top: var(--sp-4); }

.st__ctx-fig b {
  display: block;
  font-family: var(--font-condensed);
  font-size: var(--fs-h4);
  font-weight: var(--wt-bold);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-mega);
  color: var(--st-ink-soft);
  font-variant-numeric: tabular-nums;
}

.st__ctx-fig--lead b { font-size: var(--fs-h3); color: var(--st-ink); }

.st.is-live .st__note--ctx .st__ctx-fig span {
  display: block;
  margin-top: var(--sp-1);
  font-size: var(--fs-caption);
  line-height: var(--lh-text);
  color: var(--st-ink-soft);
}

.st.is-live .st__note--ctx .st__ctx-figs .st__note-label { display: block; margin-bottom: var(--sp-3); }

/* The frame of rest (П12). It sits on the trial screen; the text stands as a
   caption below it (развилка 3, «cap» — Pavlo's pick), centred — the one place
   in the scene where centring is earned: a still and its caption. Everywhere
   else the system ranges left (editorial rules §4).

   No `transform` here: `paint()` writes the entrance drift into `transform` on
   every note every frame, so a translate used for centring would be
   overwritten on the first paint. Auto margins do the same job and survive. */
.st.is-live .st__note--calm {
  right: var(--st-pad);
  bottom: calc(var(--st-pad) - var(--st-calm-lift));
  left: var(--st-pad);
  max-width: none;
  text-align: center;
}

/* 62ch, not 52: the caption is bottom-anchored, so every extra line grows it
   upward into the row of screens. At 52ch the copy set to three lines and
   overlapped the screens by 7px on a 760px-high window. 62ch holds it at two
   lines with a word of slack, and `--st-calm-lift` keeps a floor of clearance
   under the screens regardless. Measured by `case-light-scene-clear.mjs`. */
.st.is-live .st__note--calm > * { max-width: 62ch; margin-inline: auto; }
.st__note--calm p:last-child { font-size: var(--fs-body); line-height: var(--lh-text); }

/* The closing frame (развилка 4, «band» — Pavlo's pick, TASK-136 §2): the row
   stays in the middle and the close runs as a band across the foot of the
   stage. Three columns, in the order the frame is read: what the flow was for,
   what it did, and the line that is not being counted. The rule over the band
   is the same device the chapter opened with.

   Text set flush left (Pavlo, 01.08): it is the ragged-left setting he objects
   to, not the position — four lines of different lengths ranged right give
   four different starts, which is what made the block read as a caption
   rather than as the close. `[data-pos]` is carried on purpose: without it this
   rule loses to `.st.is-live .st__note[data-pos="br"]` on specificity — three
   class-level parts against four. */
/* The band's rule runs the full width of the stage — the same fields the
   indicator bar stands between — and in the frame edge grey, not ink: the
   short dark rule read as a second, misaligned progress bar (Pavlo, 11.08). */
.st.is-live .st__note--result[data-pos] {
  text-align: left;
  right: 0;
  bottom: var(--st-pad);
  left: 0;
  display: grid;
  grid-template-columns: 1fr auto 1.1fr;
  align-items: start;
  gap: var(--sp-7);
  max-width: none;
  padding-top: var(--sp-4);
  border-top: var(--rule-bold) solid var(--st-edge);
}

.st.is-live .st__res-goal p { max-width: 34ch; }
.st__res-count { margin-top: var(--sp-4); }
.st.is-live .st__res-count { margin-top: 0; }
.st__res-count .st__num { margin-top: var(--sp-1); }
.st.is-live .st__note--result .st__close {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
  justify-self: end;
  max-width: 26ch;
}

/* The closing line (П14). It is the last thing in the frame and the only line
   in the scene set at heading weight: everything above it is being counted,
   and this one is not. */
.st__note .st__close {
  margin-top: var(--sp-5);
  padding-top: var(--sp-3);
  border-top: var(--rule-bold) solid var(--st-ink);
  font-family: var(--font-condensed);
  font-size: var(--fs-h4);
  font-weight: var(--wt-bold);
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-tight);
  color: var(--st-ink);
}

/* ── marks: one caption parked under its own screen ──────────────────────
   Written every frame by the camera, so the transform below is only a
   starting value. The text keeps its reading size while the row scales —
   that is the whole reason these are not inside the plane. */

.st.is-live .st__marks {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
}

.st.is-live .st__mark {
  position: absolute;
  top: 0;
  left: 0;
  margin: 0;
  padding-top: var(--sp-2);
  border-top: var(--rule-hair) solid var(--st-edge);
  font-size: var(--fs-caption);
  line-height: var(--lh-tight);
  color: var(--st-ink-soft);
}

/* The two ends of the funnel. Only these two numbers attach honestly to a
   screen: install is where the count starts and 18% is measured on the
   paywall itself. Everything between them would have to be invented. */
.st.is-live .st__mark b {
  display: block;
  margin-top: var(--sp-2);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: var(--wt-med);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--st-ink);
  font-variant-numeric: tabular-nums;
}

/* ── captions on the slow pass ───────────────────────────────────────────
   Parked under their own screens and cross-faded by the same nearness curve
   that dims the neighbours, so the caption and the screen it belongs to can
   never drift apart. Not uppercase: written as jobs, a full sentence in
   tracked caps is slower to read, not louder. Sit on a bold rule where the
   marks below the original flow sit on a hairline — that separates the two
   layers by register, so the argument reads as the argument and the analysis
   reads as the analysis. */

.st.is-live .st__caps {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.st.is-live .st__cap {
  position: absolute;
  top: 0;
  left: 0;
  margin: 0;
  padding-top: var(--sp-2);
  border-top: var(--rule-bold) solid var(--st-ink);
  font-family: var(--font-condensed);
  font-size: var(--fs-small);
  font-weight: var(--wt-semi);
  line-height: var(--lh-snug);
  color: var(--st-ink);
  opacity: 0;
}

/* ── the indicator (TASK-136 §3.3) ───────────────────────────────────────
   Hairlines, zero radius, the project's accent, the mono service layer. Not a
   row of dots: bullets belong to carousels, and a carousel is a thing you
   click. This one is read, never touched — there is no `button`, no `role`,
   no cursor of its own, and the DOM it is built from carries none either.

   Below the plate, in the strip the sticky frame leaves inside a 100vh pin
   (развилка 5, «below» — Pavlo's pick): inside the plate the captions of the
   slow pass hang in the padding and the bar ran into them. */

.st__ind { display: none; }

.st.is-live .st__ind {
  --st-ind-ink: var(--text);
  --st-ind-track: var(--rule-muted);
  position: absolute;
  left: 50%;
  translate: -50% 0;
  display: flex;
  align-items: center;
  width: var(--st-w);
  bottom: var(--sp-3);
  pointer-events: none;
}

/* The gap between cells has to beat the notches, or the two divisions read as
   one: at 4px against a 1px notch the filled run looked like a single rule
   with nicks in it, and a boundary between meaningful frames is the louder of
   the two by definition. */
.st__ind-bar { display: flex; flex: 1; align-items: center; gap: var(--sp-2); }

.st__ind-cell {
  position: relative;
  height: var(--rule-hair);
  background: var(--st-ind-track);
  transition: height 170ms ease;
}

.st__ind-cell[data-state="now"],
.st__ind-cell[data-state="done"] { height: var(--rule-bold); }

/* The reward, and the whole of it: the rule thickens and settles inside 180ms.
   Fired by the change of state rather than by the scroll, so it takes the same
   time however fast the reader is going, and it lands on the boundary where
   the strong magnet has just slowed the camera down. No glow, no shadow, no
   gradient — the event is the timing. */
@keyframes st-ind-tick {
  0% { height: var(--rule-bold); }
  30% { height: var(--rule-heavy); }
  100% { height: var(--rule-bold); }
}

.st__ind-cell.is-tick { animation: st-ind-tick 180ms cubic-bezier(0.25, 0.9, 0.3, 1); }

.st__ind-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  background: var(--accent);
  transition: background-color 170ms ease;
}

.st__ind-cell[data-state="done"] .st__ind-fill { width: 100%; background: var(--st-ind-ink); }

/* Screen steps. Fine enough to read as a rhythm inside a cell and not as a
   division of it — a notch that looked like a boundary would be claiming that
   the thought changes at every screenshot. */
.st__ind-notch {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: var(--rule-hair);
  background: var(--st-ind-track);
}

@media (prefers-reduced-motion: reduce) {
  .st__ind-cell, .st__ind-fill { transition: none; }
  .st__ind-cell.is-tick { animation: none; }
}

/* ── the still version: reduced motion or no JS, at 900px and up ──────────
   Below 900px `.st-mobile` (case-light.css) takes over instead — see the
   comment above that block. This is the plain fallback for everything else:
   a wide window where the mechanic simply did not start. */

.st:not(.is-live) .st__frame { padding: var(--sp-6); }

/* Off the mechanic the intro frame is simply the chapter opening it always was
   underneath, standing above the row instead of lying over it. Nothing is
   dropped — same text, no second copy to keep in sync. */
.st:not(.is-live) .st__intro { margin-bottom: var(--sp-6); }
.st:not(.is-live) .st__intro-word { font-size: var(--fs-h2); }
.st:not(.is-live) .st__intro-body { grid-template-columns: 1fr; gap: var(--sp-5); }
.st:not(.is-live) .st__fig { font-size: var(--fs-h3); }

/* The context frame is a note like the others off the mechanic: the two
   columns stack, the figures keep their rules. */
.st:not(.is-live) .st__ctx-figs { margin-top: var(--sp-5); }

.st:not(.is-live) .st__plane {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sp-4);
}

.st:not(.is-live) .st__cell { width: var(--st-flat-w); }
.st:not(.is-live) .st__seam { flex-basis: 100%; height: var(--rule-hair); background: var(--rule); }

.st:not(.is-live) .st__notes {
  display: grid;
  gap: var(--sp-5);
  max-width: 68ch;
  margin-top: var(--sp-6);
}

.st:not(.is-live) .st__note { max-width: none; }
.st:not(.is-live) .st__hyps { grid-template-columns: 1fr; }

/* Off the mechanic the two travelling layers stop travelling and become what
   they always were underneath: a list of what each screen carries, and a list
   of what each new screen says. Same text, no second copy to keep in sync. */
.st:not(.is-live) .st__marks,
.st:not(.is-live) .st__caps {
  display: grid;
  gap: var(--sp-3);
  max-width: 68ch;
  margin-top: var(--sp-6);
}

.st:not(.is-live) .st__mark,
.st:not(.is-live) .st__cap {
  margin: 0;
  padding-top: var(--sp-2);
  border-top: var(--rule-hair) solid var(--rule-muted);
  font-size: var(--fs-small);
  line-height: var(--lh-text);
}

.st:not(.is-live) .st__mark b { font-family: var(--font-mono); font-size: var(--fs-caption); }

/* Below 900px the scene (live or reflowed) does not render at all — the
   mobile chapter in case-light.css takes its place. */
@media (max-width: 899px) {
  .st { display: none; }
}
