/* =============================================================================
   blocks.css — per-block layout, PHASE 1: B0 · B1 · B2 · B3.
   Phase 2 appends B4–B9 below the marker at the bottom of this file.

   Convention: every selector is namespaced by its block (`.b0-…`, `.b1-…`).
   Anything reused across blocks belongs in main.css, not here.
   Contract: brief/SPEC.md §8. B0/B1 seeded from qa/mock-A.
   ========================================================================= */


/* ============================================================
   B0 — ТИТУЛЬНЫЙ ЭКРАН · «cinematic diptych» (DECISIONS, mock-B)

   Two portraits as one full-height background, SPLIT 50/50 (the client's
   «почему бы не сделать просто 50 на 50», 2026-09-08) and dissolving into each
   other over an 8vw feather centred on the middle. Both graded to one exposure,
   eye lines matched, faces dissolving into a bottom grade + a left→right
   vignette that starts exactly where the headline does. Zero accent objects
   except the eyebrow hairline.

   FACE ANCHORING — the trick that keeps the two eye lines locked at every
   viewport, and since round 2 it is the SAME trick for all ten frames. Each
   photograph names a face point in its own source (`--fx`, `--fy`) and the
   panel names where that point has to land (`--lx`, `--ly`); the image is
   sized by `--fh` (a multiple of the panel height) with a floor that
   guarantees it still covers the panel, and translated by exactly its own
   face fraction. Changing `--fh` zooms around the face and nothing drifts.
   Round 1 had Виктория on `object-fit: cover` and Арут on a bare height
   multiple; `cover` reacts to the panel's aspect, and the 50/50 split changes
   that aspect, so both are now on the anchor system.
   ============================================================ */

.b0 {
  position: relative;
  min-height: 100vh;            /* fallback: dvh is unsupported < Safari 15.4 */
  min-height: 100dvh;
  overflow: hidden;
  isolation: isolate;
  display: block;
  /* D1 — same landscape-notch guard as `.blk` (main.css §3) */
  padding: 0 max(var(--gutter), env(safe-area-inset-right, 0px))
           0 max(var(--gutter), env(safe-area-inset-left, 0px));

  /* Арут — face anchor. `--a-h` is his rendered height as a share of the panel
     height; the top of his head sits ~7% of the panel below the top edge and
     the source is upscaled ~1.9× — the ceiling before an 864px-wide frame turns
     to mush.
     ROUND 2: the numbers dropped from 229/210/245 % to 151/139/162 % because
     the SOURCE was cropped, not because the picture changed. `arut/02.jpg` is
     now exported as its visible band only (y 0.30–0.96 of the original, see
     qa/b0-cinema/export.py), so the same composition needs 0.66 of the old
     height. 229 × 0.66 = 151.1. */
  --a-h:  151%;
  --a-lx: 58%;
  --a-ly: 19%;

  /* THE JOIN. The two panels dissolve into each other between `--seam-a` and
     `--seam-b` of the viewport and cross at exactly 50 % — the client's
     «просто 50 на 50». See `.b0-panel--arut` for how the ramp is built and why
     Виктория's panel has to run past the middle for it to work at all. */
  --seam-a: 46%;
  --seam-b: 54%;
}

/* --- portrait layer (z 0): its own stacking context, always under the copy --- */
.b0-portraits { position: absolute; inset: 0; z-index: 0; pointer-events: none; }

/* One shared hue over both frames - a studio portrait and a night rooftop shot
   read as one grade only if they share a colour cast.

   History, because the construction has moved twice and both moves were forced
   by measurement. A12 made it a SIBLING of `.b0-portraits` rather than
   `.b0-portraits::after`, so the scroll parallax was not scrubbing a blend
   group. The B0 cinema round moves it again, INSIDE each frame:

   a `mix-blend-mode` layer has to read its backdrop, and the photo cycle now
   changes that backdrop continuously, so a full-viewport cast re-blended the
   whole viewport on every frame of every cross-dissolve. Measured at 1440x900,
   12 s idle at scrollY 0 with the cycle and the wave running: 22.9 % of frames
   over 32 ms with the cast outside, 2.1 % with it inside - it was 20.8 of the
   22.1 points (qa/phase12/ablate.json, castfix.json). Same lesson as the noise
   overlay in the perf round.

   Inside the frame the blend group is a STATIC picture: the compositor
   rasterises it once and the cycle only changes its opacity and scale.
   `isolation: isolate` keeps the blend from reaching past its own frame.

   One measured side effect, deliberately accepted: the old full-viewport blend
   layer forced Blink to render every glyph above it with grayscale
   antialiasing. Without it the B0 captions get subpixel antialiasing - which is
   what the rest of the dark page has had all along (measured colour spread on
   `.b4-item-t` 189 and `.tile-w` 176 against `.b0-name-r` 8 - qa/phase12/aa.py).
   B0 was the odd one out, not the new normal. */
.b0-fr { isolation: isolate; }
.b0-fr::after {
  content: "";
  position: absolute; inset: 0;
  background: rgba(20,30,44,.16);
  mix-blend-mode: color;
  pointer-events: none;
}

.b0-panel { position: absolute; top: 0; bottom: 0; overflow: hidden; }

/* ------------------------------------------------------------------
   THE DISSOLVE AT THE JOIN (ROUND 2).

   Round 1 faded HER panel out over a black band and let his opaque panel start
   6 % earlier. That is not a dissolve — the two pictures never overlap, the
   handover happens over the stage, and `.b0-seam` had to paint an 0.84-alpha
   scrim to hide it. The result was the dark vertical column the client asked
   to get rid of («переход, чтобы был менее заметным, более качественным»).

   Now the RIGHT panel is the one that fades, and it fades over HER picture:
   Арут is later in the DOM, so he paints on top; his mask ramps 0 → 1 across
   [--seam-a, --seam-b] and Виктория is fully opaque underneath the whole ramp.
   Composite alpha is therefore 1 at every column — the profile can only move
   monotonically from her exposure to his, and there is nothing left for a
   scrim to hide. `.b0-seam` is gone.

   The cost, and it is unavoidable: for the crossing point to be at 50 %, her
   picture has to EXIST at 54 %. Her panel is 55 % wide (1 % past the ramp's
   end, so its hard right edge is under fully-opaque Арут), and a 0.75-aspect
   photograph covering a 55vw × 100vh box has to be scaled to the box's WIDTH
   instead of its height from ~47vw up. Her face therefore renders ~20 % larger
   at 1440×900 than it did at a 46 % panel. That is arithmetic, not a choice:
   on a width-limited cover the face measures panelWidth × faceHeight/srcWidth,
   so it scales with the panel. It also renders her 848px source at 792 CSS px
   instead of 675 — closer to 1:1, i.e. sharper.

   The ramp is a smoothstep, not a straight line: zero slope at both ends means
   neither the start nor the end of the dissolve has a slope discontinuity to
   read as an edge. Stops are in panel-%, i.e. (viewport% − 46) / 54.
   ------------------------------------------------------------------ */
