/* ============================================================
   STEPS — the twenty-one step sign-up sheet (figure.pv-fig.cs-sheet)
   Loaded only by /raf/. Paired with raf/motion/steps.js.

   WHAT THIS REPLACES
   A flat six-column grid with CSS-drawn serpentine connector arrows. The
   section's argument is that twenty-one steps is too many, and a tidy grid
   states that rather than making you feel it. This turns the figure into a
   full-bleed staggered mosaic — the whole product at once, running edge to
   edge on a page where every other element is 760px wide — so the mass is the
   first thing that lands, before the number is read.

   THE SAFETY CONTRACT (this is the important part of this file)
   Nothing here can make a plate invisible.
     * Every scroll-driven value is a REGISTERED @property whose initial-value
       is the FINISHED state: --sheet-p: 1 (all resolved), --sheet-d: 0 (no
       drift), --p: 1 (this plate resolved). If steps.js never loads, never
       parses, or throws, the sheet renders finished and correct.
     * Every var() that a transform depends on is written with an explicit
       fallback, so a browser without @property support ALSO lands on the
       finished state rather than dropping the declaration.
     * The one filter (blur) lives behind the .is-scrub class, which only
       steps.js adds, and only after it has checked CSS.registerProperty and
       prefers-reduced-motion. No class, no filter, sharp plates.
     * The static column stagger is plain CSS and does not depend on scroll at
       all. Scroll only adds a small drift on top of it.
   No opacity is animated anywhere on a plate. The only opacity-free rule that
   moves is the hairline under the sticky head, which is decoration.

   WHY THE HEAD IS STICKY
   The mosaic is roughly one and a half viewports tall, so a head that scrolls
   away takes the claim and the counter with it — and the counter reaching 21
   at the same moment the last plate resolves is the whole point. Sticky keeps
   the claim beside the evidence for the entire descent, which is also what the
   count is measuring. The title is set a step smaller here than the sibling
   "Twenty-one steps became four" head so the sticky rail stays a rail.
   ============================================================ */

/* ---- the three scroll-driven numbers, all initialised FINISHED ---- */

@property --sheet-p {
  syntax: '<number>';
  inherits: true;
  initial-value: 1;   /* 1 = the reader has travelled the whole sheet */
}

@property --sheet-d {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;   /* 0 = columns sit at their designed offsets, no drift */
}

/* Per-plate progress, derived from --sheet-p and the plate's own index. A
   registered <number> so the clamp() resolves to a plain number children can
   multiply. If registration fails the token stream is substituted literally
   instead, which is STILL valid inside the calc()s below — clamp() of unitless
   numbers is legal there — so the degradation is graceful either way. */
@property --p {
  syntax: '<number>';
  inherits: true;
  initial-value: 1;
}

/* ---- the figure: full bleed ---- */

/* .cs-body .pv-fig already bleeds by --cs-bleed (200px at >=1280). This takes
   it the rest of the way to the viewport edges, matching the formula the page
   already uses for its other full-bleed bands (.cs-scene, .cs-next), so the
   scrollbar arithmetic behaves the same way it does for those. */
.cs-body .pv-fig.cs-sheet {
  --sheet-over: 0px;   /* see NOTES: dial this up to crop the outer columns */
  width: auto;
  margin-inline: calc(50% - 50vw);
  margin-block: clamp(72px, 8vw, 150px);
}

/* The reading layer (.cs-fx, set by reading-hero.js) does three things to a
   .pv-fig that all fight this figure, so all three are switched off for it:

     clip-path: inset(7% 4%)  — a clip-path ancestor clips position: sticky
                                descendants, which would kill the sticky head.
     opacity: 0.35 -> 1       — a content opacity that depends on an
                                IntersectionObserver callback. Removing it
                                makes this section STRICTLY safer than it is
                                today, which is the point of the rule.
     transform on :first-child — reading-hero writes --plx into a translate on
                                the figure's first child, which here is the
                                head. A per-frame transform on the element that
                                is trying to be sticky. */
.cs-fx .cs-body .pv-fig.cs-sheet {
  clip-path: none;
  opacity: 1;
  transition: none;
}

.cs-fx .cs-body .pv-fig.cs-sheet > :first-child { transform: none; }

/* .cs-fx also holds every .pv-fig img at scale(1.05) until is-inview lands.
   Neutralised here; the .is-scrub rule further down has higher specificity and
   takes over when steps.js is running. */
.cs-fx .cs-body .pv-fig.cs-sheet .pv-flow-step img {
  transform: none;
  transition: none;
}

/* ---- the sticky head ---- */

