/* ============================================================
   READING RHYTHM — chapters 2 to 6
   Loaded AFTER styles-sidebar.css. Additive only: nothing in this file
   is required for the article to render finished and readable.

   SCOPE. Everything here is keyed to #pv-ch2 .. #pv-ch7 and the two
   figures that live inside them. Chapter 1, .cs-sheet, .cs-statement and
   .cs-shortcut are owned elsewhere and are never selected.

   THE RULE THIS FILE OBEYS
   Every scroll-driven custom property below is registered with an
   initial-value that is the FINISHED state. If rhythm.js never loads,
   fails to parse, or @property is unavailable, every value falls back to
   "arrived" and the page renders complete. Nothing here can hide content:
   opacity is not animated anywhere in this file. The only two rules that
   mention it force it back to 1.

   ON BLUR. Two things resolve out of focus here, a chapter heading and
   the one still image in scope. It is not lifted from caliper.studio —
   what looks like a designed blur there is a low-res placeholder
   resolving behind a lazily loaded asset, and the motion they actually
   author is an opacity fade on an observer. The blur is here because a
   chapter opener needs an arrival, and of the three available a fade can
   fail closed, a translate is the exact thing the owner called boring,
   and a blur is the only one that still shows the whole element at every
   intermediate value. The move that carries meaning on this page is the
   spine below, not the blur; if the blur reads as decoration it is two
   rules and can go on its own.

   It also OVERRIDES two existing rules for the elements it owns.
   .cs-fx sets `opacity: 0` on chapter headings and `opacity: .35` on
   figures and hands the job of undoing it to an IntersectionObserver in
   reading-hero.js. Two reasons to take chapters 2-6 off that:

   1. It fights the scrub. Those rules also carry a 0.6-0.7s transition,
      so every per-frame value written here would be chased rather than
      tracked, and the arrival would lag the scroll by most of a second.
   2. It is the shape of the failure GSAP was removed from this page for:
      content whose visibility is a side effect of an event firing.

   To be straight about the evidence: I did NOT reproduce a real
   user-visible blank heading in a foreground tab. Every screenshot in
   which one appeared was taken in a BACKGROUNDED tab, where rAF and
   IntersectionObserver are both suspended — the same artifact chart.js
   documents, and I walked into it as well. reading-hero.js also
   force-marks anything already on screen at init, which covers the load
   case. So the objection above is about shape and about the transition
   fighting the scrub, not about a bug I caught in the act.

   What is not in doubt is the direction: everything below moves the
   arrival onto transform and filter, so opacity on content is never a
   variable at all and the question cannot come up again.
   ============================================================ */

/* ---- registered properties: initial-value == finished ---- */

/* how far the reader is through this chapter, 0 at its opening line and 1
   at its last. 1 means "read", which is also what a printed column rule
   looks like, so the fallback is a real typographic object rather than an
   unfinished animation. */
@property --ch-read {
  syntax: '<number>';
  /* INHERITS: TRUE, and this is not a style preference. A registered
     property with inherits:false resolves to its initial-value on every
     descendant AND on every pseudo-element, so a value written on the
     chapter would never reach `.pv-chapter::before`, the h2, or an img
     inside a figure — each of them would silently read 1 forever and the
     whole file would look like it was doing nothing. Every property here
     is written on a container and read on something inside it, so every
     one of them inherits. */
  inherits: true;
  initial-value: 1;
}

/* the rule under a result number. 1 = fully drawn. */
@property --stat-r {
  syntax: '<number>';
  inherits: true;
  initial-value: 1;
}

/* ============================================================
   0. REPAIR — chapters 2-7 are never invisible
   ============================================================ */

/* Specificity: an id beats the .cs-fx .cs-body .pv-chapter chain, so these
   win without !important. */