.b0-panel--vika { left: 0; width: 55%; }
.b0-panel--arut {
  left: var(--seam-a); right: 0;
  -webkit-mask-image: linear-gradient(to right,
    rgba(0,0,0,0) 0%, rgba(0,0,0,.033) 1.85%, rgba(0,0,0,.156) 3.70%,
    rgba(0,0,0,.316) 5.56%, rgba(0,0,0,.500) 7.41%, rgba(0,0,0,.684) 9.26%,
    rgba(0,0,0,.844) 11.11%, rgba(0,0,0,.967) 12.96%, #000 14.82%);
          mask-image: linear-gradient(to right,
    rgba(0,0,0,0) 0%, rgba(0,0,0,.033) 1.85%, rgba(0,0,0,.156) 3.70%,
    rgba(0,0,0,.316) 5.56%, rgba(0,0,0,.500) 7.41%, rgba(0,0,0,.684) 9.26%,
    rgba(0,0,0,.844) 11.11%, rgba(0,0,0,.967) 12.96%, #000 14.82%);
}

/* Per-panel exposure grade. It is transparent exactly at the eye line (~20% of
   the panel) and closes down above and below it, which is what lets a studio
   portrait and a night rooftop frame read as one shot: it kills the bright sky
   of his frame and the specular white of his t-shirt without touching his face,
   and it takes the brushed-metal highlights out of hers. */
.b0-panel::after {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(14,15,18,.40) 0%,
    rgba(14,15,18,.05) 20%,
    rgba(14,15,18,.34) 35%,
    rgba(14,15,18,.60) 50%,
    rgba(14,15,18,.78) 64%,
    rgba(14,15,18,.94) 78%,
    var(--bg) 92%);
}

/* `.por` is the element the intro timeline fades and scales — it must not clip,
   or the 1.04 scale would drag the panel edge across the seam */
.por { position: absolute; inset: 0; margin: 0; }

.b0-photo { position: absolute; max-width: none; }

/* THE PANEL GRADE IS BAKED INTO THE FILES (round 2), and `.b0-photo--v/--a` are
   now markers only — the values that used to live here,
     Виктория  brightness(.58) saturate(.5) contrast(1.14)
     Арут      brightness(.53) saturate(.5) contrast(1.10)
   are applied by qa/b0-cinema/export.py as the last step before the webp is
   written. Reason, measured: a CSS filter is re-applied on every raster, and
   round 2 animates a scale on the element that carried it, so the idle cycle
   dropped ~4 % of its frames inside every arrival (60 of 1441 frames over 32 ms
   in 25 s at 1440). With the filter gone and nothing else changed: 0 of 1501.
   The grade is affine and alpha compositing is a convex combination, so baking
   it cannot change the cross-dissolve — and the rendered page was diffed before
   and after to prove it (qa/phase14/gradechk.json). It also cut the ten frames
   from 916.6 KB to 482.5 KB, because a dark low-contrast picture is cheaper to
   encode than the bright one webp was being handed. */

/* ------------------------------------------------------------------
   THE FRAME STACK — FIVE frames per side, and the list order IS the cycle
   order (client's pick, USER-PICKS.md 19:45; the pairing chain is verified on
   the rendered page by qa/phase14/pairs.py):
     left  Виктория  30 → 15 → 29 → 17 → 27
     right Арут      02 → 27 → 01 → 24 → 35

   ROUND 2 puts frame 1 on the SAME anchor system as every other frame. Round 1
   left Виктория on `object-fit: cover` + `object-position` and Арут on a bare
   `min(--a-h, 2080px)`, which was fine while the panels never moved; the 50/50
   split moves them, and `cover` reacts to a panel's aspect while the anchor
   system does not. One system also makes the crop scale comparable across
   frames, which is what the client asked for («чтобы более-менее всё
   сочеталось»). `.b0-photo--v/--a` are markers only now — the grade they used
   to carry is baked into the files (see above).

   Every frame is placed by six numbers, so swapping one is a one-line change:
     --asp  source aspect (w/h)          --fx/--fy  the face point IN the source
     --fh   rendered height, % of panel  --lx/--ly  where that point lands IN
                                                    the panel
     --wk   how much wider than the panel the frame has to be, because the face
            is anchored OFF centre: max(lx/fx, (1-lx)/(1-fx))
   `height` takes the LARGEST of --fh, the panel height, and the height needed
   to cover the panel width at that anchor, so a frame can never uncover the
   panel at any viewport aspect; --cap then limits the upscale. --fh is the same
   rule on the other axis, max(ly/fy, (1-ly)/(1-fy)), and is viewport-free
   because both terms are fractions of the panel height.
   ------------------------------------------------------------------ */
.b0-panel--vika { --panel-w: 55vw; }
.b0-panel--arut { --panel-w: 54vw; }

.b0-fr {
  position: absolute;
  inset: 0;
  /* the arrival push-in happens around the face, not the panel centre */
  transform-origin: var(--lx, 50%) var(--ly, 50%);
}

/* Where the anchor point lands. Арут's frames all follow `--a-lx`/`--a-ly`, so
   his drift across the four breakpoints is 0 by construction. Виктория's
   anchor is 42 % / 19 %: 45 %, which the first cut used, put every frame's eye
   3.1 % of the panel (20 px at 1440) to the right of frame 1's at EVERY width;
   42 % measures 0.1 % (qa/phase13/anchor.json, re-measured in phase14). */
.b0-panel--vika .b0-fr { --lx: 42%; --ly: 19%; }
.b0-panel--arut .b0-fr { --lx: var(--a-lx); --ly: var(--a-ly); }

.b0-fr .b0-photo {
  inset: auto;
  left: var(--lx);
  top:  var(--ly);
  width: auto;
  /* --cover-pad: the width term is a geometric FLOOR — at exactly 1.00 the
     frame is flush with the panel and nothing absorbs sub-pixel rounding.
     Виктория's frame 1 is the proof: its anchor sits on the panel's own
     (42 %, 19 %), so --wk is exactly 1 and at 1440 the rendered width came out
     791.984 px in a 792 px panel — 0.024 px of bare stage on the right
     (measured, qa/phase14/b0dip.json before this line existed). 0.6 % is
     ~4.7 px at 1440, invisible in the crop and enough at every width. */
  height: min(
            max(var(--fh), 100%,
                calc(var(--panel-w) / var(--asp) * var(--wk, 1) * var(--cover-pad, 1.006))),
            var(--cap));
  object-fit: fill;
  transform: translate(calc(-1 * var(--fx)), calc(-1 * var(--fy)));
}

/* Виктория. `--fh` and `--wk` follow the rule above: fh = max(ly/fy,
   (1-ly)/(1-fy)), wk = max(lx/fx, (1-lx)/(1-fx)), rounded up. Frame 1's anchor
   sits exactly on the panel's (42 %, 19 %), so its wk is 1.00 and its fh is
   103 % — 3 points over the 100 % minimum, because at 100 % the frame is flush
   with the panel on both edges and a sub-pixel rounding could uncover it. */
.b0-panel--vika .b0-fr--1 .b0-photo { --asp: .75044; --fx: 42%; --fy: 19%; --fh: 103%; --wk: 1.00; --cap: 1600px; }
/* 2 — ig 15, the night flash frame, and the one place where `--fh` is a
   picture decision rather than a coverage floor. The floor is 140 %; at 143 %
   (round 1) the panel measured 13.2 L against 21-35 L on every other left frame
   and read as an empty black half with a small figure in it
   (qa/phase14/chk-v2.png). 190 % crops to her upper body: the panel comes up to
   15.8 L, her face lands nearer the rest of the chain, and the crown of her
   head is still 0.025 of the source inside the top edge at every viewport (band
   top 0.306-0.320 against a crown at 0.345). */
.b0-panel--vika .b0-fr--2 .b0-photo { --asp: .7508;  --fx: 38%; --fy: 42%; --fh: 190%; --wk: 1.11; --cap: 2000px; }
.b0-panel--vika .b0-fr--3 .b0-photo { --asp: .75;    --fx: 50%; --fy: 31%; --fh: 118%; --wk: 1.17; --cap: 2300px; }
/* 4 — ig 29, the tight close-up. Her head cannot be brought down to frame 1's
   scale: 1.14 is the SMALLEST --wk that still covers the panel at this anchor,
   and at that height her eye-to-chin already measures 2.5x frame 1's. It is a
   closer shot in the sequence and the eye line is what stays locked. This is
   why the chain puts it next to Арут's two closest frames (ig 24 and ig 35). */
.b0-panel--vika .b0-fr--4 .b0-photo { --asp: .7504;  --fx: 37%; --fy: 22%; --fh: 105%; --wk: 1.14; --cap: 1600px; }
.b0-panel--vika .b0-fr--5 .b0-photo { --asp: .75;    --fx: 40%; --fy: 24%; --fh: 107%; --wk: 1.06; --cap: 2300px; }

/* Арут. Frame 1 is the rooftop, and its source is now the CROPPED export
   (864x723, y 0.30-0.96 of the original), so --asp, --fy and --cap are all the
   old numbers times 0.66 on the vertical: 45.2 % → 23.03 %, 2080px → 1373px,
   and --a-h carries the same factor (see `.b0` above). --wk 1.43 is the worst
   of the four `--a-lx` breakpoints (0.46 / 0.50 / 0.58 / 0.60), so the cover
   holds at every width without a per-breakpoint value. */
.b0-panel--arut .b0-fr--1 .b0-photo { --asp: 1.19502; --fx: 62.15%; --fy: 23.03%; --fh: var(--a-h); --wk: 1.43; --cap: 1373px; }
/* 2 — ig 01, the night boat. 165 %, not the 132 % the first cut used: the zoom
   is anchored on his face, so raising it pushes the lit superstructure beside
   him off the panel. Measured across the join, with the export shoulder in
   place: 3.20 L at 132 %, 1.93 L at 165 % (qa/phase12/tune-a2.json). */
.b0-panel--arut .b0-fr--2 .b0-photo { --asp: .75;     --fx: 57%;    --fy: 21%;    --fh: 165%; --wk: 1.26; --cap: 2600px; }
/* 3 and 4 are the two daylight frames, and both are wide environmental shots
   with his face at ~36 % of the source width. His anchor is at 46-60 % of the
   panel depending on the breakpoint, so `--wk` is 1.63 / 1.67 — the frame has
   to be two thirds wider than the panel just to reach its left edge, and that
   is what makes these two a closer shot than frame 1. */
.b0-panel--arut .b0-fr--3 .b0-photo { --asp: .8829;   --fx: 37%;    --fy: 20%;    --fh: 103%; --wk: 1.63; --cap: 2200px; }
.b0-panel--arut .b0-fr--4 .b0-photo { --asp: .8;      --fx: 36%;    --fy: 19%;    --fh: 106%; --wk: 1.67; --cap: 2400px; }
/* 5 — ig 27, the dusk road (new this round). His cheek is 46 % down the source
   and backlit, so the vertical rule (0.81/0.54) sizes it at 150 % minimum;
   155 % also pushes the horizon to ~9 % of the panel, where `.b0-panel::after`
   is still 0.24 alpha and the sky reads as a graded band, not as daylight. */
.b0-panel--arut .b0-fr--5 .b0-photo { --asp: .8;      --fx: 39%;    --fy: 46%;    --fh: 155%; --wk: 1.54; --cap: 2400px; }


/* ------------------------------------------------------------------
   THE PHOTO CYCLE - CSS keyframes only, no GSAP loop (SPEC 7a).

   Both sides now hold five frames, so both run one 50 s loop. Each side
   changes every 10 s and the two are 5 s out of phase, so an arrival lands
   every 5 s, left then right — which is the client's «сначала правая, потом
   левая же появляется… они друг друга заменяют», and it is what makes the
   PAIRING chain meaningful: only one panel is ever moving.

   FOUR ARRIVAL TYPES, not one (client: «а можно разные типы появления…
   кинематографичном стиле»). Round 1 gave every frame the same hard top-down
   `clip-path` wipe, which is exactly the "они как будто сверху вниз
   обновляются" he objected to. There is no `clip-path` left anywhere in B0.

     push     scale 1.065 → 1 over 2.6 s with a 1.8 s dissolve. No mask.
     drift    scale 1.07 → 1 plus a 2.2 % / 1.2 % translate over 3 s, 2 s
              dissolve. The scale is deliberately larger than the translate, so
              the layer still covers the panel at every point of the move
              (0.42 × 7 % of the width > 2.2 %, 0.19 × 7 % of the height > 1.2 %).
     wipe     a horizontal FEATHERED wipe: a 250 %-wide gradient mask slid from
              100 % to 0 %, whose soft edge is 12 % of the mask, i.e. 30 % of the
              panel width. Not a clip: there is no hard edge anywhere in it.
     curtain  the same thing on the vertical, 250 % tall, edge 30 % of the panel
              height. The one top-down move that survives, and it is a wash
              rather than a wipe.

   Assignment alternates so the same type never runs twice in a row across the
   two sides (arrival order in ĉ: L2 push, R2 drift, L3 wipe, R3 push, L4 drift,
   R4 wipe, L5 curtain, R5 curtain, L1 base, R1 base):
     left   2 push   3 wipe   4 drift  5 curtain
     right  2 drift  3 push   4 wipe   5 curtain

   Three ROLES, independent of the type:
     base  frame 1. Never fades; it only pushes in as the top frame uncovers it.
           Always opaque, so no crossfade is ever against the bare stage.
     mid   frames 2-4. Arrive over the frame below, hold, then CUT to 0 at 13 s
           — one second after the next frame is fully covering them.
     top   frame 5. Same arrival, then FADES out over 2 s at the 10 s mark and
           reveals frame 1, the only layer still painted.
   Percentages are seconds / 50 s: 2.0 s (full cover) = 4 %, 2.6 s = 5.2 %,
   3.0 s = 6 %, 10 s = 20 %, 12 s = 24 %, 12.8 s = 25.6 %, 13 s = 26 %,
   48.5 s = 97 %.
   ------------------------------------------------------------------ */
/* `visibility`, not just `opacity`: a transparent layer is still painted, and
   ten 1600px frames with a grade stack each cost 15 fps at idle on the QA rig
   (measured: 123 frames over 32ms in 12s; with the inactive layers hidden, 0 —
   qa/phase13/idle.json). Every keyframe below turns visibility on for the
   frame's own window and off for the rest of the loop, so at any moment only
   the two layers a side actually shows are painted. */
.b0-fr:not(.b0-fr--1) { opacity: 0; visibility: hidden; }

/* The two masks. They sit on the resting position (`0% 0%` = fully opaque)
   whenever the layer is not arriving, so a browser that will not animate
   `mask-position` degrades to a plain dissolve rather than to a hole. The
   feather is 12 % of a 250 % mask = 30 % of the panel. */
.b0-tr--wipe {
  -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 44%, rgba(0,0,0,0) 56%, rgba(0,0,0,0) 100%);
          mask-image: linear-gradient(90deg, #000 0%, #000 44%, rgba(0,0,0,0) 56%, rgba(0,0,0,0) 100%);
  -webkit-mask-size: 250% 100%;         mask-size: 250% 100%;
  -webkit-mask-repeat: no-repeat;       mask-repeat: no-repeat;
  -webkit-mask-position: 0% 0%;         mask-position: 0% 0%;
}
.b0-tr--curtain {
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 44%, rgba(0,0,0,0) 56%, rgba(0,0,0,0) 100%);
          mask-image: linear-gradient(180deg, #000 0%, #000 44%, rgba(0,0,0,0) 56%, rgba(0,0,0,0) 100%);
  -webkit-mask-size: 100% 250%;         mask-size: 100% 250%;
  -webkit-mask-repeat: no-repeat;       mask-repeat: no-repeat;
  -webkit-mask-position: 0% 0%;         mask-position: 0% 0%;
}

/* delay = -(local time at ĉ = 0). ĉ = 0 is when `.is-cycling` lands, and both
   sides are in their settled "frame 1 alone" window there (left 7.5 s into it,
   right 2.5 s) — the cycle can never start mid-dissolve. Left arrivals land on
   ĉ = 2.5 s + 10k, right on ĉ = 7.5 s + 10k. */
.b0-portraits.is-cycling .b0-panel--vika .b0-fr--1 { animation: b0-base        50s linear  -7.5s infinite; }
.b0-portraits.is-cycling .b0-panel--vika .b0-fr--2 { animation: b0-mid-push    50s linear -47.5s infinite; }
.b0-portraits.is-cycling .b0-panel--vika .b0-fr--3 { animation: b0-mid-wipe    50s linear -37.5s infinite; }
.b0-portraits.is-cycling .b0-panel--vika .b0-fr--4 { animation: b0-mid-drift   50s linear -27.5s infinite; }
.b0-portraits.is-cycling .b0-panel--vika .b0-fr--5 { animation: b0-top-curtain 50s linear -17.5s infinite; }

.b0-portraits.is-cycling .b0-panel--arut .b0-fr--1 { animation: b0-base        50s linear  -2.5s infinite; }
.b0-portraits.is-cycling .b0-panel--arut .b0-fr--2 { animation: b0-mid-drift   50s linear -42.5s infinite; }
.b0-portraits.is-cycling .b0-panel--arut .b0-fr--3 { animation: b0-mid-push    50s linear -32.5s infinite; }
.b0-portraits.is-cycling .b0-panel--arut .b0-fr--4 { animation: b0-mid-wipe    50s linear -22.5s infinite; }
.b0-portraits.is-cycling .b0-panel--arut .b0-fr--5 { animation: b0-top-curtain 50s linear -12.5s infinite; }

/* Frame 1 is hidden while a frame above it is FULLY opaque — two filtered
   1100x1400 layers on top of each other is what costs the frame budget, one
   costs nothing (measured: 114 dropped frames in 15s with both painted, 0 with
   only the top one — qa/phase13/idle.json). The window is the base's own
   timeline: it goes dark 0.8s after the first frame above it reaches full
   cover and comes back 1.5s before the top frame starts to fade off it, so
   what is uncovered is always an already-painted layer. Every visibility
   switch is a 0.01 % pair, so it reads the same under both `visibility`
   interpolation rules (discrete-at-midpoint and visible-wins). */
@keyframes b0-base {
  0%     { visibility: visible; transform: scale(1.06);
           animation-timing-function: cubic-bezier(.22,.61,.36,1); }
  5.2%   { transform: scale(1); }
  25.59% { visibility: visible; }
  25.6%  { visibility: hidden; }
  96.99% { visibility: hidden; }
  97%    { visibility: visible; }
  100%   { visibility: visible; transform: scale(1); }
}

/* 1 — PUSH. Opacity does the reveal, the scale gives it the slow lens move. */
@keyframes b0-mid-push {
  0%     { opacity: 0; visibility: visible; transform: scale(1.065);
           animation-timing-function: cubic-bezier(.22,.61,.36,1); }
  3.6%   { opacity: 1; }
  5.2%   { transform: scale(1); }
  25.99% { opacity: 1; visibility: visible; transform: scale(1); }
  26.01% { opacity: 0; visibility: hidden; transform: scale(1.065); }
  100%   { opacity: 0; visibility: hidden; transform: scale(1.065); }
}

/* 2 — DRIFT. A lateral move under a dissolve. The scale is what keeps the
   panel covered while the layer is off its rest position. */
@keyframes b0-mid-drift {
  0%     { opacity: 0; visibility: visible; transform: translate3d(2.2%, -1.2%, 0) scale(1.07);
           animation-timing-function: cubic-bezier(.25,.55,.3,1); }
  4%     { opacity: 1; }
  6%     { transform: translate3d(0, 0, 0) scale(1); }
  25.99% { opacity: 1; visibility: visible; transform: translate3d(0, 0, 0) scale(1); }
  26.01% { opacity: 0; visibility: hidden; transform: translate3d(2.2%, -1.2%, 0) scale(1.07); }
  100%   { opacity: 0; visibility: hidden; transform: translate3d(2.2%, -1.2%, 0) scale(1.07); }
}

/* 3 — WIPE. The mask does the reveal, so opacity is 1 from the first frame;
   the layer is nevertheless invisible at 0 %, because the mask shows none of
   it. See qa/phase14/b0dip.py: the coverage probe counts a layer whose mask is
   off its rest position as contributing NOTHING, which is the conservative
   reading and is why the gate still means something with masks in play. */
@keyframes b0-mid-wipe {
  0%     { opacity: 1; visibility: visible; transform: scale(1.03);
           -webkit-mask-position: 100% 0%; mask-position: 100% 0%;
           animation-timing-function: cubic-bezier(.33,.6,.28,1); }
  4%     { -webkit-mask-position: 0% 0%;   mask-position: 0% 0%; }
  5.6%   { transform: scale(1); }
  25.99% { opacity: 1; visibility: visible; transform: scale(1);
           -webkit-mask-position: 0% 0%;   mask-position: 0% 0%; }
  26.01% { opacity: 0; visibility: hidden; transform: scale(1.03);
           -webkit-mask-position: 100% 0%; mask-position: 100% 0%; }
  100%   { opacity: 0; visibility: hidden; transform: scale(1.03);
           -webkit-mask-position: 100% 0%; mask-position: 100% 0%; }
}

/* 4 — CURTAIN, and the only frame that also has to LEAVE: it fades off over
   2 s at the 10 s mark and hands the panel back to frame 1. It leaves on
   opacity with the mask at rest, so the exit is a clean dissolve. */
@keyframes b0-top-curtain {
  0%     { opacity: 1; visibility: visible; transform: scale(1.02);
           -webkit-mask-position: 0% 100%; mask-position: 0% 100%;
           animation-timing-function: cubic-bezier(.36,.62,.3,1); }
  4.4%   { -webkit-mask-position: 0% 0%;   mask-position: 0% 0%; }
  6%     { transform: scale(1); }
  20%    { opacity: 1; visibility: visible; transform: scale(1);
           -webkit-mask-position: 0% 0%;   mask-position: 0% 0%; }
  24%    { opacity: 0; visibility: visible; transform: scale(1);
           -webkit-mask-position: 0% 0%;   mask-position: 0% 0%; }
  24.01% { opacity: 0; visibility: hidden; }
  100%   { opacity: 0; visibility: hidden; transform: scale(1.02);
           -webkit-mask-position: 0% 100%; mask-position: 0% 100%; }
}

/* Paused off screen (SPEC 7a rule 3). `animation-name` is KEPT here, unlike the
   chain wave: every keyframe of `b0-base` is opaque, so a frame frozen where it
   stands can never show an empty panel, and dropping the name would snap a
   half-arrived layer back to its 0 %. */
.b0-portraits:not(.is-inview) .b0-fr { animation-play-state: paused; }

/* --- grade layer (z 1): sits over the photographs, under every glyph --- */
.b0-stage { position: absolute; inset: 0; z-index: 1; pointer-events: none; }

/* `.b0-seam` (a 0.84-alpha scrim over the join) is GONE — the panels dissolve
   into each other now, so there is nothing for a scrim to hide and a scrim is
   precisely the dark column the client asked to remove. See `.b0-panel--arut`.
   The element is out of index.html too. */

/* the faces dissolve into the stage over the bottom ~70%, so every glyph of the
   headline, the subtitle and the names sits on darkness, never on a face */
.b0-grade {
  position: absolute; inset: 0;
  background: linear-gradient(to top,
    var(--bg) 4%,
    rgba(14,15,18,.97) 20%,
    rgba(14,15,18,.84) 34%,
    rgba(14,15,18,.46) 52%,
    rgba(14,15,18,.14) 66%,
    rgba(14,15,18,0) 78%);
}
/* THE VIGNETTE, RE-SHAPED FOR THE CENTRED COPY (2026-09-09).

   It used to be a left→right ramp, 0.56 alpha at the left edge falling to 0 at
   82 % — "darkest exactly where the headline begins". That was correct while
   the headline was flush left, and it is the direct cause of what the client
   saw: Виктория sat under the darkest 0.31–0.50 of it and Арут under ~0.00, so
   one partner was buried and the other was not. Moving the copy to the middle
   without moving the darkening would have left her buried AND taken the
   headline off its ground.

   Three layers now, and the shape is symmetric so neither portrait is
   favoured:
     1. the COPY POOL — an ellipse sized to the copy box (headline ink is
        x 24–76 %, y 40–75 % at 1440, the subtitle to y 84 %), so the darkening
        follows the text instead of the left edge. Both frame-1 face anchors
        (23.1 %/19 % and 77.3 %/19 % of the viewport) sit at r ≈ 1.00 of this
        ellipse, i.e. on its zero stop — measured, not eyeballed: a 28 px patch
        on each anchor came out at 119.6 % (Виктория) and 96.7 % (Арут) of its
        old luminance (qa/phase15/b0centre/legibility.json).
     2. a mild SYMMETRIC edge fall-off, replacing the old one-sided ramp. It
        frames the diptych and is the layer that gives Виктория her exposure
        back: 0.31 → 0.05 at her face.
     3. the corner fall-off, re-centred 60 % → 50 % for the same reason.
   Worst-case headline contrast over all ten photo pairs went 9.79 → 10.58 on
   the single brightest pixel under the ink, 12.13 → 12.26 on its p99 and
   15.68 → 15.90 on its mean (same file). */
.b0-vig {
  position: absolute; inset: 0;
  background:
    radial-gradient(60% 46% at 50% 60%,
      rgba(14,15,18,.27) 0%,
      rgba(14,15,18,.24) 50%,
      rgba(14,15,18,.12) 80%,
      rgba(14,15,18,0) 100%),
    linear-gradient(to right,
      rgba(14,15,18,.20) 0%,
      rgba(14,15,18,.06) 18%,
      rgba(14,15,18,0) 40%,
      rgba(14,15,18,0) 60%,
      rgba(14,15,18,.06) 82%,
      rgba(14,15,18,.20) 100%),
    radial-gradient(124% 82% at 50% 30%, rgba(14,15,18,0) 46%, rgba(14,15,18,.42) 100%);
}

/* --- copy layer (z 2) --- */
.b0-ui {
  position: relative;
  z-index: 2;
  min-height: 100vh;            /* fallback: dvh is unsupported < Safari 15.4 */
  min-height: 100dvh;
  display: grid;
  grid-template-rows: auto 1fr auto auto;
  /* D1 — the cover is 100dvh, i.e. it paints UNDER the status bar / Dynamic
     Island, and row 1 (the eyebrow) starts 26–46px down. A 59px inset on an
     iPhone 15 Pro would put the caption behind the island, so the top padding
     carries the inset. 0px on every desktop → same computed box. */
  padding: calc(clamp(26px, 4.2vh, 46px) + env(safe-area-inset-top, 0px))
           0
           calc(clamp(22px, 3.4vh, 38px) + env(safe-area-inset-bottom, 0px));
}
/* the hairline must never end up alone on its own line when the caption wraps,
   so the rule is pinned beside the text and only the text wraps */
.b0 .eyebrow {
  max-width: min(760px, 52vw);
  color: var(--fg-2);
  flex-wrap: nowrap;
  align-items: flex-start;
  /* centred with the headline (2026-09-09) — a centred title card with a
     left-flush kicker reads as a mistake, and the hairline is the block's only
     accent object (TZ), so it travels with the caption rather than being cut */
  margin-inline: auto;
  justify-content: center;
  text-align: center;
}
.b0 .eyebrow .rule { margin-top: .62em; }
/* B6 — flex items never split across lines, so the caption can only ever
   break after «предложение ·», never leave «ПРОДЮСИРОВАНИЕ» alone */
/* `justify-content: center` matters only when it DOES wrap (≤1180 the caption
   breaks after «предложение ·»): the second line has to centre under the first
   now that the whole block is centred, or the group reads as ragged-right. */
.b0 .eyebrow .b0-eb-t { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center; }

/* CENTRED (2026-09-09, client). The copy used to be flush left, and the
   left→right vignette was built to sit under it — which is exactly what buried
   Виктория while Арут stayed clear: «оглавление и подзаголовок поставить
   посередине, потому что мы Викторию перекрываем, а больше видно Арута».
   The block is centred in `.wrap`, and `.wrap` is centred in the viewport
   (`margin: 0 auto` + symmetric gutters on `.b0`), so the headline is centred
   on the seam at every width. Nothing about the intro timeline changes: the
   lines still rise inside their own `.ln` masks, which are unchanged.
   `.b0-vig` was re-shaped to match — see there. */
.b0-copy {
  grid-row: 3;
  padding-bottom: clamp(24px, 5vh, 58px);
  max-width: var(--copy-max);
  margin-inline: auto;
  text-align: center;
}

/* DECISIONS: the line breaks are semantic — «Метода Тальписа» must never split.
   That caps the display size: at the SPEC token (108px) the second line is
   ~890px and the copy column is 832px, so it wrapped. 6.1vw / 90px max keeps
   the three lines intact at every width; `nowrap` guarantees it. */
.b0-title {
  font-family: var(--font-dp);
  font-size: clamp(42px, 6.1vw, 90px);
  /* B6 — one Prata ramp, display step (see main.css `.statement`) */
  line-height: 1;
  letter-spacing: -.014em;
  color: var(--fg);
}
.ln { display: block; overflow: hidden; padding-bottom: .06em; margin-bottom: -.06em; }
.ln-i { display: block; white-space: nowrap; }

/* ------------------------------------------------------------------
   THE MASK HEADROOM (B0 cinema round). USA was clipped top AND bottom.
   Measured at 4x DSF with the glyphs isolated by a differential
   (qa/b0-cinema/ink.json): line 3 pushes ink +0.011..+0.033 em ABOVE the `.ln`
   box and +0.011..+0.032 em BELOW it at 360/390/1024/1440/1920 - the pointed
   apex of A and the crown of C overshoot the cap line, and p/y hang past the
   old .06em bottom pad. Lines 1 and 2 clear the bottom but come within
   +0.020 em of the top.
   Worst case is 0.033 em; the mask carries .14em above and .18em below, i.e.
   >= .107 em and >= .088 em of clearance - which is also the room the flag
   wave's +-3.4 px displacement needs. Padding plus a matching negative margin,
   so the line box and every measurement built on it are unchanged.
   The rise in main.css section 10 is 126 %, not 108 %, for the same reason: the
   inner line has to clear the taller mask (100 % + 18 % + 3.3 % of ink).
   ------------------------------------------------------------------ */
.b0-title .ln {
  padding: .14em 0 .18em;
  margin:  -.14em 0 -.18em;
}


/* ------------------------------------------------------------------
   HEADLINE LIGHT SWEEP - one pass per line, as the line lands.
   `background-clip: text` on `.ln-i` with a five-stop gradient whose RESTING
   position paints flat `--fg`. No overlay element, no blend mode, and the
   resting render is the shipped one.
   The gradient image is 320 % of the line, so ANY background-position between
   0 % and 100 % still covers every glyph - the resting state must never leave a
   letter unpainted (the first cut used a 100 %-wide image, and the whole
   headline rendered as nothing). At 100 % the element maps to the image's
   68.75-100 % band, which is flat `--fg`; sweeping to 0 % drags the white stop
   at the image's 50 % across the line from left to right.
   ------------------------------------------------------------------ */
.b0-title .ln-i {
  background-image: linear-gradient(96deg,
    var(--fg) 0%, var(--fg) 38%,
    #FFFFFF 50%,
    var(--fg) 62%, var(--fg) 100%);
  background-size: 320% 100%;
  background-repeat: no-repeat;
  background-position: 100% 0;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}
.b0-title .ln-i.is-lit { animation: ln-sweep 1.15s cubic-bezier(.25,.6,.2,1) both; }
@keyframes ln-sweep {
  from { background-position: 100% 0; }
  to   { background-position: 0% 0; }
}


/* ------------------------------------------------------------------
   THE FLAG WORD. The span wraps existing text and adds nothing, so the rendered
   copy is unchanged (qa/phase8/copy-diff.py = 0). It stays selectable text;
   `background-clip: text` only changes how the glyphs are filled. Behind
   `body.flag-usa` (default ON, the client's call) so the plain-`--fg` version
   is one class away for comparison.

   ROUND 2 — STATIC. The client's words: «надо было буквы статичные сделать…
   не сильно выделяющуюся… просто статичную картинку». Everything that moved is
   gone: the SMIL `#usa-wave` filter (and its `<svg class="b0-defs">` in
   index.html), the 9 s `usa-sheen` drift, and the `.is-waving` class that
   main.js used to add at 2.41 s. The word now does exactly what the rest of the
   headline does — it rises through its mask and stops.

   The flag itself is muted: `mkflag.py` mixes every ink 62 % of the way to
   `--fg`, so the three colours land at #D3ABAD / #E9E6DF / #AFB3C1. Contrast
   against the B0 ground goes from a worst quartile of 2.15 : 1 to 9.16 : 1 on
   the darkest ink; the flag's own internal spread drops from 5.48 : 1 to
   1.68 : 1. That is the trade the client asked for: read the word first, see
   the flag second.
   ------------------------------------------------------------------ */
.usa { color: inherit; }
/* the sweep fills `.ln-i`; the flag word owns its own fill and must not be
   repainted by it */
body.flag-usa .b0-title .ln-i .usa { -webkit-text-fill-color: transparent; }

body.flag-usa .usa {
  background-image: url("../img/b0/flag.svg");
  background-repeat: no-repeat;
  /* Geometry, measured (qa/b0-cinema/flag-sweep.png, six candidates). The
     window into the flag is ~31 % of its width and 4 of its 13 stripes, placed
     so the canton covers only the leading ~15 % of the word: C takes the stars,
     SHA take clean red/pale bands. The first cut sampled 420 %/460 % at
     26 % 46 %, which put ~85 % of the word over the canton and whole strokes
     dropped out (qa/b0-cinema/usalum.py). */
  background-size: 322% 325%;
  background-position: 51.2% 23.1%;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  /* the glyph edges are the only thing the flag has to hold, so a hairline of
     the page's own ink keeps them from dissolving into the stage */
  -webkit-text-stroke: .012em rgba(243,241,237,.30);
  padding-right: .04em;   /* the stroke needs the sidebearing */
  margin-right: -.04em;
}

/* the flag-off comparison state */
body:not(.flag-usa) .usa { -webkit-text-fill-color: currentColor; }


/* ------------------------------------------------------------------
   REDUCED MOTION - frame 1 only, no cycle, no sweep. The flag word needs no
   rule of its own any more: it does not move under any condition.
   Both switches fire independently: `html.rm` (set by the inline script in
   index.html) and the media query, so a reader whose OS setting is seen only by
   CSS is covered too.
   NOT killed under `html.nogsap`: the cycle is CSS and a missing GSAP bundle
   has no bearing on it, so `cinema()` runs on that path too.
   ------------------------------------------------------------------ */
html.rm .b0-fr:not(.b0-fr--1) { display: none; }
html.rm .b0-fr { animation: none !important; transform: none !important; }
html.rm .b0-title .ln-i { animation: none !important; }

@media (prefers-reduced-motion: reduce) {
  .b0-fr:not(.b0-fr--1) { display: none; }
  .b0-fr { animation: none !important; transform: none !important; }
  .b0-title .ln-i { animation: none !important; }
}

/* DECISIONS: no one-word last line. `.nb` keeps «через медиа» together, so the
   subtitle can only ever break as "Привлечение новой аудитории / через медиа". */
.b0-sub {
  margin-top: clamp(20px, 2.4vw, 30px);
  /* the max-width still governs the break; `auto` centres the measure under
     the centred headline instead of parking it on the left edge */
  margin-inline: auto;
  max-width: 32ch;
  color: var(--fg-2);
  font-size: clamp(17px, 1.5vw, 22px);
  line-height: 1.45;
}
.b0-sub .nb { white-space: nowrap; }

/* names row — one hairline, a pair parked in each bottom corner, under the
   portrait each of them belongs to (DECISIONS: "names/roles at the bottom
   corners under each portrait") */
.b0-names {
  grid-row: 4;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  padding-top: clamp(16px, 2.2vh, 24px);
  border-top: 1px solid var(--line);
}
.b0-name { max-width: 40ch; }
.b0-name--right { text-align: right; }
.b0-name-n {
  color: var(--fg);
  font-size: var(--t-small);
  font-weight: 600;
  letter-spacing: .01em;
}
.b0-name-r {
  margin-top: 5px;
  color: var(--fg-3);
  /* E6 — 11.5/11px was the smallest type on the cover; 12px is the floor and
     the roles still fit two lines at 360 (qa/phase5/b0-360.png) */
  font-size: 12px;
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: .11em;
  text-transform: uppercase;
}


/* ============================================================
   B1 — ГЛАВНАЯ ИДЕЯ
   ============================================================ */

/* a modest extra tail: B1 hands off to B2, so it no longer needs mock-A's
   240px runway for the last reveal (the chip) to reach its trigger */
.b1 { padding-bottom: clamp(120px, 13vw, 184px); }

.b1-lead {
  margin-top: clamp(28px, 3vw, 40px);
  max-width: 48ch;
}

/* --- pinned question → answer (CSS sticky, not a GSAP pin) --- */
.b1-pin { position: relative; margin-top: clamp(48px, 7vw, 90px); }
.b1-sticky { position: relative; }

.b1-q {
  font-family: var(--font-dp);
  /* C1 — capped below `.b0-title` (6.1vw / 90px): the cover headline has to
     stay the biggest type on the page */
  font-size: clamp(40px, 5.6vw, 82px);
  line-height: 1;                    /* B6 Prata ramp — display step */
  letter-spacing: -.014em;
  color: var(--fg);
  /* em, not ch — the break is tuned to Prata's real metrics and must scale */
  max-width: 7.8em;
}
.b1-a {
  margin-top: clamp(28px, 3.4vw, 46px);
  font-family: var(--font-dp);
  font-size: var(--t-h1);
  line-height: 1.12;
  color: var(--fg);
  max-width: 18ch;
}

.b1-rail { display: none; }

@media (min-width: 900px) {
  /* the hold is real but short: every vh of pin travel is a vh of empty
     stage in a static view, so it is tuned to read as editorial air */
  .b1-pin { min-height: 92vh; padding-left: 56px; margin-left: -56px; }
  .b1-sticky { position: sticky; top: 20vh; }
  .b1-rail {
    display: block;
    position: absolute;
    left: 0; top: 6px; bottom: 8vh;
    width: 1px;
    background: var(--line);
  }
  .b1-rail i {
    position: absolute;
    inset: 0 auto 0 0;
    width: 1px; height: 100%;
    background: linear-gradient(to bottom, var(--accent), rgba(228,58,110,0));
    transform: scaleY(0);
    transform-origin: 50% 0;
  }
}

/* --- face strip --- */
.b1-faces {
  display: flex;
  align-items: flex-start;
  gap: clamp(10px, 1.1vw, 16px);
  margin-top: clamp(4px, 1vw, 12px);
}
.b1-face {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  max-width: 132px;
  aspect-ratio: 2 / 3;
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--bg-3);
  -webkit-mask-image: linear-gradient(to bottom, #000 72%, rgba(0,0,0,.35) 95%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 72%, rgba(0,0,0,.35) 95%, transparent 100%);
}
.b1-face img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 30%;
  filter: saturate(.7) contrast(1.05) brightness(.9);
}
/* one clean alternation reads as rhythm; three offsets read as broken images */
.b1-face:nth-child(even) { transform: translateY(20px); }