.cs-body .pv-fig.cs-sheet .cs-shortcut-head {
  /* top: 0 under the site nav, which is itself sticky at top: 0 with z-index
     100 and hides itself (translateY(-100%)) the moment you scroll down. So
     while you are descending the mosaic — the only direction that matters here
     — the head has the top of the screen to itself; on the way back up the nav
     reappears over it. .pv-media already sticks at top: 0 the same way, so this
     matches how the rest of the page behaves rather than inventing an offset. */
  position: -webkit-sticky;
  position: sticky;
  top: 0;
  z-index: 3;
  /* An opaque band: the plates pass underneath it, so it cannot be
     transparent. Painted with the page ground so it reads as the page holding
     still rather than as a bar. */
  background: var(--bg-cream);
  padding-block: clamp(11px, 1.2vw, 18px);
  margin-bottom: clamp(24px, 3vw, 48px);
  border-bottom: 1px solid var(--rule-soft);
  align-items: baseline;
}

/* A step down from the 60px sibling head. A sticky rail that is 110px tall
   eats a seventh of the screen for the entire descent; at this size it is
   about 70px at 1440 and still reads as display type. */
.cs-body .pv-fig.cs-sheet .cs-shortcut-title {
  font-size: clamp(20px, 2.1vw, 32px);
  max-width: 30ch;
}

.cs-body .pv-fig.cs-sheet .cs-shortcut-count {
  font-size: clamp(22px, 2.2vw, 34px);
  font-variant-numeric: tabular-nums;
  /* the digits are rewritten every frame by steps.js; tabular-nums plus a
     fixed two-digit pad is what stops the note beside it shifting */
}

/* THE DRAWN RULE — the one thing on this figure that visibly "draws" with the
   scroll, in the same spirit as the decline chart above. It is decoration: a
   1px line. Its resting transform is scaleX(1), i.e. a full-width rule, so
   with steps.js absent the head simply carries an underline. */
.cs-body .pv-fig.cs-sheet .cs-shortcut-head::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: var(--text-primary);
  transform: scaleX(var(--sheet-p, 1));
  transform-origin: 0 50%;
  pointer-events: none;
}

/* ---- the mosaic ---- */

.cs-body .pv-fig.cs-sheet .pv-flow {
  /* tuning surface — everything below is derived from these six numbers */
  --sheet-gap: clamp(9px, 1.2vw, 24px);
  --sheet-stag: clamp(16px, 3.2vw, 62px);     /* one unit of column offset */
  --sheet-drift: clamp(7px, 1vw, 22px);       /* peak scroll drift per column */
  --sheet-span: 0.2;                          /* how much of the descent one plate takes */
  --sheet-lead: 0.036;                        /* how far apart two plates start */
  --sheet-maxstag: 2.8;                       /* largest --sheet-sh in play */

  display: grid;
  gap: var(--sheet-gap);
  /* flush to both viewport edges: the bleed IS the composition. --sheet-over
     pushes the grid further out and the clip below crops the outer columns,
     which is closer to the reference but loses part of steps 1/7/8/14/15/21.
     Left at 0. */
  margin-inline: calc(-1 * var(--sheet-over));
  padding-inline: 0;
  /* the stagger is a transform, so it does not reserve its own space; this is
     the room the lowest column and the peak drift need not to collide with
     the caption below */
  padding-top: var(--sheet-drift);
  padding-bottom: calc(var(--sheet-maxstag) * var(--sheet-stag) + var(--sheet-drift) + 8px);
  /* overflow-x rather than overflow: the vertical box must stay open for the
     drop shadows. `clip` rather than `hidden` because `hidden` would make this
     a scroll container and re-parent the sticky head's scrollport. */
  overflow-x: clip;
}