.cs-fx .cs-body #pv-ch2 h2,
.cs-fx .cs-body #pv-ch3 h2,
.cs-fx .cs-body #pv-ch4 h2,
.cs-fx .cs-body #pv-ch5 h2,
.cs-fx .cs-body #pv-ch6 h2,
.cs-fx .cs-body #pv-ch7 h2,
.cs-fx .cs-body #pv-ch2.is-inview h2,
.cs-fx .cs-body #pv-ch3.is-inview h2,
.cs-fx .cs-body #pv-ch4.is-inview h2,
.cs-fx .cs-body #pv-ch5.is-inview h2,
.cs-fx .cs-body #pv-ch6.is-inview h2,
.cs-fx .cs-body #pv-ch7.is-inview h2 {
  opacity: 1;
  /* the scrub writes a value every frame; a transition on top of that only
     adds a frame of lag and fights the scroll */
  transition: none;
  transform: none;
}

.cs-fx .cs-body #pv-ch3 .pv-fig,
.cs-fx .cs-body #pv-ch4 .pv-fig,
.cs-fx .cs-body #pv-ch3 .pv-fig.is-inview,
.cs-fx .cs-body #pv-ch4 .pv-fig.is-inview {
  opacity: 1;
  /* the clip reveal is replaced by the resolve below; settling it here
     means a figure is never clipped by a class that has not arrived */
  clip-path: none;
  transition: none;
}

.cs-fx .cs-body #pv-ch4 .pv-fig img,
.cs-fx .cs-body #pv-ch4 .pv-fig.is-inview img {
  transition: none;
}

/* ============================================================
   1. THE SPINE — the chapter's column rule, drawn as it is read

   A hairline in the 64px gutter between the two prose columns, running
   from under the chapter heading to the foot of the chapter. It is a
   newspaper column rule when it is finished, and it fills downward at
   exactly the rate the reader moves through the chapter, so it is also a
   position readout: how much of this argument is behind you.

   Why this and not another entrance. It is the one thing on the lower half
   of the page whose motion IS information. It reads at a glance, it costs a
   single composited scaleY, and at rest it is a typographic object the
   article would be happy to have anyway.

   [data-spine] is written by rhythm.js after it measures where the heading
   ends. Without JS the pseudo-element never renders and the page is exactly
   as it is today — decoration absent, never content hidden.
   ============================================================ */

.cs-body .pv-chapter[data-spine] { position: relative; }

.cs-body .pv-chapter[data-spine]::before {
  content: '';
  position: absolute;
  /* --spine-top is the measured bottom of the h2 inside this chapter;
     --spine-h is the height from there to the last line of prose. */
  top: var(--spine-top, 0px);
  background: var(--rule-soft);
  pointer-events: none;
}

/* Two columns: the rule lives in the gutter and fills downward. */
.cs-body .pv-chapter[data-spine='col']::before {
  left: 50%;
  width: 1px;
  height: var(--spine-h, 0px);
  margin-left: -0.5px;
  transform: scaleY(var(--ch-read, 1));
  transform-origin: 50% 0;
}

/* One column: there is no gutter to put a rule in, so the same readout
   moves under the heading and runs left to right. Same variable, same
   meaning, and at rest it is a rule under a chapter opener.

   WHICH BRANCH APPLIES IS DECIDED BY MEASUREMENT, NOT BY A BREAKPOINT.
   This started as `@media (max-width: 904px)`, derived from 380 + 64 +
   380 = 824px of content inside the 1060px stage with a 40px gutter. To
   be accurate about it: that number turned out to be RIGHT at every width
   tested — at a 900px viewport the chapter measures 820px and does drop
   to one column, so the query and the measurement agree.

   It is measured anyway for two reasons. 904 is a derived number that
   silently depends on --wrap-gutter and on the 380px column minimum, both
   of which live in a file this one must not edit; change either and the
   query is wrong with no symptom but a rule in the wrong place. And the
   obvious runtime check does not work — `getComputedStyle(ch).columnCount`
   reports "2" even where only one column is being used, which is what
   sent the first pass looking for a bug that was not there. Comparing a
   paragraph's width to the chapter's is unambiguous at every width.

   rhythm.js writes data-spine="col" or "flat" from that comparison. */