/* the last frame reads as a mirror, not a portrait */
.b1-face--mirror { border-color: var(--line-2); }
.b1-face--mirror img { filter: saturate(.28) contrast(1.1) brightness(.42); }
.b1-face--mirror::after {
  content: "";
  position: absolute; inset: 0;
  background:
    linear-gradient(152deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,.02) 26%, rgba(255,255,255,0) 46%),
    linear-gradient(180deg, rgba(16,19,24,.62) 0%, rgba(11,13,17,.86) 100%);
}

.b1-faces-note { margin-top: clamp(26px, 3vw, 38px); }

/* --- text blocks --- */
.b1-text {
  margin-top: clamp(56px, 7vw, 96px);
  display: grid;
  gap: clamp(18px, 1.8vw, 26px);
  max-width: 62ch;
}

.b1-split { margin-top: clamp(52px, 6.4vw, 92px); }
.b1-h2 {
  font-size: var(--t-h2);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -.02em;
  color: var(--fg);
  max-width: 16ch;
}
.b1-aside { padding-top: clamp(0px, 2.4vw, 34px); }

/* --- chain --- */
.b1-chain-lead {
  margin-top: clamp(56px, 7vw, 100px);
  max-width: 52ch;
  color: var(--fg-2);
  font-size: var(--t-small);
  line-height: 1.5;
}

/* --- closing line + chip --- */
.b1-foot {
  margin-top: clamp(56px, 7vw, 100px);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: clamp(28px, 4vw, 56px);
  padding-top: clamp(30px, 3.4vw, 44px);
  border-top: 1px solid var(--line);
}
.b1-close { max-width: 46ch; color: var(--fg); }


/* ============================================================
   CHAIN — the stepped infographic used by B1, B2 (dark), B6 (paper) and
   B7 (inside the paper card). Desktop: N equal columns sharing the full row.
   Mobile (< 900px): a vertical rail.

   All geometry rides on two pseudo-elements per item, so the markup stays a
   plain <ol> (an <ol> may only contain <li>, so a single shared track element
   is not legal here):

     .chain-i::before   ONE segment of the track — from this node's RING EDGE to
                        the next node's ring edge. Segments meet the rings
                        exactly, so the row reads as one continuous hairline
                        that stops on the last node instead of dangling past it.
     .chain-i::after    the node — a ring (40px on B1, 36px tight, 32px inside
                        the B7 card, 32px on a phone) with a transparent fill.
     .chain-i > .ic     the Lucide glyph, centred in that ring. Icon round
                        2026-09-08: the ring used to hold a 6px raspberry core
                        painted by a radial-gradient; the glyph replaces it, so
                        each step of the story is named twice — by a picture and
                        by a word.

   Two custom properties are the animation contract with main.js (§ chains):
     --seg      0 → 1   the segment's scaleX / scaleY  (the track draws)
     --node-s  .4 → 1   the node's scale — and the icon's, so the ring and the
                        glyph inside it pop as one object
   Both fall back to their FINAL value, so reduced motion, a GSAP-less page
   and a plain `file://` open all render the finished infographic with no JS.
   The icon's own draw-on (stroke-dashoffset) is applied by main.js only, never
   by CSS, for the same reason.
   ============================================================ */

.chain {
  --chain-gap: clamp(16px, 2vw, 32px);
  --node: 40px;                          /* ring diameter */
  --node-c: calc(var(--node) / 2);       /* ring centre, from the item's edge */
  --track: var(--line-2);

  margin-top: clamp(26px, 3vw, 40px);
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  column-gap: var(--chain-gap);
}
.chain--tight { --node: 36px; }
.b7-steps     { --node: 32px; }

.chain-i {
  position: relative;
  padding-top: calc(var(--node) + 16px);   /* the room the node sits in */
}

/* the segment spans ring-edge → ring-edge: from `--node` (centre + radius) in
   this item to the next item's ring, which starts one gap past its own edge */
.chain-i::before {
  content: "";
  position: absolute;
  left: var(--node);
  right: calc(-1 * var(--chain-gap));
  top: var(--node-c);
  height: 1px;
  background: var(--track);
  transform: scaleX(var(--seg, 1));
  transform-origin: 0 50%;
}
.chain-i:last-child::before { content: none; }

.chain-i::after {
  content: "";
  position: absolute;
  left: 0; top: 0;
  width: var(--node); height: var(--node);
  border: 1px solid var(--accent-line);
  border-radius: 50%;
  background: none;
  /* `--halo` is the node's RESTING ring glow. It is a variable and not a plain
     declaration because the wave loop below animates the same box-shadow, and
     an animation beats a normal declaration: with a hard-coded value on the
     terminal node its permanent halo would blink off on every cycle. */
  box-shadow: 0 0 0 var(--halo, 0px) var(--accent-soft);
  transform: scale(var(--node-s, 1));
}

.chain-i > .ic {
  position: absolute;
  left: calc(var(--node-c) - var(--ic) / 2);
  top: calc(var(--node-c) - var(--ic) / 2);
  --ic: 20px;
  /* `--ic-rest` is the settled colour, `--ic-lift` the colour the wave lifts it
     to for the length of one draw. One `color` declaration, four contexts. */
  --ic-rest: var(--fg-2);
  --ic-lift: var(--fg);
  color: var(--ic-rest);
  transform: scale(var(--node-s, 1));
}
.chain--tight .chain-i > .ic { --ic: 18px; }
.b7-steps .chain-i > .ic     { --ic: 16px; --icw: 1.75; }

/* P1 — accent dosage (SPEC §2, <= ~3 accent objects per viewport). Only the
   TERMINAL node is raspberry: B1/B2 «ИЗМЕНЕНИЕ» (the outcome of the story) and
   B7 «ДЕТАЛЬНЫЙ ПЛАН» (the outcome of the three steps). B6's «МАРКЕТИНГ» is an
   inventory, not a story — that chain stays uniformly muted.
   Icon round: the raspberry moved from the core to the GLYPH, and the ring
   itself stays --accent-line on every node including the last one, so the
   number of strong (alpha >= .5) accent objects per chain is still exactly 1. */
.chain > .chain-i:last-child { --halo: 6px; }
.chain > .chain-i:last-child > .ic  { --ic-rest: var(--accent); --ic-lift: var(--accent); }
.b6-contour > .chain-i:last-child { --halo: 0px; }
.b6-contour > .chain-i:last-child > .ic  { --ic-rest: var(--ink-2); --ic-lift: var(--fg); }