/* --- per-column offsets, seven across --- */
.cs-body .pv-fig.cs-sheet .pv-flow { grid-template-columns: repeat(7, minmax(0, 1fr)); --sheet-maxstag: 2.8; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(7n+1) { --sheet-sh: 0; --sheet-dr: 1; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(7n+2) { --sheet-sh: 2.1; --sheet-dr: -0.6; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(7n+3) { --sheet-sh: 0.8; --sheet-dr: 0.75; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(7n+4) { --sheet-sh: 2.8; --sheet-dr: -1; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(7n+5) { --sheet-sh: 0.4; --sheet-dr: 0.5; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(7n+6) { --sheet-sh: 1.9; --sheet-dr: -0.85; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(7n) { --sheet-sh: 1.2; --sheet-dr: 0.3; }

/* --- per-plate index, 0..20, the only thing that sets reading order now --- */
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(1) { --sheet-i: 0; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(2) { --sheet-i: 1; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(3) { --sheet-i: 2; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(4) { --sheet-i: 3; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(5) { --sheet-i: 4; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(6) { --sheet-i: 5; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(7) { --sheet-i: 6; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(8) { --sheet-i: 7; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(9) { --sheet-i: 8; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(10) { --sheet-i: 9; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(11) { --sheet-i: 10; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(12) { --sheet-i: 11; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(13) { --sheet-i: 12; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(14) { --sheet-i: 13; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(15) { --sheet-i: 14; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(16) { --sheet-i: 15; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(17) { --sheet-i: 16; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(18) { --sheet-i: 17; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(19) { --sheet-i: 18; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(20) { --sheet-i: 19; }
.cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(21) { --sheet-i: 20; }

/* This plate's own progress. Written so that at --sheet-p: 1 (the initial
   value) even the last plate — index 20, phase 0.72 — resolves to 1. */
.cs-body .pv-fig.cs-sheet .pv-flow-step {
  --p: clamp(
    0,
    (var(--sheet-p, 1) - var(--sheet-i, 0) * var(--sheet-lead, 0.036)) / var(--sheet-span, 0.2),
    1
  );
  position: relative;
  margin: 0;
  /* The column offset (static) plus the scroll drift (0 at rest). translate3d
     rather than translateY so this is a compositor transform from the first
     frame instead of being promoted mid-scroll. */
  transform: translate3d(
    0,
    calc(
      var(--sheet-sh, 0) * var(--sheet-stag)
      + var(--sheet-dr, 0) * var(--sheet-d, 0) * var(--sheet-drift)
    ),
    0
  );
}

/* --- device depth --- */

.cs-body .pv-fig.cs-sheet .pv-flow-step img {
  display: block;
  width: 100%;
  height: auto;
  border: 0;
  aspect-ratio: 380 / 826;
  object-fit: cover;
  cursor: zoom-in;
  border-radius: clamp(7px, 0.85vw, 16px);
  /* a hairline edge, a contact shadow, and a cast shadow: three layers,
     because one blurred shadow reads as a sticker and the reference's plates
     read as objects sitting on a surface */
  box-shadow:
    0 0 0 1px var(--rule-soft),
    0 1px 2px rgba(0, 0, 0, 0.05),
    0 6px 14px -6px rgba(0, 0, 0, 0.15),
    0 22px 44px -22px rgba(0, 0, 0, 0.22);
}

/* On #151514 a black shadow is invisible, so depth there is carried by the
   ring and a single deep cast. */
.dark-mode .cs-body .pv-fig.cs-sheet .pv-flow-step img {
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.1),
    0 2px 4px rgba(0, 0, 0, 0.5),
    0 24px 46px -24px rgba(0, 0, 0, 0.75);
}

/* --- THE RESOLVE: rise-to-place, behind .is-scrub ---

   This was blur-to-sharp and the blur was REMOVED at the owner's request,
   along with the chapter-opener and figure blurs in rhythm.css. The plate
   still rises 24px and settles from 97% scale, which is the arrival; what
   is gone is the lens effect on top of it. Both remaining terms are
   compositor-only, so .is-scrub no longer gates a repaint — it is kept
   because it is still what makes the no-JS state pixel-exact. */

/* Only steps.js adds .is-scrub, and only after it has confirmed
   CSS.registerProperty and that the reader has not asked for reduced motion.
   steps.js also REMOVES it once --sheet-p reaches 1, so a fully-read sheet
   carries no filter at all and costs nothing to repaint. With the class
   absent, none of this exists: no filter, no transform, sharp plates. */
.cs-body .pv-fig.cs-sheet.is-scrub .pv-flow-step img {
  transform:
    translate3d(0, calc((1 - var(--p, 1)) * 24px), 0)
    scale(calc(1 - (1 - var(--p, 1)) * 0.03));
  transform-origin: 50% 30%;
}

/* The number badge is the reading order now that the arrows are gone, so it
   stays sharp while the screen behind it settles. */
.cs-body .pv-fig.cs-sheet .pv-flow-step::before {
  top: 5px;
  left: 5px;
  padding: 3px 5px;
  font-size: 9px;
  border-radius: 2px;
}

/* The caption is now inside a full-bleed figure, so its own 28px padding put
   it at x=28 while the head's text started at x=100. Same formula as the head,
   so the two line up on the 1240px measure while the plates run edge to edge. */
.cs-body .pv-fig.cs-sheet .pv-fig-cap {
  padding-inline: max(var(--wrap-gutter, 24px), calc((100% - 1240px) / 2));
  /* the shared caption rule caps the box at 68ch. With border-box sizing
     that cap included the padding above, and on a wide window the text
     was left with a 100px column. Content-box keeps the measure for the
     words and lets the padding sit outside it. */
  box-sizing: content-box;
}

/* --- the old serpentine connectors, retired ---
   They encode a six-column grid in nth-child(6n) rules and draw an arrow
   across the column gap and a wrap back to the left edge. Neither survives a
   staggered mosaic: the plate an arrow points at is no longer level with it.
   !important because the existing rules are more specific than anything this
   file can write without editing them (see NOTES — this is the one place a
   markup or shared-file change would be cleaner). */
.cs-body .pv-fig.cs-sheet .pv-flow-step::after { content: none !important; }
.cs-body .pv-fig.cs-sheet .pv-flow-turn { display: none !important; }

/* ============================================================
   BREAKPOINTS
   Seven across is the desktop shape: 21 = 3 x 7 exactly, so the mosaic is
   three full rows with no orphan. Below that the count drops to keep a plate
   above ~115px, which is where a phone screenshot stops reading as a screen.
   A ragged final row does not matter here — the columns are already staggered,
   so the bottom edge is ragged by construction.
   ============================================================ */

@media (max-width: 1179px) {
  .cs-body .pv-fig.cs-sheet .pv-flow { grid-template-columns: repeat(6, minmax(0, 1fr)); --sheet-maxstag: 2.6; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(6n+1) { --sheet-sh: 0; --sheet-dr: 1; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(6n+2) { --sheet-sh: 2.2; --sheet-dr: -0.65; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(6n+3) { --sheet-sh: 0.9; --sheet-dr: 0.8; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(6n+4) { --sheet-sh: 2.6; --sheet-dr: -1; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(6n+5) { --sheet-sh: 0.5; --sheet-dr: 0.45; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(6n) { --sheet-sh: 1.7; --sheet-dr: -0.8; }
}

/* 900 rather than 899: the page's own head rule stacks .cs-shortcut-head into
   two rows at exactly max-width: 900px, and a stacked head measured 137px of an
   800px screen while still sticky. The two breakpoints have to be the same
   number or the widest stacked head is also the tallest sticky one. */
@media (max-width: 900px) {
  .cs-body .pv-fig.cs-sheet .pv-flow { grid-template-columns: repeat(5, minmax(0, 1fr)); --sheet-maxstag: 2.4; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(5n+1) { --sheet-sh: 0; --sheet-dr: 0.9; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(5n+2) { --sheet-sh: 2.0; --sheet-dr: -0.7; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(5n+3) { --sheet-sh: 0.7; --sheet-dr: 1; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(5n+4) { --sheet-sh: 2.4; --sheet-dr: -0.5; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(5n) { --sheet-sh: 1.1; --sheet-dr: 0.6; }

  /* Below 900 the page's own --cs-bleed is 0 and the article is the full
     width of the screen anyway, so the mosaic keeps a gutter rather than
     running under the phone's rounded corners. */
  .cs-body .pv-fig.cs-sheet .pv-flow { padding-inline: var(--wrap-gutter, 20px); }

  /* A sticky rail on a short screen costs too much of the viewport, and at
     this width the page stacks the head (title over count) so it is twice as
     tall: measured 137px of an 800px screen. Static from here down. */
  .cs-body .pv-fig.cs-sheet .cs-shortcut-head {
    position: static;
    padding-block: 0 clamp(10px, 1.6vw, 18px);
  }
}

@media (max-width: 619px) {
  .cs-body .pv-fig.cs-sheet .pv-flow { grid-template-columns: repeat(3, minmax(0, 1fr)); --sheet-maxstag: 1.6; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(3n+1) { --sheet-sh: 0; --sheet-dr: 0.8; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(3n+2) { --sheet-sh: 1.6; --sheet-dr: -1; }
  .cs-body .pv-fig.cs-sheet .pv-flow-step:nth-child(3n) { --sheet-sh: 0.7; --sheet-dr: 0.5; }

  .cs-body .pv-fig.cs-sheet .pv-flow {
    --sheet-gap: 8px;
    --sheet-stag: 16px;
    --sheet-drift: 6px;
    padding-inline: 14px;
  }

  .cs-body .pv-fig.cs-sheet .pv-flow-step::before {
    font-size: 8px;
    padding: 2px 4px;
  }
}

/* ============================================================
   REDUCED MOTION
   Degrades to the finished layout, never to a hidden one: the staggered
   mosaic, every plate sharp and in place, the rule drawn full width, the
   count reading 21. steps.js returns before it adds .is-scrub or writes a
   single property, so this block is belt-and-braces against the class being
   left on the element by anything else.
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  .cs-body .pv-fig.cs-sheet.is-scrub .pv-flow-step img {
    filter: none;
    transform: none;
  }

  .cs-body .pv-fig.cs-sheet .pv-flow-step {
    transform: translate3d(0, calc(var(--sheet-sh, 0) * var(--sheet-stag)), 0);
  }

  .cs-body .pv-fig.cs-sheet .cs-shortcut-head::after { transform: none; }
}