.cs-body .pv-chapter[data-spine='flat']::before {
  left: 0;
  right: 0;
  height: 1px;
  transform: scaleX(var(--ch-read, 1));
  transform-origin: 0 50%;
}

/* ============================================================
   2 & 3. THE SCROLL BLURS — REMOVED

   Chapter openers used to resolve from blur(3.5px) and chapter four's
   phone shot from blur(10px), both scrubbed off scroll position. Removed
   at the owner's request: the argument for them was that a blurred thing
   is still visibly there, which made them a safe way to give six chapters
   an arrival — but "safe" is not the same as "earned", and on a page whose
   other motion is a position readout, a lens effect was the one piece that
   read as decoration rather than as information.

   Nothing replaces them. The chapters now arrive with the spine and the
   underlines and nothing else, which is the point: the remaining motion
   all reports where you are, and none of it performs.

   Removed with them: the --ch-in and --fig-r registered properties, their
   two targets in rhythm.js, and their clauses in the reduced-motion block.
   Chapter four's figure keeps the --plx parallax it already had from
   styles-sidebar.css, which owns that transform again now that nothing
   here is overriding it.
   ============================================================ */

/* A sticky caption was tried here and REMOVED. See rhythm.NOTES.md:
   `position: sticky; bottom: 28px` does hold the caption in view while a
   tall figure passes, but both figures in scope have captions that sit
   INSIDE the media's own horizontal footprint, so the held caption lands
   on top of the picture it is describing. Screenshotted at 1655px on
   chapter 4: the caption sat across the middle of the phone shot and both
   were unreadable. The idea needs a caption set in a side column, which
   is a markup change, not a motion one. */

/* ============================================================
   4. THE RESULT NUMBERS UNDERLINE THEMSELVES

   Chapter 4 is the payoff, and its two load-bearing figures are already
   marked up as <strong>. A hairline draws under each one as it crosses
   the reading line — the smallest possible version of the chart drawing
   itself, applied to prose. Finished state is a plain rule under a
   statistic, which is what a printed report would do anyway.
   ============================================================ */

.cs-body #pv-ch2 p strong,
.cs-body #pv-ch3 p strong,
.cs-body #pv-ch4 p strong,
.cs-body #pv-ch5 p strong,
.cs-body #pv-ch6 p strong,
.cs-body #pv-ch7 p strong {
  position: relative;
}

.cs-body #pv-ch2 p strong::after,
.cs-body #pv-ch3 p strong::after,
.cs-body #pv-ch4 p strong::after,
.cs-body #pv-ch5 p strong::after,
.cs-body #pv-ch6 p strong::after,
.cs-body #pv-ch7 p strong::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 1.5px;
  background: currentColor;
  opacity: 0.3;
  transform: scaleX(var(--stat-r, 1));
  transform-origin: 0 50%;
  pointer-events: none;
}

/* ============================================================
   5. REDUCED MOTION

   rhythm.js returns before it registers a single listener when reduced
   motion is asked for, but it still runs the layout pass — so the spine
   renders at its initial-value of 1, which is the finished column rule,
   and none of the [data-rz-*] attributes are ever set. That is the whole
   degradation: the static finished layout, with the rule present and
   nothing moving. Never a hidden one.

   The rules below are the belt to that braces, in case an attribute
   survives a cached document or a media query flips mid-session.
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  .cs-body .pv-chapter[data-spine]::before { transform: none; }

  .cs-body #pv-ch2 p strong::after,
  .cs-body #pv-ch3 p strong::after,
  .cs-body #pv-ch4 p strong::after,
  .cs-body #pv-ch5 p strong::after,
  .cs-body #pv-ch6 p strong::after,
.cs-body #pv-ch7 p strong::after { transform: none; }
}