.chain-n {
  display: block;
  color: var(--fg-3);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: .1em;
  font-variant-numeric: tabular-nums;
}
.chain-t {
  display: block;
  margin-top: 8px;
  color: var(--fg);
  font-size: clamp(16px, 1.3vw, 20px);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.chain--tight .chain-t { font-size: clamp(15px, 1.15vw, 18px); }
.b7-steps .chain-t      { font-size: clamp(14px, 1vw, 16px); }

/* on paper the hairline is ink, not white */
.blk--paper .chain,
.paper-card .chain { --track: rgba(18, 19, 22, .18); }
.blk--paper .chain-n,
.paper-card .chain-n { color: var(--ink-2); }
.blk--paper .chain-i > .ic,
.paper-card .chain-i > .ic { --ic-rest: var(--ink-2); }


/* --- THE WAVE (2026-09-08, loops round) -----------------------------------
   The client: «цепочки должны быть анимированы, зациклено, слева направо».
   A light walks the chain once every 7s: node i starts at i x 0.38s, and for
   0.9s its glyph re-draws itself while a soft raspberry halo blooms out of its
   ring and the glyph lifts to the section's brightest text colour.

   CSS only — no GSAP tween, so the idle-tween gate stays at 0 (SPEC §7).
   Two properties per node and nothing that touches layout: the icon's
   `stroke-dashoffset` (a repaint of a 20px box) and the ring's `box-shadow`
   spread (a repaint of a 50px box).

   Three conditions gate it, and all three have to hold:
     .is-revealed   added by the chains() timeline's onComplete. The one-shot
                    reveal draw-on owns the dash until then; it also means the
                    loop simply does not exist under html.rm / html.nogsap /
                    no JS, where that timeline never runs and the icons render
                    solid with no dash property at all.
     .is-inview     from an IntersectionObserver with hysteresis (running at
                    ratio >= .3, paused only at ratio 0), so a chain is never
                    frozen mid-draw while any of it is still on screen.
     --len          the icon's dash length, written once per glyph by main.js.
                    The <use> shadow tree is not scriptable, but dasharray and
                    dashoffset are INHERITED SVG properties, so one value on
                    the host <svg class="ic"> reaches every geometry element
                    inside it — the same fact the reveal draw-on rides on.

   On a phone the chain is a vertical rail and the same nth-child delays make
   the wave run top -> bottom. Nothing else changes. */
.chain {
  --wave: 7s;                    /* one full pass */
  --wave-step: .38s;             /* node i starts at i * this */
}
.chain-i:nth-child(1) { --wave-d: 0s; }
.chain-i:nth-child(2) { --wave-d: calc(var(--wave-step) * 1); }
.chain-i:nth-child(3) { --wave-d: calc(var(--wave-step) * 2); }
.chain-i:nth-child(4) { --wave-d: calc(var(--wave-step) * 3); }
.chain-i:nth-child(5) { --wave-d: calc(var(--wave-step) * 4); }
.chain-i:nth-child(6) { --wave-d: calc(var(--wave-step) * 5); }

/* 0.9s of 7s = 12.857%. The glyph is re-armed on the frame the node's turn
   starts and draws itself back over that window; the colour holds for the draw
   and eases back over the next 0.5s.

   The redraw is CAPPED at 34 % of the dash length (2026-09-08, art direction).
   A full `--len` offset empties the glyph for a moment, so the icon vanished
   and came back on every pass — a blink, not a wave. At .34 the least-drawn
   frame still shows 66 % of every path, so what travels down the chain is a
   partial re-draw plus the colour lift and the ring bloom, and no glyph is ever
   gone (measured: min 66.0 % drawn — all 19 chain glyphs, every 100ms step of
   the 7s cycle, phase 0 and 400ms included, qa/phase13/wave.json). */
@keyframes chain-wave-ic {
  0%      { stroke-dashoffset: calc(var(--len, 96) * .34); color: var(--ic-lift); }
  12.857% { stroke-dashoffset: 0;              color: var(--ic-lift); }
  20%     { stroke-dashoffset: 0;              color: var(--ic-rest); }
  100%    { stroke-dashoffset: 0;              color: var(--ic-rest); }
}
/* The halo blooms in 0.21s and fades over the rest of the 0.9s window. It is
   written as `--halo + 5px` so the terminal node — which already wears a 6px
   halo at rest — grows from 6 to 11 instead of blinking down to 0 first. */
@keyframes chain-wave-ring {
  0%      { box-shadow: 0 0 0 var(--halo, 0px) var(--accent-soft); }
  3%      { box-shadow: 0 0 0 calc(var(--halo, 0px) + 5px) var(--accent-soft); }
  12.857% { box-shadow: 0 0 0 var(--halo, 0px) var(--accent-soft); }
  100%    { box-shadow: 0 0 0 var(--halo, 0px) var(--accent-soft); }
}

/* ARMED but idle: the timing is already on the element and `animation-play-state`
   already reads `paused`, but the keyframes are NOT attached. That last part is
   load-bearing and is not the same as a plain pause: `paused` freezes a keyframe
   where it stands, and at delay 0 node 01 stands at `stroke-dashoffset: --len`,
   i.e. an invisible glyph. A chain that has scrolled away would keep that frame
   for any full-page screenshot or any repaint that catches it. With no keyframes
   attached the element falls back to its own resting style — solid — and the
   wave restarts from node 01 the next time the chain comes back. */
.chain.is-revealed .chain-i > .ic {
  /* dasharray >= the longest sub-path, offset 0 => solid. The settled frame of
     the loop is pixel-identical to the settled frame without it. */
  stroke-dasharray: var(--len, 96);
  animation: none var(--wave) ease-out var(--wave-d, 0s) infinite paused;
}
.chain.is-revealed .chain-i::after {
  animation: none var(--wave) ease-out var(--wave-d, 0s) infinite paused;
}
.chain.is-revealed.is-inview .chain-i > .ic {
  animation-name: chain-wave-ic;
  animation-play-state: running;
}
.chain.is-revealed.is-inview .chain-i::after {
  animation-name: chain-wave-ring;
  animation-play-state: running;
}

/* belt and braces: the timeline that adds `.is-revealed` never runs under
   html.rm, but a user who flips the OS switch after load gets the same answer */
html.rm .chain .chain-i > .ic,
html.rm .chain .chain-i::after { animation: none; stroke-dasharray: none; }
@media (prefers-reduced-motion: reduce) {
  .chain .chain-i > .ic,
  .chain .chain-i::after { animation: none !important; stroke-dasharray: none !important; }
}


/* ============================================================
   B2 — ЧТО МЫ СОЗДАЁМ: ПРОДУКТ И ФОРМАТ
   ============================================================ */

.b2-intro {
  margin-top: clamp(34px, 4vw, 56px);
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(30px, 4vw, 72px);
  align-items: end;
}
.b2-lead { padding-bottom: 6px; }

.b2-tiles {
  margin-top: clamp(52px, 6vw, 88px);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(14px, 1.6vw, 22px);
}
.b2-tiles .tile { min-height: 210px; }

.b2-chips {
  margin-top: clamp(22px, 2.4vw, 30px);
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.b2-mech { margin-top: clamp(64px, 8vw, 110px); }
.b2-mech-l { margin-bottom: 4px; }

.b2-emo {
  margin-top: clamp(64px, 8vw, 110px);
  padding-top: clamp(30px, 3.4vw, 44px);
  border-top: 1px solid var(--line);
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(24px, 3vw, 56px);
  align-items: start;
}
.b2-emo-t {
  font-family: var(--font-dp);
  font-size: clamp(24px, 2.5vw, 36px);
  line-height: 1.2;
  color: var(--fg);
  max-width: 22ch;
}

@media (max-width: 899px) {
  .b2-intro { grid-template-columns: minmax(0, 1fr); gap: 24px; align-items: start; }
  .b2-tiles { grid-template-columns: minmax(0, 1fr); }
  .b2-tiles .tile { min-height: 0; }
  .b2-emo { grid-template-columns: minmax(0, 1fr); gap: 18px; }
}


/* ============================================================
   B3 — НАША ЭКСПЕРТИЗА: ПОЧЕМУ МЫ  (paper section)
   ============================================================ */

.b3-intro {
  margin-top: clamp(34px, 4vw, 56px);
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(30px, 4vw, 72px);
  align-items: start;
}
.b3-tasks { margin-top: 18px; display: grid; gap: 14px; }
/* three columns since the icon round: glyph · index · sentence */
.b3-task > .ic { --ic: 20px; align-self: center; }
.b3-task {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr);
  gap: 14px;
  align-items: baseline;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  color: var(--fg);
  font-size: var(--t-small);
  line-height: 1.5;
}
/* D6b — an ordinal is not an emphasis. In accent-deep on paper the two «01/02»
   markers also measured 4.33:1 at 13.5px, i.e. under AA, and they spent two of
   the ~3 accent objects SPEC §2 allows in the B3 intro viewport. */
.b3-task-n {
  color: var(--fg-3);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: .1em;
  font-variant-numeric: tabular-nums;
}

/* --- person rows (not cards) --- */
.b3-people { margin-top: clamp(64px, 8vw, 110px); display: grid; }
.b3-person {
  display: grid;
  grid-template-columns: 140px minmax(0, 5fr) minmax(0, 6fr);
  gap: clamp(24px, 3vw, 48px);
  align-items: start;
  padding: clamp(30px, 3.4vw, 46px) 0;
  border-top: 1px solid var(--line);
}
.b3-person:last-child { border-bottom: 1px solid var(--line); }

.b3-por {
  position: relative;
  width: 140px; height: 190px;
  overflow: hidden;
  border-radius: 2px;
  background: transparent;
  -webkit-mask-image: linear-gradient(to bottom, #000 78%, rgba(0,0,0,.5) 94%, rgba(0,0,0,.15) 100%);
          mask-image: linear-gradient(to bottom, #000 78%, rgba(0,0,0,.5) 94%, rgba(0,0,0,.15) 100%);
}
.b3-por img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: var(--op, 50% 20%);
  transform: scale(var(--z, 1));
  transform-origin: var(--op, 50% 20%);
  filter: saturate(.72) contrast(1.04);
}

.b3-name {
  font-family: var(--font-dp);
  font-size: var(--t-h2);
  line-height: 1.1;
  color: var(--fg);
}
.b3-role { margin-top: 12px; }
.b3-desc { padding-top: 6px; color: var(--fg-2); font-size: var(--t-body); line-height: 1.55; max-width: 46ch; }

/* --- results panel --- */
.b3-res { margin-top: clamp(64px, 8vw, 110px); }
.b3-res-h { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 16px; }
.b3-res-t {
  font-size: var(--t-h2);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -.02em;
  color: var(--fg);
}
.b3-nums {
  margin-top: clamp(28px, 3vw, 40px);
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: clamp(14px, 1.6vw, 22px);
}
.b3-num { grid-column: span 2; }
/* 3 + 2: the last two items take three columns each */
.b3-num:nth-child(4), .b3-num:nth-child(5) { grid-column: span 3; }

.b3-num {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  width: 100%;
  /* tile grammar (main.css §2) — one radius, one padding scale, one chip inset
     with `.tile` and `.b6-tile` */
  padding: var(--tile-pad) var(--tile-pad) var(--tile-pad-b);
  border: 1px solid var(--line-2);
  border-radius: var(--tile-radius);
  background: var(--bg-2);
  color: var(--fg);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  transition: border-color .4s var(--ease), background-color .4s var(--ease);
}
.b3-num > * { transition: transform .4s var(--ease); }
.b3-num:hover > *, .b3-num:focus-visible > * { transform: translateY(-2px); }
.b3-num::before {
  content: "";
  position: absolute; inset: 0 0 auto 0;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform .45s var(--ease);
}
.b3-num:hover, .b3-num:focus-visible { border-color: var(--accent-line); }
.b3-num:hover::before, .b3-num:focus-visible::before { transform: scaleX(1); }
.b3-num:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.b3-num[aria-expanded="true"] { border-color: var(--accent-line); }
.b3-num[aria-expanded="true"]::before { transform: scaleX(1); }

/* the glyph heads the figure — same construction as `.tile` and `.b6-tile`,
   so all three families of result cards on the page read as one system */
.b3-num > .ic { margin-bottom: clamp(14px, 1.6vw, 20px); }

.b3-n {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0;
  color: var(--fg);
  font-size: var(--t-num);
  font-weight: 700;
  line-height: .95;
  letter-spacing: -.035em;
  font-variant-numeric: tabular-nums;
}
/* D2 — the unit suffix («млн+», «млрд+») and the bare «+» share ONE rule, so
   every tile reads as the same system; the «0 →» prefix is the quiet one */
.b3-n .num-pre {
  color: var(--ink-2);
  font-size: .46em;
  letter-spacing: -.01em;
  /* .26em, not .16/.12 (2026-09-08, art direction): the figure is set at
     -.035em tracking and the suffix at 0, so at .12em the unit read as if it
     were glued to the last digit. Measured gap: qa/phase13/b3.json. */
  margin-right: .26em;
}
.b3-n .num-u {
  color: var(--accent-deep);
  font-size: .46em;
  font-weight: 700;
  letter-spacing: 0;
  margin-left: .26em;
}
/* the fifth tile prints «0 → 100 000+» — six digits plus a prefix and a suffix
   on one baseline, so it steps down or it wraps */
.b3-num:nth-child(5) .b3-n { font-size: calc(var(--t-num) * .62); }
/* …but only on the half-width card. From 1024 up it is either full width (to
   1180) or a 3-of-6 span with room to spare, and .62 made the biggest number of
   the five the smallest thing in the panel. .92 still sets it on one line at
   1280 / 1440 / 1920 (qa/phase13/b3.json). */
@media (min-width: 1024px) {
  .b3-num:nth-child(5) .b3-n { font-size: calc(var(--t-num) * .92); }
}

.b3-num-l {
  margin-top: 16px;
  color: var(--fg-2);
  font-size: var(--t-small);
  line-height: 1.5;
  max-width: 34ch;
}
/* the chip inset is the shared one now (main.css §2) — no per-block override */
.b3-num:hover .tile-chip, .b3-num:focus-visible .tile-chip { color: var(--accent-tx); }
.b3-num:hover .tile-chip .why-cross,
.b3-num:focus-visible .tile-chip .why-cross { transform: rotate(90deg); }
.b3-num[aria-expanded="true"] .tile-chip .why-cross { transform: rotate(45deg); }

.b3-note { margin-top: clamp(26px, 3vw, 36px); color: var(--ink-2); }

@media (max-width: 1180px) {
  .b3-nums { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .b3-num { grid-column: span 2; }
  .b3-num:nth-child(4), .b3-num:nth-child(5) { grid-column: span 2; }
  .b3-num:nth-child(5) { grid-column: span 4; }
  .b3-n { font-size: clamp(46px, 6vw, 78px); }
}
@media (max-width: 899px) {
  .b3-intro { grid-template-columns: minmax(0, 1fr); gap: 24px; }
  .b3-person { grid-template-columns: 116px minmax(0, 1fr); gap: 20px; }
  .b3-por { width: 116px; height: 158px; }
  .b3-desc { grid-column: 1 / -1; padding-top: 0; }
  .b3-nums { grid-template-columns: minmax(0, 1fr); }
  .b3-num, .b3-num:nth-child(4), .b3-num:nth-child(5) { grid-column: span 1; }
  .b3-n { font-size: clamp(48px, 15vw, 72px); }
  .b3-num-l { max-width: none; }
}


/* ============================================================
   TABLET (B0 cover geometry)
   ============================================================ */

/* B6 — at 1024 the caption ran «…ПРЕДЛОЖЕНИЕ · КРЕАТИВНОЕ И МЕДИЙНОЕ» and left
   «ПРОДЮСИРОВАНИЕ» alone on line two. 34em is wider than «Коммерческое
   предложение ·» (~19em) and narrower than the whole caption (~45em), so the
   only break point left is the one after the separator. */
@media (max-width: 1180px) {
  .b0 .eyebrow .b0-eb-t { max-width: 34em; }
}

@media (max-width: 1180px) and (min-width: 900px) {
  :root { --copy-max: min(600px, 52vw); }
  /* «Метода Тальписа» is 8.61px wide per 1px of Prata; at this range the copy
     column is min(600px, 52vw), so 5.35vw is the largest size that never wraps */
  .b0-title { font-size: clamp(40px, 5.35vw, 66px); }
  /* the panel gets narrower faster than it gets shorter — pull his face in a
     little so it stays clear of the headline column. 139 % is the old 210 %
     through the 0.66 source crop. */
  .b0 { --a-h: 139%; --a-lx: 60%; --a-ly: 20%; }
}


/* ============================================================
   MOBILE — real layout, not a shrunken desktop
   B0 stays a 100dvh diptych: faces side by side 50/50, names stacked
   (DECISIONS: "мобайл = 100dvh diptych с лицами 50/50, имена столбиком")
   ============================================================ */

@media (max-width: 899px) {

  /* The panel is very tall and narrow here, so both portraits are height-
     limited: her frame renders at exactly the panel height whatever the panel
     width does, and his stays on `--a-h`. 162 % is the old 245 % through the
     0.66 source crop. The panel geometry and the dissolve are the SAME rules
     as the desktop — a phone diptych was always 50/50, and the round-2 join is
     50/50 everywhere, so the whole mobile panel block collapsed to this one
     line. */
  .b0 { --a-h: 151%; --a-lx: 50%; --a-ly: 19%; }

  /* the copy block is taller relative to the screen, so the faces have to
     dissolve higher up */
  .b0-grade {
    background: linear-gradient(to top,
      var(--bg) 6%,
      rgba(14,15,18,.97) 32%,
      rgba(14,15,18,.80) 46%,
      rgba(14,15,18,.30) 60%,
      rgba(14,15,18,0) 74%);
  }
  .b0-vig {
    background: linear-gradient(to right,
      rgba(14,15,18,.42) 0%, rgba(14,15,18,.10) 30%,
      rgba(14,15,18,.10) 70%, rgba(14,15,18,.42) 100%);
  }

  /* two clean lines, no orphaned separator. Sizes are measured: at 390px the
     text column is 314px and «Креативное и медийное продюсирование» is 285px
     at 10.5px/.11em, so it can never become a third line. */
  .b0 .eyebrow {
    font-size: 10.5px; letter-spacing: .11em;   /* C8 — 2.7vw was 9.7px at 360 */
    max-width: none;                 /* the desktop 52vw cap would force 4 lines */
  }
  .b0 .eyebrow .b0-eb-t { max-width: none; }
  .b0 .eyebrow .rule { flex-basis: 22px; }
  /* `align-items` is `baseline` in the base rule, which in a column shrinks each
     line to its own content and parks it left — so the centred block has to say
     `center` here explicitly */
  .b0 .eyebrow .b0-eb-t { display: flex; flex-direction: column; gap: 5px; align-items: center; }
  .b0 .eyebrow .dot { display: none; }

  .b0-copy { max-width: none; padding-bottom: clamp(20px, 4vh, 34px); }
  /* 9.7vw is the measured ceiling: «Метода Тальписа» is 8.61px per 1px of
     Prata and `nowrap` means an oversized line would be clipped, not wrapped */
  .b0-title { font-size: clamp(32px, 9.7vw, 66px); }
  .b0-sub { max-width: 26ch; font-size: 17px; }

  .b0-names { flex-direction: column; gap: 14px; }
  .b0-name { max-width: none; }
  .b0-name--right { text-align: left; border-top: 1px solid var(--line); padding-top: 14px; }
  .b0-name-r { letter-spacing: .1em; }

  /* --- B1 --- */
  .b1-q { font-size: clamp(38px, 10.4vw, 56px); max-width: 12ch; }
  .b1-a { font-size: clamp(27px, 7.2vw, 40px); max-width: 16ch; }

  .b1-faces {
    /* no pin on a phone, so the strip needs its own breathing room */
    margin-top: clamp(38px, 9vw, 60px);
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 8px;
    row-gap: 16px;
  }
  .b1-face { max-width: none; }
  .b1-face:nth-child(even) { transform: translateY(12px); }

  .b1-aside { padding-top: 0; }
  .b1-h2 { max-width: none; }

  .b1-foot { display: block; }
  .b1-foot .rv:last-child { margin-top: 32px; }
  .why { padding: 0 16px 0 18px; gap: 12px; font-size: 11.5px; }

  /* --- chain becomes a vertical rail ---
     The same two pseudo-elements plus the glyph, rotated. The node is a 32px
     ring with a 16px icon, vertically centred on its item; the label sits to
     its right and the segment runs DOWN from this ring's bottom edge to the
     next ring's top edge, so the rail passes through every node centre and
     ends exactly on the last one. The index is hidden here — a phone rail has
     to stay laconic (the client's own constraint). */
  .chain {
    --node: 32px;
    --row: 18px;          /* row gap between items */
    display: block;
    margin-top: 22px;
  }
  .chain--tight, .b7-steps { --node: 32px; }

  .chain-i {
    padding-top: 0;
    padding-left: calc(var(--node) + 14px);
    min-height: var(--node);
    display: flex;
    align-items: center;
  }
  .chain-i + .chain-i { margin-top: var(--row); }

  .chain-i::before {
    left: calc(var(--node-c) - .5px);   /* rail centre x = the node centre */
    right: auto;
    top: calc(50% + var(--node) / 2);
    bottom: calc(-1 * var(--row));
    width: 1px; height: auto;
    transform: scaleY(var(--seg, 1));
    transform-origin: 50% 0;
  }
  .chain-i::after {
    left: 0;
    top: 50%;
    transform: translateY(-50%) scale(var(--node-s, 1));
  }
  /* the desktop `.chain--tight` / `.b7-steps` icon rules are two classes deep,
     so a bare `.chain-i > .ic` here would lose to them — all three are stated */
  .chain .chain-i > .ic,
  .chain--tight .chain-i > .ic,
  .b7-steps .chain-i > .ic {
    --ic: 16px;
    --icw: 1.75;
    left: calc(var(--node-c) - var(--ic) / 2);
    top: 50%;
    transform: translateY(-50%) scale(var(--node-s, 1));
  }
  .chain > .chain-i:last-child { --halo: 4px; }
  .b6-contour > .chain-i:last-child { --halo: 0px; }

  .chain-n { display: none; }
  /* the `.chain--tight` selector is stronger than a bare `.chain-t`, so the
     phone size has to be stated for both or the tight chains stay at 15px */
  .chain-t,
  .chain--tight .chain-t {
    margin-top: 0;
    font-size: 14.5px;
    line-height: 1.35;
    letter-spacing: .06em;
  }
  .b7-steps .chain-t { font-size: 14px; }
}

/* B4 — at ≤760 the panel is a narrow slot and putting Арут's face at 50 % of it
   pushed his head into the right vignette. Виктория needs no rule of her own
   any more: the round-2 anchor system pins her face point to (42 %, 19 %) at
   every width by construction, which is what the old `object-position: 42% 16%`
   was approximating. */
@media (max-width: 760px) {
  .b0 { --a-lx: 46%; }
}

/* B5 — on a short desktop (1366×640 laptops) the 6.1vw headline grew to a
   260px-tall box that landed across her mouth. Height, not width, is the
   constraint here, so the cap is on the viewport height. */
@media (min-width: 900px) and (max-height: 720px) {
  .b0-title { font-size: clamp(38px, 4.4vw, 58px); }
}

@media (max-width: 600px) {
  /* on a phone Виктория's panel crops down to a very tight close-up, so his
     frame has to zoom in further to keep the two heads on the same scale.
     162 % is the old 245 % through the 0.66 source crop. */
  .b0 { --a-h: 162%; }
}

@media (max-width: 420px) {
  .why-label { white-space: normal; text-align: left; line-height: 1.25; }
}

/* C8 — 10.5px is the floor for a label, so the caption buys its room back
   from the tracking instead of from the type size. */
@media (max-width: 400px) {
  .b0 .eyebrow { letter-spacing: .08em; }
}


/* ============================================================
   REDUCED MOTION
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  .b1-pin { min-height: 0 !important; padding-left: 0 !important; margin-left: 0 !important; }
  .b1-sticky { position: static !important; }
  .b1-rail { display: none !important; }
  .b1-face:nth-child(even) { transform: translateY(20px) !important; }
}
@media (prefers-reduced-motion: reduce) and (max-width: 899px) {
  .b1-face:nth-child(even) { transform: translateY(12px) !important; }
}


/* ==========================================================================
   PHASE2: B4–B9. Reuse: `.blk` + `.wrap` + `.eyebrow` + `.statement` + `.lead`
   from main.css, `.chain` / `.chain-i` from this file, `.tile` / `.why` /
   `.tile-chip` / `.b3-num` patterns. B6 is a paper section.
   ========================================================================== */


/* ============================================================
   SHARED — a paper card standing inside a dark section (B7, B8)
   Same token swap as `.blk--paper`, scoped to a card instead of a section.
   ============================================================ */

.paper-card {
  --bg:      var(--paper);
  --bg-2:    var(--paper-2);
  --bg-3:    #E3DED5;
  --fg:      var(--ink);
  --fg-2:    #4A4A47;
  --fg-3:    #66645F;
  --line:    rgba(18,19,22,.12);
  --line-2:  rgba(18,19,22,.22);
  --accent:      var(--accent-deep);
  /* C3 — accent-deep is 4.87:1 on bare paper but only 4.20:1 once a tile
     tints its background with --accent-soft on hover, so small accent TEXT
     on paper goes one step darker again. */
  --accent-tx:   #A82049;
  --accent-soft: rgba(196,42,90,.10);
  --accent-line: rgba(196,42,90,.42);
  padding: clamp(28px, 3.4vw, 48px);
  border-radius: var(--radius-m);
  background: var(--paper);
  color: var(--ink);
  /* B1 — the old 0 30px 90px black blur pulled the ground under the card from
     L 15 down to L 10 and was still 4 L deep 66px away, which read as a smudge
     rather than a lift. A hairline + a tight contact shadow + one long,
     heavily-offset blur lifts the card without staining the section.
     Measured 30px under the B7 card, ground L 15.85:
       old 0 30px 90px .45      → 11.87  (Δ3.88)
       0 28px 56px -34px .9     → 13.90  (Δ1.95)  still over the 1.5 L gate
       0 24px 48px -42px .9     → 14.85  (Δ1.00)  ← this one
     The contact darkness at 10px is Δ2.05, so the card still reads as lifted. */
  box-shadow:
    0 0 0 1px rgba(244,241,236,.10),
    0 2px 4px rgba(0,0,0,.5),
    0 24px 48px -42px rgba(0,0,0,.9);
}


/* ============================================================
   B4 — ПРОДВИЖЕНИЕ И РОСТ
   ============================================================ */

.b4-intro {
  margin-top: clamp(34px, 4vw, 56px);
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(30px, 4vw, 72px);
  align-items: end;
}
.b4-lead { padding-bottom: 6px; }

/* --- decomposition: 1 выпуск → десятки единиц --- */
.b4-decomp {
  margin-top: clamp(52px, 6vw, 88px);
  display: grid;
  grid-template-columns: clamp(150px, 16vw, 210px) clamp(40px, 5vw, 88px) minmax(0, 1fr);
  align-items: center;
}
.b4-src {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  min-height: clamp(118px, 13vw, 168px);
  padding: clamp(20px, 2.2vw, 30px);
  border: 1px solid var(--line-2);
  border-radius: var(--radius-m);
}
.b4-src-n {
  color: var(--fg);
  font-size: clamp(38px, 4.2vw, 60px);
  font-weight: 700;
  line-height: .95;
  letter-spacing: -.035em;
  font-variant-numeric: tabular-nums;
}
.b4-src-l {
  color: var(--fg-3);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
}
/* A4 — the arm is drawn, not faded in: the rule lives on ::before so it can be
   scaled out of the source box, and the accent dot rides on ::after so it is
   never dragged by that scale. Both custom properties default to their FINAL
   value; main.css §10 pre-hides them only when GSAP can actually run. */
.b4-arm { position: relative; height: 1px; background: none; --b4-draw: 1; --b4-dot: 1; }
.b4-arm::before {
  content: "";
  position: absolute; inset: 0;
  background: var(--line-2);
  transform: scaleX(var(--b4-draw));
  transform-origin: 0 50%;
}
.b4-arm::after {
  content: "";
  position: absolute; right: 0; top: 50%;
  margin-top: -3px;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent);
  opacity: var(--b4-dot);
}
.b4-grid {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: clamp(5px, .6vw, 9px);
}
.b4-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  min-height: clamp(32px, 3.4vw, 48px);
  padding: 4px 3px;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--bg-3);
  color: var(--fg-2);
  /* E6 — .8vw rendered 9px at 390. 10.5px is the floor for a label; the grid is
     already 3 columns below 900px, so «НАРЕЗЧИКИ» (~76px) still fits a 113px
     column at 360. */
  font-size: clamp(10.5px, .8vw, 11px);
  font-weight: 600;
  letter-spacing: .07em;
  line-height: 1.2;
  text-align: center;
  text-transform: uppercase;
}
/* D3 / B2 — the six channel names are stated once, on row 1. Rows 2-4 keep the
   tiles (the point IS the multiplication) but the words go faint and the row
   TAPERS instead of vanishing: `color: transparent` + opacity .24 left rows 2-4
   at L 23.9/20.9/18.9 on a 15.0 ground, i.e. an invisible skeleton. Faint text
   plus a shrinking row reads as "one becomes many" and stays on screen.
   `filter` and not `opacity`: the GSAP reveal owns `opacity`.
   Desktop only — the mobile grid states the same rule for its 3-column set. */
@media (min-width: 900px) {
  .b4-grid .b4-cell:nth-child(n+7) {
    color: rgba(169,167,161,.30);
    font-size: 10px;
    min-height: clamp(26px, 2.4vw, 34px);
  }
  .b4-grid .b4-cell:nth-child(n+13) { min-height: clamp(20px, 1.9vw, 26px); }
  .b4-grid .b4-cell:nth-child(n+7):nth-child(-n+12)  { filter: opacity(.62); }
  .b4-grid .b4-cell:nth-child(n+13):nth-child(-n+18) { filter: opacity(.50); }
  .b4-grid .b4-cell:nth-child(n+19) { filter: opacity(.40); }
}
.b4-cap { margin-top: clamp(16px, 1.8vw, 22px); }

/* --- the seven items as an editorial list --- */
.b4-list { margin-top: clamp(64px, 8vw, 110px); display: grid; }
.b4-item {
  display: grid;
  grid-template-columns: clamp(54px, 7vw, 104px) minmax(0, 1fr);
  gap: clamp(14px, 2vw, 30px);
  padding: clamp(26px, 3vw, 42px) 0;
  border-top: 1px solid var(--line);
}
.b4-item:last-child { border-bottom: 1px solid var(--line); }
.b4-item-n {
  display: flex;
  align-items: center;
  gap: 10px;
  padding-top: .3em;
  color: var(--fg-3);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: .14em;
  font-variant-numeric: tabular-nums;
}
/* two columns from 900 up: the numeral lives in a 56–104px gutter, so the
   glyph goes ABOVE it rather than beside it. Below 900 the item is one column
   and the index row runs across the full measure, so the two sit side by side. */
@media (min-width: 900px) {
  .b4-item-n { flex-direction: column; align-items: flex-start; gap: 12px; }
}
.b4-item-t {
  color: var(--fg);
  font-size: var(--t-h3);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -.01em;
}
.b4-item-d {
  margin-top: 14px;
  max-width: 62ch;
  color: var(--fg-2);
  font-size: var(--t-small);
  line-height: 1.55;
}
.b4-item-b > .why { margin-top: clamp(20px, 2.2vw, 28px); }
.b4-tag {
  display: inline-block;
  margin-left: 12px;
  padding: 5px 12px;
  vertical-align: middle;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  color: var(--fg-3);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
}

/* D4 — at >=1180px an item with two paragraphs sets the second one in a
   narrower right column, offset 8px down. Items with figures, a chip or a
   single paragraph keep one column (the `p + p` adjacency guarantees it). */
@media (min-width: 1180px) {
  .b4-item {
    grid-template-columns: 56px minmax(0, 1fr);
    column-gap: clamp(32px, 4vw, 64px);
  }
  .b4-item-b {
    display: grid;
    grid-template-columns: minmax(0, 7fr) minmax(0, 4fr);
    column-gap: clamp(32px, 4vw, 64px);
    align-items: start;
  }
  .b4-item-b > * { grid-column: 1; }
  .b4-item-b > p.b4-item-d + p.b4-item-d {
    grid-column: 2;
    grid-row: 2;
    margin-top: 8px;
  }
  /* D3 — a grid item stretches by default, so `.why` (an inline-flex pill)
     was blown out to the full 7fr column: 706px of border around a 220px
     label. A chip has to be the width of its own text. */
  .b4-item-b > .why { justify-self: start; }
}

/* --- item 03: the two internal numbers, each with its case proof --- */
.b4-figs-l { margin-top: clamp(22px, 2.4vw, 30px); }
.b4-figs {
  margin-top: 12px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(12px, 1.4vw, 18px);
  max-width: 640px;
}
.b4-fig {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  padding: clamp(18px, 1.9vw, 24px) clamp(18px, 1.9vw, 24px) clamp(52px, 4.4vw, 62px);
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  background: var(--bg-2);
  color: var(--fg);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color .4s var(--ease);
}
.b4-fig::before {
  content: "";
  position: absolute; inset: 0 0 auto 0;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform .45s var(--ease);
}
.b4-fig:hover, .b4-fig:focus-visible,
.b4-fig[aria-expanded="true"] { border-color: var(--accent-line); }
.b4-fig:hover::before, .b4-fig:focus-visible::before,
.b4-fig[aria-expanded="true"]::before { transform: scaleX(1); }
.b4-fig:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.b4-fig-n {
  display: flex;
  align-items: baseline;
  gap: 0 .14em;
  font-size: clamp(30px, 3.4vw, 46px);
  font-weight: 700;
  line-height: .95;
  letter-spacing: -.035em;
  font-variant-numeric: tabular-nums;
}
.b4-fig-u { color: var(--accent); font-size: .36em; font-weight: 700; letter-spacing: 0; }
.b4-fig-l { max-width: 26ch; color: var(--fg-2); font-size: var(--t-small); line-height: 1.45; }
.b4-fig .tile-chip { left: clamp(18px, 1.9vw, 24px); bottom: clamp(18px, 1.9vw, 24px); font-size: 10.5px; }
.b4-fig:hover .tile-chip, .b4-fig:focus-visible .tile-chip { color: var(--accent-tx); }
.b4-fig:hover .tile-chip .why-cross,
.b4-fig:focus-visible .tile-chip .why-cross { transform: rotate(90deg); }
.b4-fig[aria-expanded="true"] .tile-chip .why-cross { transform: rotate(45deg); }


/* ============================================================
   B5 — ПУТЬ ЗРИТЕЛЯ · «пять сцен на одном пути»  (B5 redesign, 2026-09-08)

   Replaces the staircase (`--b5-step`, the tread/riser pseudo-elements and the
   `.b5-strip*` band under it). The five stages are now five SCENE PANELS that
   share one horizontal path line; the active one widens and opens its dossier
   inside its own panel. Nodes are still rendered from window.FUNNEL by main.js.

   Layout contract (desktop, >= 900px):
     .b5-path      positioning context; carries `--b5-line` (the path's y) and
                   the travelling light `.b5-pulse`
     .b5-scenes    5-column x 2-row grid. `[data-active="i"]` (set by main.js)
                   picks the column template, and the transition on
                   `grid-template-columns` is the panel-widening animation.
                   Row 1 = the panel header, row 2 = the dossier.
     .b5-scene     one panel, grid-row 1/-1 of its own column (the surface),
                   `::before` = the accent hairline on its top edge
     .b5-dos       ONE dossier element, moved between columns with `--b5-col`
                   (desktop) / rows with `--b5-row` (phone). It is a sibling of
                   the panels, never a child: the panels span both rows, so a
                   child could not be placed in row 2, and — the binding reason
                   — `#page`'s innerText order has to stay byte-identical to the
                   copy snapshot (qa/phase8/copy-diff.py), which has the stage
                   prose AFTER all five headers. One element is also what the
                   aria-live contract wants: a region that is never re-created.

   Phone (< 900px): the same grid, one column, ten rows — scene i in row 2i-1,
   the dossier in row 2i of the open stage. No `order`, so tab order = DOM
   order at every width.
   ============================================================ */

.b5-intro { margin-top: clamp(34px, 4vw, 56px); }

.b5-path {
  position: relative;
  margin-top: clamp(56px, 7vw, 100px);
  --b5-rail: 20px;                 /* phone: the rail's x */
  /* desktop: the path's y, from the panel top. 56 -> 48 (2026-09-08, loops
     round): the ring is 40px, so at 56 its top edge sat 36px below the panel
     edge against 24px of horizontal panel padding — the node read as hung, not
     inset. At 48 the ring's top inset is 28px, and the index below it keeps its
     designed 12px clearance from the ring's bottom (that gap is what fixes the
     +32px offset in `.b5-btn`'s padding: it is the ring radius plus 12). */
  --b5-line: 48px;
  --b5-ring: 32px;                 /* phone node */
  --b5-pad: 22px;                  /* phone: nothing; desktop: panel padding */
}

.b5-scenes {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  /* the path draws itself; final value here, pre-hidden only behind
     `.js:not(.rm):not(.nogsap)` in main.css §10 */
  --b5-draw: 1;
}

/* ---- THE PATH -------------------------------------------------------------
   One continuous hairline. `z-index: 1` puts it OVER the panel surfaces (the
   panels are `position: relative` with `z-index: auto`, so they never trap it
   in a stacking context of their own); the node rings sit at `z-index: 2` and
   stop the line at their edge. */
.b5-scenes::before {
  content: "";
  position: absolute;
  z-index: 1;
  left: var(--b5-rail); top: calc(10px + var(--b5-ring) / 2);
  bottom: calc(10px + var(--b5-ring) / 2);
  width: 1px;
  background: var(--line-2);
  transform: scaleY(var(--b5-draw));
  transform-origin: 50% 0;
}

/* explicit rows: no auto-placement, so the dossier can be dropped into the row
   under the open stage without touching the DOM */
.b5-scene:nth-child(1) { grid-row: 1; }
.b5-scene:nth-child(2) { grid-row: 3; }
.b5-scene:nth-child(3) { grid-row: 5; }
.b5-scene:nth-child(4) { grid-row: 7; }
.b5-scene:nth-child(5) { grid-row: 9; }
.b5-dos { grid-row: var(--b5-row, 2); min-width: 0; }

.b5-scene {
  position: relative;
  border-radius: 14px;
  transition: border-color .4s var(--ease), background-color .4s var(--ease);
}
/* The rail's tail. `.b5-scenes::before` spans the whole grid and stops 26px
   above its bottom edge, which on a phone is 26px above the LAST PANEL's bottom
   — a 60-90px stub of line hanging below the «05 ВЫРУЧКА» node with nothing on
   it. This masks the rail from the last node's centre down; the ring itself is
   `background: var(--bg)` and already stops the line at its own edge, so the
   two together end the path exactly on the last node.
   `:nth-child(5)`, not `:last-child`: the shared dossier `#b5-dos` is the last
   child of `.b5-scenes`, so `:last-child` matches nothing here. */
.b5-scene:nth-child(5)::after {
  content: "";
  position: absolute;
  z-index: 1;                        /* same layer as the rail, painted later */
  left: calc(var(--b5-rail) - 2px);
  width: 5px;
  top: calc(10px + var(--b5-ring) / 2);
  bottom: 0;
  background: var(--bg);
  pointer-events: none;
}

.b5-btn {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 7px;
  width: 100%;
  padding: 12px 0 18px calc(var(--b5-rail) + var(--b5-ring) / 2 + 18px);
  border: 0;
  background: none;
  color: var(--fg);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.b5-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 12px; }

/* ---- the node on the path ---- */
.b5-node {
  position: absolute;
  z-index: 2;
  left: calc(var(--b5-rail) - var(--b5-ring) / 2);
  top: 10px;
  display: grid;
  place-items: center;
  width: var(--b5-ring); height: var(--b5-ring);
  border: 1px solid var(--accent-line);
  border-radius: 50%;
  background: var(--bg);
  transition: box-shadow .45s var(--ease);
}
.b5-node > .ic { --ic: 16px; color: var(--fg-3); transition: color .45s var(--ease); }
/* the active node carries the same mark the terminal chain node carries: the
   raspberry is on the GLYPH, the ring stays --accent-line, so the strong-accent
   count per viewport does not move (SPEC §2, DECISIONS "Icon round") */
.b5-scene.is-on .b5-node { box-shadow: 0 0 0 5px var(--accent-soft); }
.b5-scene.is-on .b5-node > .ic { color: var(--accent); }

/* ---- header type ---- */
.b5-n {
  color: var(--fg-3);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: .14em;
  font-variant-numeric: tabular-nums;
}
.b5-state {
  font-family: var(--font-dp);
  font-weight: 400;
  font-size: clamp(24px, 2.1vw, 32px);
  line-height: 1.05;
  letter-spacing: 0;
  color: var(--fg);
  /* the panel is narrow when it is not the open one: the word shrinks with the
     clamp, it never breaks mid-word */
  overflow-wrap: normal;
  hyphens: none;
}
.b5-stage {
  color: var(--fg-3);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.b5-lead { color: var(--fg-2); font-size: var(--t-small); line-height: 1.45; }

/* the affordance: every panel says at rest that it opens something.
   The chevron points DOWN in every state — it points at the dossier below it,
   and a rotation would claim the panel closes. */
.b5-cue {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 4px;
  color: var(--fg-3);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  transition: color .35s var(--ease);
}
.b5-cue svg { transition: transform .35s var(--ease); }
.b5-btn:hover .b5-cue,
.b5-btn:focus-visible .b5-cue,
.b5-scene.is-on .b5-cue { color: var(--accent-tx); }
.b5-btn:hover .b5-cue svg,
.b5-btn:focus-visible .b5-cue svg,
.b5-scene.is-on .b5-cue svg { transform: translateY(2px); }

/* ---- the dossier ---- */
/* `.vh` is `white-space: nowrap` on one line; three flex items make the stage
   line three lines again, which is how a screen reader reads it out and how the
   copy snapshot (qa/phase8/copy-diff.py) has always had it */
.b5-dos-h { display: flex; }
.b5-dos-in {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .4s var(--ease) .22s, transform .4s var(--ease) .22s;
}
.b5-dos-in.is-in { opacity: 1; transform: none; }
.b5-dos-txt p { max-width: 46ch; color: var(--fg-2); font-size: var(--t-small); line-height: 1.55; }
.b5-dos-txt p + p { margin-top: 11px; }
.b5-meta {
  margin-top: clamp(16px, 1.6vw, 22px);
  padding-top: clamp(14px, 1.4vw, 18px);
  border-top: 1px solid var(--line);
  display: grid;
  gap: 16px;
  align-content: start;
}
.b5-meta dt {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--fg-3);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.b5-meta dt > .ic { color: var(--fg-3); }
.b5-meta dd { margin: 6px 0 0; color: var(--fg); font-size: 16px; line-height: 1.5; }

/* ---- the travelling light -------------------------------------------------
   The one looped animation on the page: CSS only, transform only, and paused
   whenever B5 is off-screen (main.js toggles `.is-inview` from an
   IntersectionObserver). Never a GSAP tween — the idle-tween gate stays 0.
   The moving element is the full length of the path and carries a 96px band in
   its background, so the travel distance needs no measurement in JS. */
.b5-pulse {
  position: absolute;
  z-index: 3;
  left: var(--b5-rail); top: 0; bottom: 0;
  width: 1px;
  overflow: hidden;
  pointer-events: none;
}
.b5-pulse > i {
  position: absolute;
  inset: 0;
  opacity: .85;
  background-repeat: no-repeat;
  background-position: center;
  background-image: linear-gradient(180deg, transparent, var(--accent) 50%, transparent);
  background-size: 100% 96px;
  transform: translateY(-54%);
  animation: b5-pulse-v 7s linear infinite;
  animation-play-state: paused;
}
.b5-path.is-inview .b5-pulse > i { animation-play-state: running; }
@keyframes b5-pulse-v { from { transform: translateY(-54%); } to { transform: translateY(54%); } }
@keyframes b5-pulse-h { from { transform: translateX(-54%); } to { transform: translateX(54%); } }

html.rm .b5-pulse { display: none; }
@media (prefers-reduced-motion: reduce) { .b5-pulse { display: none; } }


/* ============================ DESKTOP ============================ */
/* 1180, not 900. Two measurements set this line. Below it the four closed
   panels are <=137px wide and «АУДИТОРИЯ» cannot be set above 14px inside one;
   and the open panel is <=328px, which wraps the stage prose to seven lines and
   pushes the dossier to 475px — 175px taller than the shortest stage, i.e. a
   175px jump under the cursor on hover. 1024 and 768 get the phone rail, which
   is also the better answer on a tablet in landscape. */
@media (min-width: 1180px) {
  /* the phone rail's tail mask has nothing to mask here — the path is horizontal */
  .b5-scene:nth-child(5)::after { display: none; }
  .b5-path {
    --b5-pad: clamp(18px, 1.7vw, 24px);
    --b5-ring: 40px;
    --b5-gap: 12px;
    /* Fixed rows, both of them, and both for the same reason: nothing under the
       cursor may move while the cursor is only moving sideways.
       h1 — the header block. 280px clears the tallest natural header measured
       (274px at 1920), so the five cues sit on one line at every width.
       h2 — the dossier. Never shorter than the tallest stage at this width, so
       hovering from a long stage to a short one does not shorten the section. */
    /* 2026-09-08 (loops round, L4). Both numbers were re-cut against a probe
       that RELEASES the fixed height and reads what the header actually needs
       (`qa/phase11/b5nat.py`), because with a fixed track the natural height is
       invisible.

       h1 — the tallest natural header, at every width and in every open state,
       is 270px at 1440 / 271px at 1920 / 289px at 1180 (the closed panel 01
       wraps «Короткие видео + аффилиаты» to three lines in a 118px column).
       Moving the path 8px up takes 14px off all of them. 1180-1279 keeps its
       own value, which also fixes a defect nobody had measured: at 280px the
       closed panel 01's cue used to hang 9px below the other four.
       h2 — 364px left the LONGEST dossier (stage 01, 336px of content at
       1440/1920) with 28px under it. 380 gives it 44px, inside the 40-56 the
       design asked for, and the four shorter dossiers keep the slack that the
       equal-section-height gate requires. */
    --b5-h1: 266px;
    --b5-h2: 380px;
  }
  /* the three-line lead only happens below 1280 */
  @media (max-width: 1279.98px) { .b5-path { --b5-h1: 284px; } }
  .b5-scenes {
    grid-template-columns: repeat(5, minmax(0, 1fr));
    grid-template-rows: var(--b5-h1) minmax(var(--b5-h2), auto);
    column-gap: var(--b5-gap);
    /* THE widening. `grid-template-columns` interpolates in all three engines;
       where it does not, the panel snaps to its new width and nothing else
       changes. */
    transition: grid-template-columns .6s var(--ease);
  }
  .b5-scenes[data-active="0"] { grid-template-columns: minmax(0, 2.4fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); }
  .b5-scenes[data-active="1"] { grid-template-columns: minmax(0, 1fr) minmax(0, 2.4fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); }
  .b5-scenes[data-active="2"] { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 2.4fr) minmax(0, 1fr) minmax(0, 1fr); }
  .b5-scenes[data-active="3"] { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 2.4fr) minmax(0, 1fr); }
  .b5-scenes[data-active="4"] { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 2.4fr); }

  .b5-scenes::before {
    left: 0; right: 0; top: var(--b5-line); bottom: auto;
    width: auto; height: 1px;
    transform: scaleX(var(--b5-draw));
    transform-origin: 0 50%;
  }

  /* ONLY the open scene spans both rows (2026-09-08, art direction). Every
     panel used to, which put four 646px-tall empty surfaces beside the one that
     had something in it — the closed panels are a header, so they are a header's
     height. The section height does not move: both tracks are fixed (266 + 380),
     the dossier still lands in row 2 of the open column, and the node rings are
     positioned off `--b5-line` inside row 1, so nothing on the path moves.
     DEVIATION from the brief: no new `is-open` class. `activate()` in main.js
     already toggles `is-on` on the same element at the same moment, and a second
     class set and unset in lockstep with the first is dead weight. */
  .b5-scene {
    border: 1px solid var(--line);
    background: var(--bg-2);
    overflow: hidden;
    grid-row: 1;
  }
  /* `grid-row` is repeated on every column rule on purpose: the phone layout
     puts each panel on its own explicit row through the SAME `:nth-child`
     selector, and `.b5-scene { grid-row: 1 }` above cannot outrank it —
     measured: without this the five panels stayed on rows 1-9 and #b5 was
     2497px tall at 1440 instead of 646. */
  .b5-scene:nth-child(1) { grid-column: 1; grid-row: 1; }
  .b5-scene:nth-child(2) { grid-column: 2; grid-row: 1; }
  .b5-scene:nth-child(3) { grid-column: 3; grid-row: 1; }
  .b5-scene:nth-child(4) { grid-column: 4; grid-row: 1; }
  .b5-scene:nth-child(5) { grid-column: 5; grid-row: 1; }
  .b5-scene.is-on { grid-row: 1 / -1; }
  /* the accent hairline draws along the open panel's top edge — the `.tile`
     construction. `--accent-line`, not `--accent`: at full strength this is a
     fourth strong accent object in the B5 viewport (SPEC §2 caps it at ~3). */
  .b5-scene::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 1px;
    background: var(--accent-line);
    transform: scaleX(0);
    transform-origin: 0 50%;
    transition: transform .45s var(--ease);
    z-index: 3;
  }
  .b5-scene.is-on { border-color: var(--line-2); }
  /* the focusable element is the 280px header, but the object the eye sees is
     the whole panel, so the ring is drawn around the panel where `:has` exists.
     Without `:has` the whole rule set is dropped and the button keeps its own
     ring — never no ring. */
  .b5-scene:has(.b5-btn:focus-visible) {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
  }
  .b5-scene:has(.b5-btn:focus-visible) .b5-btn { outline: 0; }
  .b5-scene.is-on::before { transform: scaleX(1); }
  /* the closed panels step back — on the text, not on the surface */
  .b5-scene:not(.is-on) .b5-btn > span:not(.b5-node) { opacity: .78; }
  /* `:not(.b5-node)` and the `.b5-state` restatement are both load-bearing:
     `.b5-btn > span` (0,1,1) outranks `.b5-node` and `.b5-state` (0,1,0), so a
     bare rule here silently deletes the ring's box-shadow transition and the
     title's size transition. */
  .b5-btn > span:not(.b5-node) { transition: opacity .4s var(--ease); }
  .b5-btn > span.b5-state {
    transition: opacity .4s var(--ease), font-size .6s var(--ease);
  }

  .b5-btn {
    height: var(--b5-h1);
    align-items: stretch;
    gap: 8px;
    /* the index sits 32px under the path line */
    padding: calc(var(--b5-line) + 32px) var(--b5-pad) 0;
  }
  .b5-node {
    left: var(--b5-pad);
    top: calc(var(--b5-line) - var(--b5-ring) / 2);
    /* the PANEL colour, so the path stops at the ring instead of running under
       it; on a phone there is no panel and the ring keeps the page colour */
    background: var(--bg-2);
  }
  .b5-node > .ic { --ic: 20px; }
  .b5-scene.is-on .b5-node { box-shadow: 0 0 0 6px var(--accent-soft); }
  .b5-cue { margin-top: auto; padding-bottom: 22px; align-self: flex-start; }

  /* the open scene's title is the enlarged frame of the filmstrip; a closed
     spine is 192px wide and «АУДИТОРИЯ» has to fit inside it unbroken */
  .b5-scene:not(.is-on) .b5-state { font-size: clamp(14px, 1.34vw, 20px); }

  .b5-dos {
    grid-row: 2;
    grid-column: var(--b5-col, 1);
    padding: 0 var(--b5-pad) 26px;
    overflow: hidden;
  }
  .b5-dos-txt p { max-width: 54ch; }
  .b5-meta {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    grid-auto-flow: column;
    gap: 0 clamp(20px, 2.4vw, 34px);
  }

  .b5-pulse {
    left: 0; right: 0; top: var(--b5-line); bottom: auto;
    width: auto; height: 1px;
  }
  .b5-pulse > i {
    background-image: linear-gradient(90deg, transparent, var(--accent) 50%, transparent);
    background-size: 96px 100%;
    transform: translateX(-54%);
    animation-name: b5-pulse-h;
  }
}


/* ============================ PHONE ============================ */
@media (max-width: 1179.98px) {
  .b5-dos {
    padding: 2px 0 clamp(20px, 3vw, 28px) calc(var(--b5-rail) + var(--b5-ring) / 2 + 18px);
  }
  .b5-dos-in {
    padding: clamp(18px, 4vw, 24px);
    border: 1px solid var(--line);
    border-left: 2px solid var(--accent-line);
    border-radius: var(--radius-m);
    background: var(--bg-2);
  }
  .b5-meta { margin-top: 18px; }
}

/* B5 has to fit one screen on a 1440x900 laptop, which is what the section is
   for: five scenes on one path, read at a glance. With the closed panels cut to
   row 1 the panel block shrank by 380px, so the only two things still pushing
   the section past the fold were the section's own top padding and a statement
   set to the page-wide measure. Both are held back only where there is height
   to spend (min-height 800), so short laptops keep the shipped rhythm. */
@media (min-width: 1180px) and (min-height: 800px) {
  #b5 { padding-top: clamp(72px, 7vw, 96px); }
  #b5 .statement { max-width: 18ch; }
}


/* ============================================================
   B6 — ЗОНЫ ОТВЕТСТВЕННОСТИ  (paper section)
   ============================================================ */

.b6-intro {
  margin-top: clamp(34px, 4vw, 56px);
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(30px, 4vw, 72px);
  align-items: end;
}
.b6-contour { margin-top: clamp(44px, 5vw, 76px); }

/* --- THE PROTECTIVE LAYER («защитный слой», ПОПАП 6A) ----------------------
   The frame around the whole scheme. 2026-09-08 (loops round): it used to be
   `border: 1px dashed`, which cannot flow — a border dash has no offset to
   animate. It is now ONE `<svg><rect>`; the dash brightens to `--accent-line`
   and travels while the ⊕ chip that names it is hovered or focused, which is
   the only place on the page where a decorative line answers a control.
   `pointer-events: none`, `aria-hidden` — it is a picture of the chip's word. */
.b6-scheme {
  position: relative;
  /* A8 — the connectors grow OUT of the centre label; final value here. */
  --b6-draw: 1;
  margin-top: clamp(70px, 8vw, 116px);
  /* D6 — the bottom padding used to leave a ~59px void under «Подрядчики»;
     it now matches the side padding, so the ring hugs the scheme */
  padding: clamp(48px, 5vw, 76px) clamp(22px, 3vw, 54px) clamp(26px, 3vw, 44px);
  border-radius: 24px;
  display: grid;
  /* 2026-09-08 (art direction, item 9): three equal tiles in ONE row with the
     core as a full-width band under them, instead of A | CORE | B over an
     orphan C. The old shape put C alone in row 2 with an empty cell either side
     — two dead quarters of the shield. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(28px, 3.4vw, 56px);
}
.b6-shield {
  position: absolute;
  inset: 0;
  /* an <svg> is a REPLACED element: with `inset: 0` and no width/height it
     falls back to its 300x150 intrinsic size instead of stretching */
  width: 100%; height: 100%;
  z-index: 0;
  /* the 1px stroke is centred on the rect edge, so half of it falls outside the
     SVG's own box — exactly where the old 1px border painted */
  overflow: visible;
  pointer-events: none;
}
.b6-shield rect {
  fill: none;
  stroke: rgba(18,19,22,.28);
  stroke-width: 1;
  stroke-dasharray: 6 5;
  transition: stroke .45s var(--ease);
}
/* 11px is one dash + one gap, so the flow is seamless at any speed.
   MEASURED, and the reason for `steps(11)`: this rect's perimeter is ~3700px,
   which a 6/5 dash pattern splits into ~336 sub-paths. Skia re-flattens all of
   them on every offset change, and a smooth 60fps flow costs 36 frames over
   32ms and 9 over 50ms in six seconds of hover, worst 150ms
   (qa/phase11/hoverperf.json, first run). Stepping the offset one whole dash at
   a time repaints 4.6 times a second instead of 60 — and marching ants are
   supposed to march, not glide. */
@keyframes b6-shield-flow { to { stroke-dashoffset: -11; } }
/* `.b6-consent` is a SIBLING of `.b6-shield`, so this needs no `:has()` and
   works in every engine, including the ones that do not have it yet. */
.b6-consent:hover ~ .b6-shield rect,
.b6-consent:focus-within ~ .b6-shield rect {
  stroke: var(--accent-line);
  animation: b6-shield-flow 2.4s steps(11) infinite;
}
html.rm .b6-shield rect { animation: none !important; }
@media (prefers-reduced-motion: reduce) { .b6-shield rect { animation: none !important; } }

.b6-consent { position: absolute; z-index: 2; top: 0; left: 50%; transform: translate(-50%, -50%); }
/* the chip interrupts the frame: it carries the paper behind it */
.b6-consent .why { background: var(--bg); }
.b6-consent .why > .ic { color: var(--fg-3); transition: color .35s var(--ease); }
.b6-consent .why:hover > .ic,
.b6-consent .why:focus-visible > .ic,
.b6-consent .why[aria-expanded="true"] > .ic { color: var(--accent-tx); }

/* DOM order is a / core / b / c and it stays that way — the copy snapshot reads
   `#page`'s innerText, which follows the DOM, so the grid does the moving. */
.b6-tile--a { grid-area: 1 / 1; }
.b6-tile--b { grid-area: 1 / 2; }
.b6-tile--c { grid-area: 1 / 3; }
.b6-core    { grid-area: 2 / 1 / 3 / -1; }

.b6-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  /* tile grammar (main.css §2) — this block's own scale is the shared one */
  padding: var(--tile-pad) var(--tile-pad) var(--tile-pad-b);
  border: 1px solid var(--line-2);
  border-radius: var(--tile-radius);
  background: var(--bg-2);
  color: var(--fg);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color .4s var(--ease), background-color .4s var(--ease);
}
/* the 2px content lift is the third leg of the grammar. The top-edge accent
   hairline that `.tile` and `.b3-num` draw on hover is deliberately NOT added
   here: `.b6-tile` answers a hover with an accent border AND an --accent-soft
   wash (the "линии оживают" of the ТЗ), and a raspberry hairline on top of a
   raspberry border on top of a raspberry wash is three marks for one event. */
.b6-tile > * { transition: transform .4s var(--ease); }
.b6-tile:hover > *, .b6-tile:focus-visible > * { transform: translateY(-2px); }
.b6-tile-t { font-size: var(--t-h3); font-weight: 600; line-height: 1.2; letter-spacing: -.01em; }
.b6-tile-s { color: var(--fg-2); font-size: var(--t-small); line-height: 1.45; }
/* D6a — REVERSES the earlier D6 call (accent chip at rest). Three tiles × (label
   + ⊕) is six raspberry objects in one paper viewport against the ~3 SPEC §2
   allows, and it was the single worst accent breach on the page. At rest the
   chip is muted like every other tile chip; the affordance is carried by the ⊕
   glyph, the tile border and the `cursor: pointer`, exactly as on `.tile`,
   `.b3-num` and `.b4-fig` — the three dark-stage controls that already read as
   clickable at --fg-3. Hover / focus / open still light it to --accent-tx. */
.b6-tile .tile-chip { color: var(--fg-3); }
.b6-tile .tile-chip .why-plus { color: var(--fg-3); }
/* `--accent-line`, not `--accent` (2026-09-08, art direction): a full-strength
   raspberry border around a card is the loudest mark on the paper section, and
   the tile already answers the same gesture with the wash and with the dash
   flowing down its connector. The wash and `b6-flow` are untouched. */
.b6-tile:hover, .b6-tile:focus-visible,
.b6-tile[aria-expanded="true"] { border-color: var(--accent-line); background: var(--accent-soft); }
.b6-tile:hover .tile-chip, .b6-tile:focus-visible .tile-chip,
.b6-tile[aria-expanded="true"] .tile-chip { color: var(--accent-tx); }
.b6-tile:hover .tile-chip .why-plus, .b6-tile:focus-visible .tile-chip .why-plus,
.b6-tile[aria-expanded="true"] .tile-chip .why-plus { color: var(--accent-tx); }
.b6-tile:hover .tile-chip .why-cross,
.b6-tile:focus-visible .tile-chip .why-cross { transform: rotate(90deg); }
.b6-tile[aria-expanded="true"] .tile-chip .why-cross { transform: rotate(45deg); }
.b6-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* --- THE HUB ---------------------------------------------------------------
   2026-09-08 (loops round, re-cut the same day by the art direction). The core
   used to be a flat accent-soft box in the middle column, then a hub whose own
   edge carried a raspberry ring. It is now a full-width BAND under the three
   tiles, and it spends exactly ONE accent object: the turning dashed ring.

   Three changes, all so the band reads as ground and not as a fourth card:
     • its own edge is a neutral hairline, `rgba(18,19,22,.16)` — it was
       `--accent-line`;
     • the surface inside it is the lifted paper token, not `--accent-soft`;
     • the ring is the only raspberry left, and it is strong enough to be seen
       turning: `rgba(196,42,90,.55)` on a `3 11` dash, sitting midway between
       the band's edge and its surface.
   Before this there were three raspberry marks stacked on one object (edge,
   wash, ring) in a paper viewport that SPEC §2 caps at about three in total. */
.b6-core {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  /* the gap between the band edge and the surface. Lengths, not percentages: a
     percentage PADDING resolves against the width on both axes, which would
     have made the vertical gap 45px instead of 26px at 1440. */
  --hub-x: clamp(28px, 3.4vw, 50px);
  --hub-y: clamp(16px, 1.8vw, 26px);
  min-height: 120px;
  /* the label sits inside the SURFACE, so the padding clears the gap first */
  padding: calc(var(--hub-y) + 14px) calc(var(--hub-x) + 18px);
  border: 1px solid rgba(18,19,22,.16);
  border-radius: var(--radius-m);
}
/* The surface. It carries the lift and NO line of its own. */
.b6-core::before {
  content: "";
  position: absolute;
  inset: var(--hub-y) var(--hub-x);
  border-radius: var(--radius-s);
  background: var(--paper-2);
}
/* The ring, halfway between the band's edge and its surface — the one accent
   object in the band, and the only thing in B6 that moves unattended.
   `stroke-dashoffset`, not a `rotate`: the band is a wide rectangle, and a
   circle around it is either an ellipse that visibly wobbles as it turns or a
   circle small enough to cut through the label. */
.b6-hub {
  position: absolute;
  inset: calc(var(--hub-y) / 2) calc(var(--hub-x) / 2);
  width: calc(100% - var(--hub-x));
  height: calc(100% - var(--hub-y));
  overflow: visible;
  pointer-events: none;
}
.b6-hub rect {
  fill: none;
  stroke: rgba(196,42,90,.55);
  stroke-width: 1;
  /* 3 11 = a 14px period, so the offset has to travel a MULTIPLE of 14 or the
     pattern jumps at the loop boundary: 224 = 16 periods, in 224 steps. */
  stroke-dasharray: 3 11;
  stroke-linecap: round;
  /* 224 steps over 24s = one 1px move every 107ms. Visually identical to a
     linear flow at this speed, and it asks Skia to re-dash the path 9 times a
     second instead of 60 — the same lesson `b6-shield-flow` was measured for,
     applied before it could cost anything. */
  animation: b6-hub-turn 24s steps(224) infinite;
  animation-play-state: paused;
}
.b6-scheme.is-inview .b6-hub rect { animation-play-state: running; }
@keyframes b6-hub-turn { to { stroke-dashoffset: -224; } }
html.rm .b6-hub { display: none; }
@media (prefers-reduced-motion: reduce) { .b6-hub { display: none; } }
.b6-core-t {
  position: relative;                /* over the surface pseudo-element */
  /* the band is full width now, so the label is one centred line and the band
     can hold its ~120px height */
  max-width: none;
  font-family: var(--font-dp);
  font-size: clamp(20px, 1.9vw, 27px);
  line-height: 1.16;                 /* B6 Prata ramp — long-form step */
  letter-spacing: -.004em;
  color: var(--fg);
}

/* Connectors. SPEC asks for SVG lines; a dashed background on a pseudo-element
   gives the same hairline and the same dash-flow on hover, and — unlike an SVG
   overlay — it cannot drift when the grid gap changes with the viewport.
   All three now DESCEND from their tile's bottom centre to the band's top edge,
   and their length is the grid's own row gap, so they meet it exactly. A8's
   rule still holds: the draw origin is the end that touches the band. */
.b6-tile::after {
  content: "";
  position: absolute;
  pointer-events: none;
  left: 50%; top: 100%;
  width: 1px; height: clamp(28px, 3.4vw, 56px);
  background-image: repeating-linear-gradient(180deg, var(--line-2) 0 5px, transparent 5px 11px);
  background-size: 1px 11px;
  background-repeat: repeat-y;
  transform: scaleY(var(--b6-draw));
  transform-origin: 50% 100%;
}
@keyframes b6-flow   { to { background-position: 11px 0; } }
@keyframes b6-flow-v { to { background-position: 0 11px; } }
.b6-tile:hover::after,
.b6-tile:focus-visible::after,
.b6-tile[aria-expanded="true"]::after {
  background-image: repeating-linear-gradient(180deg, var(--accent) 0 5px, transparent 5px 11px);
  animation: b6-flow-v .9s linear infinite;
}


/* ============================================================
   B7 — ДОРОЖНАЯ КАРТА  (no dates anywhere — editorial ban)
   ============================================================ */

.b7-statement { margin-top: clamp(24px, 2.6vw, 34px); max-width: 20ch; }

.b7-stack { position: relative; margin-top: clamp(56px, 7vw, 96px); }
/* The empty map is a real row at the top of the stack, full wrap width, and the
   light card is centred under it (ТЗ: «Поверх — центральная светлая карточка»).
   It is in flow at every width, so no node is ever covered by the card. */
.b7-ghost {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: clamp(10px, 1.4vw, 22px);
  opacity: .3;
}
.b7-gnode { position: relative; padding-top: 22px; border-top: 1px solid var(--line-2); }
.b7-gnode::before {
  content: "";
  position: absolute; left: 0; top: -3.5px;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--fg-3);
}
.b7-gn {
  display: block;
  color: var(--fg-2);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: .14em;
  font-variant-numeric: tabular-nums;
}
.b7-gs {
  display: block;
  margin-top: 10px;
  color: var(--fg-3);
  font-size: 11px;                       /* E6 — 10.5px was the page floor */
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: .12em;
  text-transform: uppercase;
}

.b7-card { position: relative; z-index: 1; max-width: 640px; margin: 0 auto; }
/* E5 — the card is centred (ТЗ line 249) and starts inside the bottom padding
   of the ghost row, so it visually sits ON the map without ever touching a node
   number or its status caption. The nodes are grid siblings, so they all share
   one row height and one bottom edge: 36px of dead space − 24px of overlap
   leaves a 12px clearance under the last line of every caption. */
@media (min-width: 900px) {
  .b7-gnode { padding-bottom: 36px; }
  .b7-card { margin: -24px auto 0; }
}
.b7-card-t {
  font-family: var(--font-dp);
  font-size: clamp(22px, 2.2vw, 30px);
  line-height: 1.16;                 /* B6 Prata ramp — long-form step */
  letter-spacing: -.004em;
  color: var(--fg);
}
.b7-steps { margin-top: clamp(22px, 2.4vw, 30px); }
.b7-need-l { margin-top: clamp(28px, 3vw, 38px); }
.b7-need {
  margin-top: 14px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 9px clamp(18px, 2vw, 28px);
}
.b7-need li {
  position: relative;
  padding-left: 18px;
  color: var(--fg-2);
  font-size: var(--t-small);
  line-height: 1.5;
}
.b7-need li::before {
  content: "";
  position: absolute; left: 0; top: .68em;
  /* C10 — a list of requirements is not an accent; a 1px raspberry hair also
     rendered as a grey smudge, so the dash is 2px of the muted token. */
  width: 8px; height: 2px;
  background: var(--fg-3);
}
.b7-after {
  margin-top: clamp(24px, 2.6vw, 32px);
  padding-top: clamp(18px, 2vw, 24px);
  border-top: 1px solid var(--line);
  color: var(--fg);
  font-size: var(--t-small);
  line-height: 1.5;
}
.b7-close { margin-top: clamp(48px, 6vw, 80px); color: var(--fg); }


/* ============================================================
   B8 — ФИНАНСОВЫЕ УСЛОВИЯ  (value map first, price second)
   ============================================================ */

.b8-intro {
  margin-top: clamp(34px, 4vw, 56px);
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(30px, 4vw, 72px);
  align-items: end;
}
.b8-map-l { margin-top: clamp(56px, 7vw, 96px); }
/* D8 — nine outlined boxes each held a label in a mostly empty 190px well and
   the whole map barely outweighed the price card (ratio 1.05). As numbered
   hairline rows the map reads as an index of what is being built and clearly
   dominates the card, which is what the ТЗ asks for. */
.b8-map {
  margin-top: clamp(20px, 2.4vw, 30px);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
  counter-reset: vm;
  border-bottom: 1px solid var(--line-2);
}
.b8-cell {
  display: flex;
  align-items: center;
  min-height: clamp(84px, 11vh, 120px);
  padding: clamp(14px, 1.6vw, 20px) 0;
  border: 0;
  border-top: 1px solid var(--line-2);
  border-radius: 0;
  color: var(--fg);
  font-size: clamp(15px, 1.45vw, 20px);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: .05em;
  text-transform: uppercase;
}
.b8-cell::before {
  content: counter(vm, decimal-leading-zero);
  counter-increment: vm;
  flex: none;
  color: var(--fg-3);
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: .14em;
  font-variant-numeric: tabular-nums;
  margin-right: 18px;
}
/* index · glyph · label — the glyph sits between them, so a scan down the
   left edge reads 01…09 uninterrupted */
.b8-cell > .ic { margin-right: 16px; }

/* the card and, under it, the ten additional budgets — both held to the right
   half so the value map above still owns the page. From 1024px up they sit
   side by side instead (U11, below). */
.b8-money {
  margin-top: clamp(56px, 7vw, 96px);
  display: grid;
  justify-items: end;
  gap: clamp(16px, 1.8vw, 22px);
}
/* A13 — the accent rule at the top of the price card is drawn, not printed */
.b8-card {
  position: relative; overflow: hidden; width: 100%; max-width: 620px;
  --b8-rule: 1;
}
.b8-card::before {
  content: "";
  position: absolute; inset: 0 0 auto 0;
  height: 2px;
  background: var(--accent);
  transform: scaleX(var(--b8-rule));
  transform-origin: 0 50%;
}
.b8-row + .b8-row {
  margin-top: clamp(20px, 2.2vw, 28px);
  padding-top: clamp(20px, 2.2vw, 28px);
  border-top: 1px solid var(--line);
}
.b8-row-l { display: flex; align-items: flex-start; gap: 9px; color: var(--fg-3); }
/* the glyph rides the FIRST line of the label: `Дополнительные бюджеты`
   wraps to two lines in the band column, and centring dropped its icon
   half a line below the other two */
.b8-row-l > .ic { --ic: 18px; margin-top: calc((1.5em - 18px) / 2); }
.b8-figs {
  margin-top: 16px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: clamp(18px, 2vw, 30px);
}
.b8-n {
  color: var(--fg);
  font-size: clamp(30px, 3.3vw, 44px);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
}
.b8-u {
  display: block;
  margin-top: 6px;
  color: var(--fg-3);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.b8-c { margin-top: 12px; max-width: 34ch; color: var(--fg-2); font-size: var(--t-small); line-height: 1.5; }
.b8-note { margin-top: clamp(18px, 2vw, 24px); color: var(--fg-3); }
.b8-extra { margin-top: 14px; max-width: 48ch; color: var(--fg-2); font-size: var(--t-small); line-height: 1.5; }
.b8-row .why { margin-top: clamp(18px, 2vw, 24px); }
/* ТЗ Блок 8: the ten items are main-screen copy. They read as the card's
   footnote, so they sit outside it — the price block stays the smaller object.
   B5 — set as apparatus (a rule above it, one measure, generous leading), not
   as fine print: the client had already complained about small desktop text,
   so 13.5px is a floor here, not a ceiling. */
.b8-budgets {
  max-width: 560px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  text-align: left;
  color: var(--fg-3);
  font-size: 13.5px;
  line-height: 1.62;
  letter-spacing: .012em;
}

/* --- B8 desktop composition (U11 + U12), 1024px up ------------------------
   U12 — nine full-width hairline rows put every label in the first quarter of
   a 1280px measure and left three quarters of each row empty, and the map was
   1100px tall. Two columns of five turn it into an index you can take in at
   once: 01–05 left, 06–09 right. `grid-auto-flow: column` keeps DOM order
   01…09, which is what the A13 clip-path wipe staggers on, so each column
   still writes itself top-to-bottom, row by row, left→right.
   The map's own `border-bottom` has to go here: column 2 is one row short, so
   a full-width bottom hairline would dangle under an empty cell. The two
   column-closing borders are stated per cell instead.

   U11 — the money row was a single-column grid with `justify-items: end`, so
   the card sat against the right edge and the left half of the page was empty.
   Card right, budgets left, bottom-aligned fixed that, at the price of ~600px
   of deliberate void in the left column (DECISIONS, "Not verified").

   ICON ROUND 2026-09-08 — U11 SUPERSEDED. The client read that void as a
   layout fault, not as air. The card is now a full-width BAND: one paper slab
   spanning the whole measure with four hairline-separated columns —
   $13 000 | +$8 000 в месяц | премия за результат | дополнительные бюджеты —
   and the ten budgets run full width underneath it. No copy moved and the DOM
   order is untouched: the three `.b8-row` children are placed on the band, and
   row 1 (one label, two figures) spans the first two columns, which is what it
   means — «ФИКСИРОВАННАЯ ЧАСТЬ» heads both figures. The card gets SHORTER as
   it gets wider, so the value map keeps its lead (F1 ratio, qa/phase9/g4.json).
   Below 1024 nothing changes: the card stays the stacked, right-aligned block
   the ТЗ-compliance round compressed. */
@media (min-width: 1024px) {
  .b8-map {
    display: grid;
    grid-auto-flow: column;
    grid-template-rows: repeat(5, 1fr);
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: clamp(48px, 6vw, 96px);
    border-bottom: 0;
  }
  /* B9 2026-09-08 — the map is the block's argument, so its labels read at
     heading size; the rows lose the height they were padding with. */
  .b8-cell {
    /* the AD asked for clamp(76px, 9vh, 92px); at a 900px-tall viewport that
       is 81px a row and the map falls to 405px, which puts F1 (map/card) at
       1024 to 1.092 — under the 1.10 gate. The vh term is raised so a 900px
       viewport reaches the cap: 92px a row, F1 1.240 at 1024 / 1.292 at 1440,
       and the row is still 25px shorter than before. */
    min-height: clamp(84px, 10.2vh, 96px);
    font-size: var(--t-h3);
  }
  .b8-cell:nth-child(5),
  .b8-cell:nth-child(9) { border-bottom: 1px solid var(--line-2); }

  .b8-money {
    grid-template-columns: minmax(0, 1fr);
    justify-items: stretch;
  }
  .b8-card    { grid-column: 1; grid-row: 1; max-width: none; }
  .b8-budgets { grid-column: 1; grid-row: 2; max-width: none; }

  /* the band. `--b8-pad` is the air on ONE side of a divider, so every
     boundary in the row — the two inside `.b8-figs` included — gets the same
     2×--b8-pad of clearance and the four columns read as one ruled table. */
  .b8-card {
    --b8-pad: clamp(16px, 2.2vw, 40px);
    display: grid;
    grid-template-columns:
      minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.22fr) minmax(0, .86fr);
    column-gap: 0;
    /* stretch, not start: the three dividers are `border-left` on the columns
       themselves, so a start-aligned row would stop its rule where its own
       copy ends and the band would have three rules of three different lengths */
    align-items: stretch;
  }
  .b8-card > .b8-row:nth-child(1) {
    grid-column: 1 / 3;
    padding-right: var(--b8-pad);
    display: flex;
    flex-direction: column;
  }
  /* so the divider INSIDE row 1 runs to the foot of the band too — it starts
     under the label the two figures share, which is what a spanning header
     looks like in a ruled table */
  .b8-card > .b8-row:nth-child(1) .b8-figs { flex: 1; }
  .b8-card > .b8-row:nth-child(2) { grid-column: 3; }
  .b8-card > .b8-row:nth-child(3) { grid-column: 4; }
  .b8-card > .b8-row + .b8-row {
    margin-top: 0;
    padding-top: 0;
    padding-left: var(--b8-pad);
    border-top: 0;
    border-left: 1px solid var(--line);
  }
  .b8-card > .b8-row:nth-child(2) { padding-right: var(--b8-pad); }

  /* row 1 holds two figures under one label; the divider between them is the
     first of the band's three rules */
  /* two columns, stated — `auto-fit, minmax(210px, 1fr)` collapsed to one at
     1024 and left the fig-divider hanging vertically down the middle of a
     single stack */
  .b8-card > .b8-row:nth-child(1) .b8-figs {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 0;
  }
  .b8-card > .b8-row:nth-child(1) .b8-fig:first-child { padding-right: var(--b8-pad); }
  .b8-card > .b8-row:nth-child(1) .b8-fig + .b8-fig {
    padding-left: var(--b8-pad);
    border-left: 1px solid var(--line);
  }
  /* the caption measure has to come down with the column width */
  .b8-card .b8-c { max-width: 28ch; }

  /* B8 2026-09-08 — the formalisation note is the BAND's footnote, not the
     tail of column 3. Inside the column it pushed all four stretch-aligned
     columns down with it and left column 4 with ~200px of dead air under two
     lines of copy. It now runs along the foot of the band, full measure,
     under one hairline; the band reserves the room with `padding-bottom`. */
  .b8-card { padding-bottom: 124px; }
  .b8-card .b8-note {
    position: absolute;
    left: clamp(28px, 3.4vw, 48px);
    right: clamp(28px, 3.4vw, 48px);
    bottom: clamp(28px, 3.4vw, 48px);
    margin-top: 0;
    padding-top: clamp(16px, 1.5vw, 22px);
    border-top: 1px solid var(--line);
    max-width: none;
  }
}

/* at 1024–1279 the four columns are ~170px each and `+$8 000` at 3.3vw would
   run into its own divider */
@media (min-width: 1024px) and (max-width: 1279px) {
  .b8-card .b8-n { font-size: 30px; }
}


/* ============================================================
   B9 — СЛЕДУЮЩИЙ ШАГ  (black, slow fade out)
   ============================================================ */

.b9 { background: #000; padding-bottom: 0; border-top: 1px solid var(--line); }
.b9-text {
  margin-top: clamp(40px, 5vw, 72px);
  display: grid;
  gap: clamp(18px, 2vw, 26px);
  max-width: 62ch;
}
.b9-final {
  margin-top: clamp(56px, 7vw, 100px);
  max-width: 16ch;
  font-family: var(--font-dp);
  font-size: var(--t-h1);
  line-height: 1.06;                 /* B6 Prata ramp — statement step */
  letter-spacing: -.01em;
  color: var(--fg);
}
/* the tail is both the "медленное затемнение до чёрного" and the runway the
   last reveals need in order to reach their trigger at max scroll (DECISIONS).
   A2 — 60vh with a `top center` start left only ~90px of scroll for the fade,
   which read as a cut. 90vh + `top 80%` gave it 630px at 1440x900.
   D5 — but `top 80%` also started the fade while the closing phrase was still
   at the BOTTOM of the screen: `.b9-in` peaked at 0.95 with the line's top at
   549px and was back to 0.57 by the time it reached the upper third, so the
   sentence the whole page builds to was never read at full strength. The scrub
   now starts at `top 50%` (main.js), which costs 0.30vh of runway, and the
   tail buys it back with interest: at 120vh the runway is 0.70vh — 630px at
   1440x900, the same figure A2 shipped — `.b9-in` is still at 0.93 when the
   phrase's top reaches 30 % of the viewport, and `--chrome-fade` lands on 0
   exactly at max scroll. Measured at 55 % / 110vh first: 0.848 at the 30 %
   line, i.e. still failing, which is what forced 50 % / 120vh
   (qa/phase8/g4.json → D5). */
.b9-tail { height: 120vh; pointer-events: none; }


/* ============================================================
   B4–B9 · TABLET / MOBILE
   ============================================================ */

@media (max-width: 1180px) {
  .b4-decomp { grid-template-columns: clamp(130px, 18vw, 180px) clamp(28px, 4vw, 56px) minmax(0, 1fr); }
  .b7-need { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 899px) {

  /* --- B4 --- */
  .b4-intro { grid-template-columns: minmax(0, 1fr); gap: 24px; align-items: start; }
  .b4-decomp {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }
  .b4-src { min-height: 0; flex-direction: row; align-items: baseline; gap: 12px; }
  .b4-src > .ic { align-self: center; }   /* a glyph has no baseline to sit on */
  .b4-arm {
    justify-self: start;
    width: 1px; height: clamp(24px, 6vw, 40px);
    margin-left: 22px;
    background: var(--line-2);
  }
  .b4-arm::after { right: auto; left: 50%; top: auto; bottom: 0; margin: 0 0 -3px -3px; }
  /* D3 — the six names repeat four times, so the column count has to divide
     six or the "row 1 states them once" rule cuts the set in half. Three
     columns put the six names on rows 1-2 and leave six clean fade rows. */
  .b4-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .b4-cell { min-height: 38px; }
  /* D2 — the desktop taper was fixed to a faint ink (.30) in the polish round
     and this rule was not: `color: transparent` left 18 of the 24 tiles as
     empty boxes on a phone, which reads as a rendering failure, not as
     multiplication. Same construction as the desktop rule, one step lighter
     because the phone tiles are smaller and the `filter` steps below are
     harsher (.55 / .38 / .24 against .62 / .50 / .40). */
  .b4-grid .b4-cell:nth-child(n+7) { color: rgba(169,167,161,.34); }
  .b4-grid .b4-cell:nth-child(-n+6) { color: var(--fg-2); }
  .b4-grid .b4-cell:nth-child(n+7):nth-child(-n+12)  { filter: opacity(.55); }
  .b4-grid .b4-cell:nth-child(n+13):nth-child(-n+18) { filter: opacity(.38); }
  .b4-grid .b4-cell:nth-child(n+19) { filter: opacity(.24); }
  .b4-item { grid-template-columns: minmax(0, 1fr); gap: 10px; }
  .b4-item-n { padding-top: 0; }
  .b4-figs { grid-template-columns: minmax(0, 1fr); }
  .b4-fig-n { font-size: clamp(34px, 11vw, 44px); }
  .b4-tag { margin-left: 0; margin-top: 10px; }
  .b4-item-t .b4-tag { display: block; width: max-content; }

  /* --- B6 --- */
  .b6-intro { grid-template-columns: minmax(0, 1fr); gap: 24px; align-items: start; }
  .b6-scheme {
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(24px, 5vw, 34px);
    padding: clamp(46px, 8vw, 62px) clamp(18px, 4vw, 28px) clamp(28px, 5vw, 38px);
  }
  /* one column: the three tiles stacked, the band last, and the connectors
     line up into ONE vertical rail down the left of the stack. Explicit rows
     rather than `order`, so the painted order and the DOM order agree. */
  .b6-tile--a { grid-area: 1 / 1; }
  .b6-tile--b { grid-area: 2 / 1; }
  .b6-tile--c { grid-area: 3 / 1; }
  .b6-core    { grid-area: 4 / 1; }
  .b6-tile::after {
    left: 28px;
    height: clamp(24px, 5vw, 34px);
    transform-origin: 50% 0;
  }
  .b6-core { min-height: 104px; }
  .b6-core-t { max-width: none; }

  /* --- B7 — static ghost row above the card, no overlap on a phone --- */
  .b7-ghost { margin-bottom: 28px; gap: 8px; }
  .b7-card { max-width: none; }

  /* --- B8 ---
     E1: below 900px the card stacks its figures and grows to ~800px while the
     map shrank to ~470px, so the price occupied MORE space than the value map
     (ТЗ line 257). The map keeps the desktop row height and the card is
     compressed: the budgets list moved out of it (E2), the rows sit tighter and
     the price figures step down. Measured in qa/phase5/tz-measures.json. */
  .b8-intro { grid-template-columns: minmax(0, 1fr); gap: 24px; align-items: start; }
  .b8-cell { min-height: 96px; }
  .b8-figs { grid-template-columns: minmax(0, 1fr); margin-top: 12px; gap: 16px; }
  .b8-card { padding: clamp(22px, 5vw, 30px); }
  .b8-row + .b8-row { margin-top: 18px; padding-top: 18px; }
  .b8-n { font-size: clamp(27px, 7.4vw, 34px); }
  .b8-c { margin-top: 10px; }
  .b8-note { margin-top: 14px; }
  .b8-extra { margin-top: 12px; }
  .b8-budgets { font-size: 13px; }        /* B5 — one step down, not fine print */
}

/* D7 — five columns leave ~65px each and «ОПРЕДЕЛЕНО» alone is ~78px, so the
   status would overflow its column. Two columns give it 170px and every node
   keeps its caption. */
@media (max-width: 470px) {
  .b7-ghost { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 20px; }
}

/* B6 annotation under the scheme (ТЗ Блок 6 / checklist "общий взгляд сверху") */
.b6-note { margin-top: clamp(28px, 3vw, 44px); max-width: 54ch; color: var(--ink-2); }


/* ============================================================
   DESKTOP TYPE LIFT (>= 1024px) — per-block captions and indices
   main.css § 12 owns the --t-label step (12.5 → 13.5px) and the pins.
   Everything below is a caption, an index or a status that the token alone
   does not lift far enough. Mobile sizes are untouched.
   ============================================================ */

@media (min-width: 1024px) {

  /* B0 — the role line under each name */
  .b0-name-r { font-size: 13px; }

  /* B3 */
  .b3-task-n { font-size: 14px; }

  /* B4 — the decomposition grid keeps its 6×4 proportion: the cell grows with
     its label, so the tiles do not turn into text boxes */
  .b4-cell { font-size: 13px; min-height: clamp(40px, 3.6vw, 54px); }
  .b4-cap  { font-size: 14px; }
  .b4-tag  { font-size: 12.5px; }   /* C2 — the desktop label floor */
  .b4-item-n { font-size: 14px; }
  .b4-fig .tile-chip { font-size: 12.5px; }
  /* D4 — the seven items are the longest reading passage on the page and they
     were set at `--t-small` (15px), the size the rest of the page uses for
     captions. Body copy gets body size. The measure is unchanged: `.b4-item p`
     in main.css §4 caps it at 54ch, which is ~550px at 17px. */
  .b4-item-d { font-size: 17px; line-height: 1.55; }

  /* B5 — `.b5-n` / `.b5-stage` / `.b5-meta dt` ride the lifted `--t-label`
     (13.5px) on their own; only the two sizes set in px are restated here */
  .b5-cue  { font-size: 12.5px; }
  .b5-lead { font-size: 16px; }

  /* B7 — the ghost row keeps its low opacity, only the type grows */
  .b7-gn { font-size: 14px; }
  .b7-gs { font-size: 12.5px; }

  /* B8 */
  .b8-cell::before { font-size: 14px; }
  .b8-u { font-size: 13.5px; }
}

