/* ═══════════════════════════════════════════════════════════════════
   LEVEL-UP TRAINING — page-owned stylesheet
   Born 2026-08-18 (overnight Mission 2 redesign). Linked ONLY by
   pages/level-up-training-madrid.html, AFTER styles.css + i18n.css —
   styles.css is frozen; everything this page adds lives here.

   Components:
     1. .cf--bal / .bcf — The Balance: five plates on one shared arc,
                words ON the plate, arrows flanking the strip. Retired
                the `.bal` ruled row on 2026-08-26 (s136); its rules are
                gone from this file and the markup lives verbatim in
                `🗑️ Delete/2026-08-26-balance-block-pre-coverflow/`.
                The component's own section is at the FOOT of this file.
     2. .cov  — What We Cover: full-width ruled index, one 3D object per
                row beside a short title and its line. Objects, NOT cards
                (D017: skills you ACQUIRE, never deliverables you RECEIVE)
                and ⛔ never numbered. Redesigned 2026-08-26 (s136); the
                pre-port file is verbatim in
                `🗑️ Delete/2026-08-26-cov-preport-backup/`. Revert word: cov.
     3. 🪦 (removed s147) was .flow, the Session Flow rail — superseded
                s145 by § SESSION SHEET at the foot of the file, swept s147.
     3b. Hero subtext 768-1023 band cap (lusubband, s147).
     5. 📱 PHONE/TABLET LAYER — stays LAST in this file (site law).

   Entrance timing mirrors the site: 1800ms var(--ease-fluid) ≥1200,
   1100ms cubic-bezier(.45,0,.15,1) below (styles.css .reveal retune).
   ═══════════════════════════════════════════════════════════════════ */


@media (min-width: 1024px) and (max-width: 1199px) {
 /* 2 lines × 1.5 line-height */
}

/* the axis — one hairline, two ends, a gold mark under the midpoint */


/* ── 2. WHAT WE COVER — .cov ───────────────────────────────────────
   A SIBLING of .text-section, never a child (LAWS ⑭).
   Rows stack at every width → trigger-only reveals, no ladder (D001).

   Redesigned 2026-08-26 (s136) on Oleg's greenlight: each row now carries
   a 3D object, a short title in the `dt` face, and the rest of the
   sentence under it. The 28px gold dash it replaces is gone.

   🔒 OBJECTS, NOT CARDS — D017. "What's Included" cards are deliverables
   you RECEIVE; these five are skills you ACQUIRE, and D017 named that the
   ruling "most likely to be fixed by a future session chasing
   consistency". Oleg saw the Events `.inc-*` cards side by side with this
   in the lab and chose this. ⛔ NEVER NUMBER THIS BLOCK (D017 ⑴): Anna
   killed numbering herself — a session whose structure adapts to you has
   no fixed order, and numbering one invents authority the page does not
   claim.

   ⚠️ The title face/weight is VALUE-COPIED from `.learn__list dt`
   (Cormorant 600), never selector-shared: `.learn` is live on Self-Makeup
   and the Bridal opener, both CLOSED, and a shared edit would reach them.
   ⚠️ The description sits at #6B6B6B = 4.71:1 on cream against an AA floor
   of 4.5 — it CANNOT be lightened further (D017 ⑸). More separation has to
   come from the title or the size. */

.cov {
  max-width: 980px;
  margin: 48px auto 0;
}

.cov__item {
  display: grid;
  grid-template-columns: 168px 1fr;
  align-items: center;
  gap: 32px;
  margin: 0;
  padding: 24px 0;
  border-top: 1px solid var(--border-color);
}
.cov__item:last-child { border-bottom: 1px solid var(--border-color); }

/* THE OBJECT SLOT IS WIDE AND SHORT ON PURPOSE — 168/92 = 1.83, wider than
   the widest object in the set (the tray of cups, 1.78). `object-fit:
   contain` fits by the LONGEST side, so in a SQUARE slot a tall object
   (the brush roll, 0.63) and a wide one are limited by different axes and
   land at different heights — which reads as a ragged row that no
   per-object multiplier can fix, because the mismatch is the axis, not the
   size. A wide-short box makes HEIGHT the limit for every object and they
   stand on one line. This is `.learn--tight`'s trick (s63, born for a set
   that "mixes portrait and landscape objects"), re-derived here.
   ⚠️ Two formula passes were tried first and Oleg's eye killed both — the
   `.inc-*` bounding-box formula (BASE 10527) and equal-ink area. Both are
   blind to an axis mismatch. Don't re-propose either for this block. */
.cov__obj {
  width: 168px;
  height: 92px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.cov__obj img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transform: scale(var(--obj-scale, 1));
}

/* Per-object optical trim, judged on a 4× contact strip, not derived — the
   tray of cups still read heaviest (a tray is a lot of gold) and the roll
   lightest. `--obj-scale` is THIS component's property: `application.webp`
   keeps its shipped `--icon-scale: 0.96` inside `.inc-*` on Events and is
   untouched there. Precedent: `madrid-trial.webp` is 1.08 in `.inc-*` and
   1 in `.learn--tight` (included-icons-inventory.md). */
.cov__obj--palette   { --obj-scale: 0.92; }
.cov__obj--balance   { --obj-scale: 0.98; }
.cov__obj--brushroll { --obj-scale: 1.10; }
.cov__obj--teacups   { --obj-scale: 0.74; }
.cov__obj--candle    { --obj-scale: 1.02; }

.cov__t {
  margin: 0 0 6px;
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--navy);
}

.cov__d {
  margin: 0;
  font-size: 15.5px;
  font-weight: 300;
  line-height: 1.65;
  color: #6B6B6B;
}

/* THE PHONE-ONLY BREAK (Oleg, 2026-08-26). Row 3's line reads better as two
   lines on a phone and one line on desktop. The break is a `<br>` inside the
   STRING, which means it is inside the dictionary VALUE — and ⚠️ a `<br>` in
   a dict value knows nothing about breakpoints: it fires at EVERY width.
   This page paid for that at s129d, where a mobile lockup silently forced
   the DESKTOP headline onto two lines in all four languages. Suppressed from
   1024 up, which is the s129d cure; and there is a REAL SPACE before the
   `<br>` in every language's value so hiding it leaves "years of", never
   "yearsof" — that space is the second half of the same bill.
   ⚠️ Each language carries the `<br>` at ITS OWN word. Never translate a
   break point, and never add one to a value without checking the width it
   was meant for. */
@media (min-width: 1024px) {
  .cov__d br { display: none; }
}

/* ── 3. SESSION FLOW — .flow ── 🪦 REMOVED s147 (2026-08-26) ───────
   The rail died s145 (D086, revert word flowsheet); its dead CSS sat
   here awaiting a dedicated pass and s147 swept it: §3 rules, the
   .flow reduced-motion pair and both phone-layer .flow chunks. Full
   removed text: the pre-s147 levelup.css copy in
   05-SYSTEM/scratchpad/2026-08-26-s147-preship-backup/css/, and the
   markup snapshot in 🗑️ Delete/2026-08-26-flow-rail-pre-contact-sheet/. */

/* ── 3b. HERO SUBTEXT, 768-1023 BAND (s147) ────────────────────────
   styles.css caps .hero__subtitle at 520px site-wide; in this band
   that wrapped the Level-Up subtext to 3 lines in all four languages
   (the s126 flag). 700px is the MEASURED 2-line width for en·uk·es·ru
   at 768/834/900/1023 (iframe probe, s147). Page-scoped on purpose —
   styles.css stays frozen. Revert word: lusubband. */
@media (min-width: 768px) and (max-width: 1023px) {
  .hero--levelup .hero__subtitle { max-width: 700px; }
}

/* ═══════════════════════════════════════════════════════════════════
   5. 📱 PHONE/TABLET LAYER — LAST in this file, max-width bands only.
   ≤1023 layer. (The Flow rail is gone — s147; the Balance no longer appears here —
   the coverflow owns its own phone layer inside its section at the foot
   of this file, where its `--bcf-h` ceiling lives beside its desktop one.) ≤767 adds the centre-anchor (🅲 — the s72 layer in
   styles.css names legacy classes only, so these carry their own).
   ═══════════════════════════════════════════════════════════════════ */

@media (max-width: 1023px) {
  /* centre-anchor (🅲) */


  .cov { margin-top: 36px; }
  .cov__item {
    grid-template-columns: 1fr;
    gap: 14px;
    justify-items: center;
    text-align: center;
    padding: 24px 0;
  }
  .cov__obj { width: 176px; height: 96px; }   /* same 1.83 ratio, one step up */
  .cov__t { font-size: 20px; }
  .cov__d { font-size: 14.5px; }

}

/* ═══════════════════════════════════════════════════════════════════════════
   WHO THIS IS FOR — the artist card  (s127, 2026-08-25 · lab candidate "D2")
   ───────────────────────────────────────────────────────────────────────────
   Replaces the plain `.text-section` prose block that shipped here from the
   start. Oleg's greenlight state, verbatim off the lab bar: Ratio Modern ·
   "Loud single" header · Who-line 48 · card title 48.
   Lab of record: `04-WORKSHOP/block-labs/levelup-who-lab.html` (deploy-ignored;
   A/B/C/D/D3 benched, the six rejected faces listed in its comments).

   THE THREE THINGS THAT ARE EASY TO BREAK HERE
   ① THE RATIO IS THE PHOTOGRAPH'S OWN. The box was 16/7.5 for an afternoon,
     inherited off the homepage banner, and `cover` silently ate the bottom
     17% of a 1.898:1 frame — exactly the strip the left-hand artist's hands
     sit in. Oleg spotted it as "did you crop the picture?" and the FILE was
     innocent every time. If the photograph is ever replaced, set this ratio
     from the new file's own dimensions before anything else. (LAWS ㊴ · L31)
   ② THE SHADE IS A SHADOW, NOT A PLATE. A flat `rgba(0,0,0,.62)` rectangle
     was built first and rejected on sight — "this stupid dark box". A
     card-wide gradient was rejected before that, for tinting the photograph.
     What survives is two blurred layers behind the type only: the picture
     keeps full opacity everywhere the type is not. Shade is BLACK, never
     navy — `rgba(17,24,39)` reads blue over warm skin.
   ③ THE PHONE IS A DIFFERENT COMPOSITION, ON PURPOSE. Copy-over-photo does
     not survive at 402px: the lab's inherited 3/5 portrait crop cut both
     artists' faces in half and two long paragraphs buried what was left.
     ≤767 stacks instead — photograph on top at its own ratio, copy below on
     cream — so the frame stays readable and so does the prose.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The loud line. `.section-title` ships as Altesse, a script: right for a
   phrase somebody reads, wrong for a four-word signpost passed at scroll
   speed ("it isn't really readable at a glance"). Ratio Modern instead, the
   face this site already runs on `.vcard__t`. levelup.css loads AFTER
   styles.css, so an equal-specificity selector wins on source order. */
.luwho__head {
  font-family: 'ratiomodern', var(--font-serif);
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1.12;
  font-size: clamp(34px, 3.5vw, 48px);
}

/* ⚠️ THE ORDER LADDER — LAWS ㉜, and this block is the worst-case shape for it.
   `.luwho__head` and `.luwho` are TWO separate `.reveal`s stacked vertically.
   They USED to ride different observers (card late, heading eager — the s127
   shape this paragraph originally described); s129 enrolled the heading in
   the LATE roster too, so the pair now shares one trigger and this ladder
   finally orders them. s143 went one further for the heading only: its
   entrance PLAYS via WAAPI (driveWaapiReveal in main.js), because Safari's
   URL-bar collapse jump-finished the running CSS transition mid-scroll
   (LAWS ④, frame-proven 2026-08-26). Trigger unchanged; engine swapped.
   Two elements, two batches, so on a phone the photograph could still rise
   BEFORE the title that introduces it without this ladder, and nothing about
   either element is wrong when it happens.
   The delay lives on the PLAIN selector, never inside `:not(.revealed)`
   (LAWS ㉙ — a transition reads the AFTER-change style, which IS `.revealed`,
   so a delay declared on the unrevealed state never runs at the one moment it
   is needed; that mistake made every stagger on this site inert once).
   180ms: enough that the title always leads, short enough that it reads as one
   movement rather than two events. */
.luwho { transition-delay: 180ms; }

.luwho {
  position: relative;
  width: 100%;
  margin-top: clamp(28px, 3vw, 44px);
  border-radius: 12px;
  overflow: hidden;
  background: var(--cream-dark);
  display: grid;
}

/* ⚠️ THE RATIO LIVES ON THE <img>, NOT ON `.luwho` — LAWS ㊴. Put
   `aspect-ratio` on the card and `height:100%` on the image and the pair is
   circular in an auto grid row: the image takes its INTRINSIC height, the row
   grows to match, `align-self:end` parks the copy at the bottom of THAT row,
   and `overflow:hidden` clips every word away with nothing in the DOM saying
   "hidden".
   ⚠️ And it is a plain <img> with `srcset`, NOT a <picture> — a <picture>
   becomes the grid item itself and steals the placement from the image inside
   it (LAWS ② · the `<picture>` clause). */
/* THE PLACEMENT MOVED OFF THE <img> AND ONTO THE <picture> (s129).
   s127's comment below still stands on WHY the ratio may not sit on the
   card — but the grid ITEM is now the `<picture>` wrapper the art-directed
   phone crop required, and a `grid-area` on the <img> inside it addresses a
   grid the <img> is no longer a child of. That declaration does not error;
   it is simply ignored, and the photograph leaves the cell the copy is laid
   over (LAWS ② · the `<picture>` clause, paid for once already). */
/* ── THE LAUREL'S THREE NUMBERS, DECLARED ONCE (s129) ────────────────────
   Oleg tuned the laurel live in the store-rebuild session; `--wreath-ratio`
   and `--wreath-dip` are HIS SEALED VALUES and the phone composition is
   exactly what he judged. They are tokens rather than literals because each
   one has TWO consumers, and the handover kit shipped both pairs as separate
   magic numbers — which is how the tablet band broke before it was caught:
     · ratio → the laurel's width AND its cap in the 768-1023 band
     · dip   → how far it hangs past the plate AND the clearance the prose
               below needs to not collide with it
     · cap   → the plate's max-width AND, through the ratio, the laurel's
   Retune any one of them in ONE place and both consumers follow. A literal
   repeated in two rules is a rule with no compiler behind it: nothing errors
   when they drift, the decoration just stops matching the thing it decorates.
   ⚠️ `--wreath-cap` is the SAME 460px as the `luwhocap` plate cap below and
   they must stay equal — that is the whole point of the token. */
.luwho {
  --wreath-ratio: 0.91;
  --wreath-dip: 30px;
  --wreath-cap: 460px;
}
.luwho picture {
  grid-area: 1 / 1;
  display: block;
}

/* ⛔ DESKTOP IS ONE LINE — THE DICT'S `<br>` IS SUPPRESSED HERE (s129d).
   The break lives inside the dictionary VALUE (each language breaks at its own
   word), and a dictionary string knows nothing about breakpoints — so the
   mobile two-line lockup fired on desktop too and forced the wide overlay's
   headline onto two lines. That was a desktop change shipped as a side effect
   of a mobile one, and nobody approved it. Suppressing the <br> above 1023
   restores one line, which is what Oleg asked for.
   Measured one line in en/es/uk/ru at 1024/1280/1440/1600/1920 — RU binds
   (widest) and still clears its column by 99px at 1024. Revert: `luwho1line`.
   ⛔ A HIDDEN `<br>` LEAVES NO SPACE BEHIND (s133 — six days live). This rule
   does not "unwrap" the headline, it DELETES the only separator between the two
   words: with the markup written tight (`working<br>makeup`) every language
   read glued on desktop — "For workingmakeup artists", "Для практикуючихвізажисток".
   Nothing errors, no layout breaks, and the phone — where the break still fires
   — looks perfect, which is why it survived a four-language measuring pass.
   The separator now lives in the markup as a real space BEFORE the tag
   (`working <br>makeup`), harmless at ≤1023 where an end-of-line space is
   dropped by white-space processing. ⚠️ If this rule is ever ported to another
   `<br>`-in-a-dict headline, PORT THE SPACE WITH IT — and check the dict values,
   not just the English (the value carries its own break, per language).
   Re-measured with the space, 1024: RU binds at 533px in a 623px column. */
@media (min-width: 1024px) {
  .luwho__title br { display: none; }
}

/* ⛔ THE LAUREL IS OFF BY DEFAULT AND TURNED ON ONLY WHERE IT WAS DESIGNED.
   Oleg composed and sealed it on the PHONE CARD; the ≥1024 overlay is a
   different composition entirely (wide frame, copy laid over it, left-aligned)
   and the laurel has no place in it. Written as default-off + opt-in rather
   than as a `min-width:1024` kill switch on purpose: the port did it the other
   way first, the kill switch lived only in the lab's own chrome, and the
   laurel shipped to the desktop probe at FULL PLATE WIDTH hanging 542px past
   the photograph. Default-off cannot fail that way — a missing rule hides the
   decoration instead of revealing it in a composition nobody designed. */
.luwho__wreath { display: none; }
.luwho__img {
  width: 100%;
  height: auto;
  aspect-ratio: 2720 / 1433;
  object-fit: cover;
  object-position: 50% 0;
  border-radius: 12px;
  display: block;
}

.luwho__copy {
  grid-area: 1 / 1;
  position: relative;
  z-index: 2;
  align-self: end;
  justify-self: start;
  /* Wide on purpose. At the narrower measure this block ran 3 lines then 4,
     with a single orphan word alone on the fourth. */
  width: min(74%, 1020px);
  /* `auto` is not wanted here — the copy is anchored into the bottom-left
     corner, and what reads as its inset is this margin PLUS the padding
     below. The padding is not decorative: it is the room the shadow needs to
     clear the glyphs, so the margin carries almost nothing. */
  margin: 0 0 10px 14px;
  padding: 34px 44px 40px;
  text-align: left;
}

/* THE SOFT SHADOW — two blurred layers, no edge anywhere.
   ::before is the wide halo, ::after the darker core under the type itself.
   Both sit at `z-index:-1` inside `.luwho__copy`'s own stacking context (its
   `z-index:2` creates one), so they paint ABOVE the photograph and BELOW every
   word. `.luwho`'s `overflow:hidden` trims the spill at the card edge, which
   is wanted — the shadow belongs to the picture, not to the page. */
.luwho__copy::before,
.luwho__copy::after {
  content: '';
  position: absolute;
  z-index: -1;
  pointer-events: none;
}
.luwho__copy::before {
  inset: -58px -96px -70px -86px;
  background: radial-gradient(115% 128% at 34% 62%,
    rgba(0, 0, 0, 0.60) 0%,
    rgba(0, 0, 0, 0.55) 26%,
    rgba(0, 0, 0, 0.42) 44%,
    rgba(0, 0, 0, 0.26) 60%,
    rgba(0, 0, 0, 0.12) 74%,
    rgba(0, 0, 0, 0.04) 86%,
    rgba(0, 0, 0, 0)    100%);
  filter: blur(46px);
}
.luwho__copy::after {
  inset: -14px -34px -20px -30px;
  background: radial-gradient(96% 108% at 38% 56%,
    rgba(0, 0, 0, 0.58) 0%,
    rgba(0, 0, 0, 0.52) 38%,
    rgba(0, 0, 0, 0.34) 62%,
    rgba(0, 0, 0, 0.12) 82%,
    rgba(0, 0, 0, 0)    100%);
  filter: blur(30px);
}

/* `.vcard__t`'s shipping recipe, carried whole: a display face set at another
   face's weight and tracking is a different design. */
.luwho__title {
  font-family: 'ratiomodern', var(--font-serif);
  font-weight: 400;
  font-size: clamp(34px, 3.3vw, 48px);
  letter-spacing: -0.02em;
  line-height: 1.12;
  color: var(--cream);
  margin: 0 0 16px -0.02em;
  padding: 0.08em 0.12em 0.14em 0;   /* LAWS ⑧ swash cushion */
}

.luwho__body p {
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.7;
  color: rgba(255, 255, 255, 0.86);
  margin: 0 0 14px;
}
.luwho__body p:last-child { margin-bottom: 0; }

/* ── ≤1023: STACKED, NOT OVERLAID ────────────────────────────────────────
   See ③ in the header block. The card stops being a card here: no clip, no
   ground, no shadow (the shadow's whole job was legibility over a photograph
   and there is no longer any type over one).
   ⚠️ THE FENCE IS 1023, NOT 767, AND THAT IS MEASURED. At 768 the overlay
   composition breaks: the same copy sets in 4 + 6 lines inside a box only
   704px wide, the copy block runs 34px PAST the image's bottom edge, and
   `overflow:hidden` cuts those 34px off — a clipped final line, with nothing
   in the DOM admitting it. The tablet band would have shipped that, because
   the phone was checked and the desktop was checked and 768 is neither.
   Caught by comparing copyBottom against imgBottom, not by looking. */
@media (max-width: 1023px) {
  /* ═══════════════════════════════════════════════════════════════════
     s129 — THE FORMAT CARD. Oleg's brief, verbatim: "maybe we use a card
     approach, as we have on a self-makeup course, in the section of Choose
     your format... but of course without the card sale because we just have
     one single image". So this is `.vcard`'s PHONE recipe (styles.css
     ~13570-13600) ported onto a single still: 4/5 plate, title on a solid
     black scrim, the doubled gold/white hairline, prose stepped OFF the
     card onto the cream below. No deck, no pile, no arrows, no rail.
     ⛔ THE SCRIM IS BLACK, NEVER NAVY — `rgba(17,24,39)` reads blue over
     warm skin, and Oleg has rejected it by name once already (s94 16:25).

     ⚠️ THE CARD IS THE `<picture>`, NOT `.luwho`. `.luwho` has to stay an
     unclipped two-row wrapper because the two paragraphs live BELOW the
     plate — clipping at the card edge is exactly what would eat them.
     So the rounding, the ratio and the scrim all belong to the picture.

     ⚠️ `.luwho__copy` IS `display:contents` HERE, and that is load-bearing,
     not a tidy-up. The title has to overlay the plate while its sibling
     prose sits in the row underneath — impossible while both are locked
     inside one positioned box. `contents` dissolves the box so the grid
     places the h3 and the prose independently, and the desktop overlay it
     exists for is untouched above 1023.
     ⚠️ Its `::before`/`::after` (the desktop blurred shadow) are already
     `display:none` in this block — which they must be, because a
     `display:contents` element paints no pseudo-elements at all.
     Revert word: luwhocard. ═════════════════════════════════════════════ */
  .luwho {
    display: grid;
    grid-template-rows: auto auto;
    margin-top: 22px;
    background: none;
    border-radius: 0;
    overflow: visible;
  }
  .luwho picture {
    grid-area: 1 / 1;
    position: relative;
    display: block;
    aspect-ratio: 4 / 5;
    border-radius: 12px;
    overflow: hidden;
    background: var(--navy);
  }
  /* the scrim and the rim, as pseudo-elements on the plate itself — the
     live block has no elements to spare and adding two empty <div>s to
     translated markup is how a decoration ends up with a dict key. */
  .luwho picture::after,
  .luwho picture::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
  }
  .luwho picture::after {
    z-index: 1;
    background: linear-gradient(to top,
      rgba(0, 0, 0, .80) 0%,
      rgba(0, 0, 0, .38) 20%,
      rgba(0, 0, 0, 0) 44%);
  }
  .luwho picture::before {
    z-index: 3;
    border: 1px solid rgba(196, 162, 101, .38);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .14);
  }
  .luwho__img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    aspect-ratio: auto;          /* ⛔ the desktop 2720/1433 must not survive
                                    here — it would fight the 4/5 plate */
    object-fit: cover;
    object-position: 50% 50%;    /* the plate IS the composition Oleg framed;
                                    50% 0 was a crop instruction for the wide
                                    frame and would re-crop his own crop */
    border-radius: inherit;
  }
  /* ── 🏆 THE LAUREL ────────────────────────────────────────────────────
     Sits in the plate's own grid cell, bottom-aligned and hanging past it.
     ⚠️ `z-index:1` TIES with `.luwho picture`, and the tie is broken by DOM
     order — the laurel wins only because it comes after `</picture>`.
     ⚠️ THE `z-index` ON THE PLATE IS NOT DECORATIVE: `<picture>` is
     `z-index:auto`, so `picture::before` (the gold rim, z-index:3) does not
     stay inside it — it escapes into this row's stacking context and paints
     OVER the title and the laurel. Capping the plate at 1 contains the rim.
     Verified by `elementFromPoint` at the title's own centre, not by eye. */
  .luwho picture { z-index: 1; }
  .luwho__wreath {
    display: block;          /* the opt-in — see the default-off rule above */
    grid-area: 1 / 1;
    align-self: end;
    justify-self: center;
    z-index: 1;
    position: relative;
    pointer-events: none;
    width: calc(var(--wreath-ratio) * 100%);
    margin-bottom: calc(var(--wreath-dip) * -1);
  }
  .luwho__wreath img { display: block; width: 100%; height: auto; }
  .luwho__copy { display: contents; }
  .luwho__title {
    grid-area: 1 / 1;
    align-self: end;
    justify-self: stretch;
    z-index: 2;
    position: relative;
    margin: 0 18px 18px;
    padding: 0;
    text-align: center;
  }
  /* 22px is the block's own rhythm; the rest is the laurel's overhang plus
     6px of air. Written as the DIP so a retune of the hang cannot leave the
     prose colliding with it — the kit shipped this as a bare `36px`, which
     is the dip + air already added up and no longer traceable to either. */
  .luwho__body {
    grid-area: 2 / 1;
    margin-top: calc(22px + var(--wreath-dip) + 6px);
  }
  /* 💤 DORMANT, NOT DEAD — do not sweep these three.
     `.luwho__copy` is `display:contents` here, so `width`/`margin`/`padding`
     apply to nothing and read as textbook dead CSS. They are the REVERT
     PATH: `luwhocard` hands the block back to s127's bare stack, where
     `.luwho__copy` is a box again and inherits the DESKTOP overlay's
     `width:min(74%,1020px)` + `margin:0 0 10px 14px` + `padding:34px 44px 40px`
     — a revert that "works" and silently ships a phone composition inset
     like a desktop one.
     ⌛ EXPIRY: delete all three the day `luwhocard` is retired as a revert
     path (i.e. once the card is locked and s127's stack is no longer a place
     anyone can go back to).
     `text-align` is the odd one out and is LIVE at every width — it inherits
     THROUGH a `display:contents` box to the title and the prose, and it is
     the whole of the `luwhocenter` fix. */
  .luwho__copy {
    width: auto;
    margin: 0;
    padding: 0;
    /* ⚠️ LOCK 🅲 — phones are CENTRE-ANCHORED site-wide, and this card was
       shipped outside that layer. styles.css's ≤767 rule only reaches
       `.text-section p`; `.luwho__copy` is a SIBLING of `.text-section`, so
       the base `text-align:left` survived into the stacked composition and
       set the h3 + two paragraphs hard left DIRECTLY UNDER a centred
       `.section-title`. That is the exact mismatch the site-wide layer was
       written for ("a left paragraph under a centred title"). Centred here,
       not in styles.css, because the alignment belongs to the STACKED
       composition and the fence for that is 1023, not 767.
       Revert word: luwhocenter. */
    text-align: center;
  }
  .luwho__copy::before,
  .luwho__copy::after { display: none; }
  /* ⛔ CREAM, NOT NAVY — s127's stacked layer set navy because the title
     sat on bare cream; it now sits ON the photograph. 28px is `.vcard__t`'s
     own phone size, carried whole rather than re-guessed. */
  .luwho__title {
    font-size: 28px;
    color: var(--cream);
  }
  .luwho__body p {
    font-size: 15px;
    color: var(--navy);
  }
}

/* ── 768-1023: THE SAME CARD, CAPPED ─────────────────────────────────────
   MY CALL, surfaced to Oleg — revert word `luwhocap`.
   The 4/5 plate is a PHONE composition and it does not scale: measured
   across the band it renders 704×880 at 768, 836×1045 at 900 and 959×1199
   at 1023 — by the top of the band the photograph is TALLER THAN THE
   VIEWPORT and a reader meets a wall of picture with the title somewhere
   off the bottom. (The reference has the same limit and dodges it by
   fencing its own 4/5 at 767 and handing 768+ back to the desktop deck —
   an exit this block does not have, because s127 measured the desktop
   OVERLAY breaking at 768 too. So the card stays across the whole stacked
   band and the plate gets a ceiling instead.)
   460px keeps it an editorial portrait plate rather than a backdrop, and
   centring it holds lock 🅲's centre axis. Phones are untouched: ≤767 the
   plate still fills its container, which is the composition Oleg approved.
   ⚠️ The CAP is on the plate only — the prose keeps the full measure
   below it, or a 460px column of centred text would set ragged and short. */
@media (min-width: 768px) and (max-width: 1023px) {
  .luwho picture {
    /* ⚠️ `width:100%` + `justify-self`, NEVER `margin-inline:auto`. The
       plate is a GRID ITEM, and auto side-margins stop it stretching to its
       column — so it falls back to its own content width, which is ZERO
       here (the <img> inside is absolutely positioned and contributes
       nothing). `aspect-ratio` then dutifully sized that nothing, and the
       card rendered 39×49 at every tablet width. Measured, not guessed. */
    width: 100%;
    max-width: var(--wreath-cap);
    justify-self: center;
  }
  /* THE LAUREL SHARES THE PLATE'S CAP — the defect this replaces:
     `width:%` resolves against the GRID COLUMN, not the picture. Below 768
     they are the same box, so the phone tuning was right. Above it the plate
     is pinned and the column is not, so they detach: measured 139% of the
     plate at 768, 165% at 900, 190% at 1023 — a laurel nearly twice the width
     of the photograph it cradles. Nothing overflowed the page, so no gate and
     no screenshot would have caught it. */
  .luwho__wreath { max-width: calc(var(--wreath-cap) * var(--wreath-ratio)); }
}


/* ═══════════════════════════════════════════════════════════════════════
   THE BALANCE — the coverflow (s133, 2026-08-26, Oleg-greenlit)
   Replaces the `.bal` ruled row. Five plates on one shared arc; the words
   ride ON the plate, only the number sits under it; arrows flank the strip.

   ⛔ THE MARKUP DELIBERATELY OMITS THE BARE `.cf` CLASS.
   Every rule this component inherits is `body.mastered .cf__stage` /
   `.cf__cell` / `.cf__arrow` — none of them needs a `.cf` ancestor. But
   `main.js`'s `initCoverflow()` starts with `document.querySelector('.cf')`,
   ONE coverflow per page: add `class="cf"` here and the shipped phone
   gallery driver seizes this block and double-drives it against
   `initBalanceFlow()`. The wrapper is `class="bcf cf--bal"` for exactly
   that reason. ⚠️ Anyone "restoring" the missing class breaks the page in a
   way that looks like an animation bug, not a class bug.
   The three `--cf-*` knobs the inherited rules read are re-declared below,
   because `body.mastered .cf` (where they normally live) no longer matches.

   Haptics come free: the arrows carry `.cf__arrow`, which is already in
   main.js's HAPTIC_FOR roster.
   ═══════════════════════════════════════════════════════════════════════ */
body.mastered .cf--bal{
  /* the knobs the inherited .cf__* rules read — normally set on `.cf` */
  --cf-w: 78;
  --cf-max-w: 440px;
  --cf-h-mult: 1.5;
  display:block;
}
.bcf{margin-top:clamp(10px,1.4vh,22px)}
/* the plate sits in the middle of the page like the reference */
.bcf .cf__stage{margin-inline:auto}

/* ---- the text stack, BELOW the plate (reference 2) ---- */
.bcf__read{
  margin:30px auto 0;max-width:44ch;text-align:center;
  padding-inline:20px;
}
.bcf__num{
  font-family:var(--font-sans);
  font-size:12px;font-weight:500;letter-spacing:.26em;
  color:var(--gold);
  transition:opacity 240ms var(--ease-soft);
}
/* it blinks out and back while the card it names is travelling */
.bcf__read.is-turning .bcf__num{opacity:0}

/* ============================================================
   H v2 — ONE SCREEN, FIVE CARDS (Oleg 2026-08-26)
   His brief, in his order:
   ① the whole block must land inside ONE screen — no scrolling
     to reach the text above or below the plate
   ② the card gets WIDER (not square — "still a little bit on the
     vertical side"), which is where the vertical space comes from
   ③ five cards on screen instead of three: centre ±2
   ④ text anchored centre and closer in (the title + line have since
     moved ONTO the plate — only the number is left under the card)
   ⑤ click a side card to jump to it · drag to move it (desktop)

   THE SIZING INVERSION, and why it is the whole trick:
   the shipped `.cf` sizes the card off the VIEWPORT WIDTH
   (`min(78vw, 440px)`) and derives height from the ratio. A block
   that must fit one SCREEN has to do the opposite — height first,
   width derived — or the card grows past the screen on any laptop.
   So `.cf--bal` sets `--bcf-h` from `vh` and hands the ratio the
   width. ⛔ Do not "restore" the width-first sizing on a port.
   ============================================================ */
.bcf-section{padding-block:clamp(18px,2.4vh,48px)}
/* the intro is centred with everything else now (Oleg: "I would
   anchor the text to the center") and held to two lines of measure */
.bcf-section .text-section{text-align:center}
.bcf-section .text-section p:not(.label){
  margin-inline:auto;max-width:62ch;text-wrap:balance;
}
@media (min-width:768px){
  /* TWO lines, not three (Oleg 2026-08-26) — a wider measure, and
     the third line's worth of height goes into the card.
     ⚠️ The cap that mattered was NOT on the paragraph: `.text-section`
     is a 720px prose column (styles.css § .text-section), so a `ch`
     max-width on the <p> could never widen it. Widen the COLUMN. */
  .bcf-section .text-section{max-width:1060px}
  .bcf-section .text-section p:not(.label){max-width:none}
}

@media (min-width:768px){
  /* ⚠️ LAWS ③ — these carry `body.mastered` on purpose. The shipped
     rules are `body.mastered .cf__cell` (0,3,1); a bare `.cf--bal
     .cf__cell` (0,2,0) LOSES, and it loses silently: the card keeps
     its width-first sizing, takes the new height too, and comes out
     LANDSCAPE. Measured, not reasoned — the first cut shipped a
     437x378 card. Match the donor's specificity or don't override. */
  body.mastered .cf--bal{
    /* height first — the card can never outgrow the screen */
    /* ⚠️ THE CARD IS SIZED BY SUBTRACTION, not by a share.
       "Everything on one screen" and "bigger cards" are the same budget
       pulling opposite ways, so the card is not given a vh share — it is
       given WHAT IS LEFT. The chrome (eyebrow + title + two-line intro +
       the number + padding) MEASURES 267px, and 60px more clears the page
       header. History, because the number moved every time the block did:
       372 guessed → 402 with a three-line intro → 375 → 267 once the
       title and line moved onto the plates. ⛔ Re-measure it whenever the
       copy above or below the card changes height; never estimate it. */
    --bcf-chrome: 267px;   /* re-MEASURED after the title+line moved onto the plates */
    --bcf-h:clamp(300px, calc(100svh - var(--bcf-chrome) - 60px), 760px);
    /* 60, not 48: the chrome's own paddings are vh-clamped, so the
       402px is exact at 900 and drifts ~11px taller by 1080. The
       extra 12px of allowance absorbs that drift instead of pretending
       the constant is constant. */
  }
  /* ARROWS TO THE SIDES (Oleg 2026-08-26: "remove this from the
     desktop… or add those buttons to the sides"). The row under the
     plate was ~62px of vertical budget, and that budget is exactly
     what the card needed. Flanking the stage costs zero height.
     ⚠️ `body.mastered` is REQUIRED here — the shipped rule is
     `body.mastered .cf__controls` (0,3,1) and a bare `.bcf__controls`
     (0,2,0) loses silently; the row stayed visible through a whole
     measurement before that was caught. Second time today.
     The COUNTER goes: it reads "1 — 5" while all five cards are on
     screen, which is a number for something the eye already has. */
  body.mastered .cf--bal{position:relative}
  body.mastered .cf--bal .bcf__controls{
    position:absolute;top:0;left:0;right:0;height:var(--bcf-h);
    /* OUT OF THE PICTURES (Oleg 2026-08-26: "put them out of the
       pictures"). The inset used to pull them inward, which parked
       both buttons ON TOP of the outermost photographs — invisible
       while those cards were blurred and faded, obvious the moment
       they went sharp. They now sit in the gutter beyond the strip,
       touching no card at any width. */
    /* the row spans the VIEWPORT, not the 1560 container, so the
       buttons can stand well off the strip instead of hugging it —
       20px of air read as "touching" (Oleg 2026-08-26). */
    margin-inline:calc(50% - 50vw);padding-inline:clamp(12px,2.4vw,40px);margin-block:0;
    display:flex;align-items:center;justify-content:space-between;
    pointer-events:none;z-index:20;
  }
  body.mastered .cf--bal .bcf__controls .cf__counter{display:none}
  body.mastered .cf--bal .bcf__controls .cf__arrow{
    pointer-events:auto;
    transition:opacity 340ms var(--ease-soft);
  }
  /* ⚠️ MY CALL, SAY THE WORD TO CHANGE IT: the arrows are always
     visible, because that is literally what was asked for. The
     alternative — start at opacity 0 and fade them in on approach,
     since the plate itself is the primary control — is one rule:
     set `opacity:0` above and add
     `.cf--bal:hover .cf__arrow, .cf__arrow:focus-visible{opacity:1}`.
     Revert word: `arrowfade`. */
  /* THE PHYSICS, borrowed from the donor: cubic-bezier(.6,.2,0,1) at
     0.55s for transform, 0.35s for opacity. Fast start, soft landing —
     the donor's own note is that this curve IS the feel, and changing it
     stops it being the same component. `body.mastered` is required: the
     shipped rule is `body.mastered .cf__cell` (0,3,1). */
  body.mastered .cf--bal .cf__cell{
    transition:transform .55s cubic-bezier(0.6,0.2,0,1),
               opacity .35s ease;
  }
  /* 🪤 THE PERSPECTIVE NEVER REACHED THE CARDS. The shipped component
     puts `perspective: 1000px` on `.cf__stage` — but a perspective only
     applies to an element's DIRECT children, and between the stage and
     the cards sits `.cf__track`. So every rotateY was resolving FLAT:
     a pure horizontal squash by cos(angle), no vanishing point, no near
     edge, no far edge. The cards were getting narrower, never folding.
     Convicted numerically before it was touched: a side card's height
     measured 391px against 465 × 0.84 = 390.6 — exactly the scale, with
     zero perspective magnification on the near edge.
     Cure: the perspective goes on the CARDS' OWN PARENT. Keeping it on
     one shared ancestor (rather than a per-card `perspective()` in the
     transform) is what gives the row ONE vanishing point — which is the
     difference between a shelf curving away and five cards each folded
     identically in place. */
  body.mastered .cf--bal .cf__track{
    /* ⚠️ DORMANT, NOT DEAD — do not sweep it. With the fold at 0 this
       declaration changes nothing on screen, so a dead-code pass will
       want to delete it. It is the enabling half of the fold: the
       perspective must sit on the cards' DIRECT parent, and putting it
       back on the stage (where the shipped component keeps it) is
       exactly the bug that made every rotation resolve flat. Delete it
       and the next person to set `rot` gets a silent horizontal squash
       instead of a fold, with nothing to point at. */
    perspective:1150px;
    perspective-origin:50% 50%;
  }
  body.mastered .cf--bal .cf__stage{
    height:var(--bcf-h);
    /* five cards need the whole container, and the stage clips
       horizontally (`overflow-x: clip`) — so give back the negative
       gutter margins the phone version needs and let the container
       hold the spread instead. */
    margin-inline:0;
  }
  body.mastered .cf--bal .cf__cell{
    width:auto;height:var(--bcf-h);
    /* 3/4 — the site's own gallery ratio. Wider than the shipped
       coverflow's 2/3 (Oleg: "a little bit wider… not 4x4
       territory"), and every step wider is height handed back to
       the one-screen budget. */
    aspect-ratio:3/4;
  }
}

.bcf__controls{margin-top:clamp(12px,1.6vh,18px)}

/* ---- ⑤ the side cards are targets, not decoration ---- */
.cf--bal .cf__cell{cursor:pointer}
.cf--bal .cf__cell[aria-hidden="false"]{cursor:default}
.cf--bal .cf__stage{
  cursor:grab;
  /* s158 (2026-08-27, Oleg on his iPhone: "the card itself swipes just
     the text inside of the cards") — the plates carry live text and a
     drag surface with selectable text hands iOS the WORDS: mid-swipe
     Safari starts a selection/callout on the paragraph and the gesture
     dies there. Same guard the .vdk deck's media already wears. The
     words on these plates are labels on a control, not copy to quote. */
  -webkit-user-select:none;user-select:none;
  -webkit-touch-callout:none;
}
/* ⚠️ DORMANT EVERYWHERE — do not sweep (s158e). Every pointer now commits
   its drag on release (one animated step, the phone contract, Oleg's
   17:09 verdict on the live-stepping feel), so nothing sets `.is-dragging`
   any more. These two rules + the mid-scrub park in main.js's bRender are
   the `livestep` revert's scaffolding — a restored stepper without them
   replays the 16:50 chaos recording. */
.cf--bal .cf__stage.is-dragging{cursor:grabbing}
.cf--bal .cf__stage.is-dragging .cf__cell{transition:none}

/* ---- PHONE: the title moves INSIDE the plate (Oleg's own idea,
   offered as a comment, built so he can see it). It buys back the
   title's line plus its two margins, and the body text then snaps
   up against the picture. ⚠️ This is the ONE place on this block
   where type sits on a photograph — revert word `phonetitle`. ---- */
@media (max-width:767px){
  /* the phone plate takes the same 3/4 as desktop AND a vh ceiling —
     the shipped 2/3 at 78vw is 470px tall at 402x874, which alone eats
     54% of the screen and pushes the controls under the fold. */
  body.mastered .cf--bal .cf__cell{aspect-ratio:3/4;height:auto}
  body.mastered .cf--bal .cf__stage{
    height:min(calc(78vw * 4 / 3), 47vh);
  }
  body.mastered .cf--bal .cf__cell{
    width:auto;height:min(calc(78vw * 4 / 3), 47vh);
  }
  .bcf-section{padding-block:clamp(22px,3vh,44px)}
  .bcf-section .text-section p:not(.label){font-size:15px;line-height:1.55}
  .bcf__num{font-size:11px}
  .bcf__plate{padding:52px 14px 14px}
  /* the arrows come up to sit directly under the plate they control —
     they were below the number, which put them a long way from the card
     on a phone. Order, not markup: the same DOM serves both widths. */
  body.mastered .cf--bal{display:flex;flex-direction:column}
  body.mastered .cf--bal .cf__stage{order:1}
  body.mastered .cf--bal .bcf__controls{order:2;margin-top:14px}
  /* s158 (Oleg, 2026-08-27: "this number should be gone… we already have
     another number of the card right above"). On the phone the controls
     row keeps its 3 — 5 counter, so the big gold number under it said the
     same thing twice. Phone-only: ≥768 the counter is display:none and
     the number is the ONLY index, so it stays there.
     Revert word: phonenum — swap this display:none back to the line it
     replaced: `body.mastered .cf--bal .bcf__read{order:3;margin-top:10px}`. */
  body.mastered .cf--bal .bcf__read{display:none}
}


/* ── THE GILDED RIM + GLINT, ported onto the cards (Oleg 2026-08-26:
   "check how beautiful the ring around the cards is… implement this
   ring in our picture gallery"). This is the house card language
   (COMPONENTS.md §10) — the identical construction `.inc-card`,
   `.swipe-card`, `.pricing-tiers__card` and `.party-est` all use: a 1px
   conic-gradient ring painted as a BORDER by masking the fill out of
   the middle (`mask-composite: exclude`), plus a second ring that
   sweeps a highlight round on hover.
   ⛔ `@property --glint-angle` and `@keyframes glint-spin` are declared
   ONCE in styles.css beside `.swipe-card` — the lab links the real
   sheet, so they are inherited. NEVER redeclare them here; that is the
   law that comes with this component.
   ⚠️ `isolation: isolate` is load-bearing, not habit: without it the
   rim competes with the NEXT card's ambient shadow instead of staying
   fenced inside its own card.
   ⚠️ The card must keep `overflow: visible` — the rim needs none, and
   clipping would cut the card's own shadow. */
.cf--bal .cf__cell{isolation:isolate}
.cf--bal .cf__cell::before,
.cf--bal .cf__cell::after{
  content:'';position:absolute;inset:0;
  border-radius:inherit;padding:1px;
  -webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);
  mask-composite:exclude;
  pointer-events:none;z-index:3;
}
.cf--bal .cf__cell::before{
  background:conic-gradient(from -90deg,
    #C4A265 0%, #ffffff 2%, #D4B97A 6%,
    rgba(196,162,101,.35) 18%, rgba(138,124,94,.45) 30%,
    rgba(196,162,101,.30) 42%, #D4B97A 52%,
    rgba(154,138,104,.45) 62%, rgba(196,162,101,.35) 72%,
    #C4A265 80%, #ffffff 84%, #D4B97A 88%, #C4A265 100%);
}
.cf--bal .cf__cell::after{
  background:conic-gradient(from var(--glint-angle, 0deg),
    transparent, var(--gold-lt) 9%, rgba(255,253,248,.95) 11%,
    var(--gold-lt) 13%, transparent 22%);
  opacity:0;transition:opacity 500ms var(--ease-smooth);
  animation:glint-spin 11s linear infinite;
  animation-play-state:paused;
}
@media (hover:hover){
  .cf--bal .cf__cell:hover::after{opacity:1;animation-play-state:running}
}
@media (prefers-reduced-motion:reduce){
  .cf--bal .cf__cell::after{animation:none;opacity:0}
}

/* THE BLUR LAYER (Oleg 2026-08-26: "the blur also must affect the text").
   ⛔ The filter CANNOT go on the card. The shipped component carries an
   explicit ban — a card holding BOTH a 3D transform and a live filter is
   a spec conflict (a filter forces transform-style to flat), and WebKit
   resolved it by dropping the card's shadow for the whole transition.
   That was convicted on a 60fps recording and the rule in styles.css
   reads "⛔ Do not put filter back on this rule."
   So the blur rides an inner layer that holds the photo AND the words,
   while the card keeps its shadow, its 3D and its rim un-blurred. */
.bcf__inner{
  position:absolute;inset:0;
  border-radius:inherit;
  overflow:hidden;               /* also rounds the photo's own corners */
  transition:filter .55s cubic-bezier(0.6,0.2,0,1);
  /* 🔴 THE EMPTY PLATE (Oleg 2026-08-26 — shipped blank on purpose, so the
     missing photographs stay visible as work owed). A calm cream surface
     with the house diamond, NOT a broken-image box: it has to read as a
     plate waiting for a picture. Adding an <img> as the first child covers
     it completely and nothing here needs removing. */
  background:#E4DCD2;
}
.bcf__inner::after{
  content:'';position:absolute;left:50%;top:50%;
  width:11px;height:11px;transform:translate(-50%,-50%) rotate(45deg);
  background:rgba(196,162,101,.5);
}
.bcf__inner:has(img)::after{content:none}
.bcf__inner img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}

/* THE WORDS MOVE ONTO THE PICTURE (Oleg 2026-08-26, lab experiment).
   EVERY card carries its own title + line on a dark scrim at its foot;
   only the number stays under the plate. They are static per card, so
   the driver no longer writes them.
   ⛔ The scrim rounds its own bottom corners — the cell deliberately
   does NOT clip (its shadow has to escape the box), so nothing else
   will round them for it. */
.bcf__plate{
  position:absolute;left:0;right:0;bottom:0;z-index:2;
  margin:0;padding:64px 18px 18px;
  text-align:center;color:#fff;pointer-events:none;
  border-radius:0 0 12px 12px;
  background:linear-gradient(180deg,rgba(20,16,12,0),rgba(20,16,12,.52) 44%,rgba(20,16,12,.8));
}
/* ⛔ div + p, NOT figcaption + span. i18n's collector emits the FIRST
   phrasing-only ancestor as ONE unit keyed on its innerHTML and then
   RETURNS without descending — a figcaption wrapping two spans would key
   as `<span…>Title</span><span…>Line</span>`, which is in no dictionary,
   and both strings would ship English in every language with nothing
   broken to notice. A DIV is not a text tag, so the walker descends and
   each <p> gets its own key. */
.bcf__plate-t{margin:0;font-family:var(--font-serif);font-weight:400;
  font-size:clamp(20px,2vw,26px);line-height:1.15}
.bcf__plate-l{margin:6px 0 0;
  font-size:clamp(12px,1.05vw,14px);line-height:1.5;
  color:rgba(255,255,255,.82);text-wrap:balance}
/* under the card: the number and nothing else */
.bcf__read{margin-top:clamp(8px,1.2vh,14px)}

/* 🪦 The `.bal*` ruled-row rules this section replaced are GONE — swept
   before s147, which verified by selector grep that zero remain (only
   comments mention .bal now). Nothing here is dead; the old "remove in a
   dedicated pass" note described rules that no longer exist. (s147) */

/* ═══════════════════════════════════════════════════════════════════
   § SESSION SHEET — .sheet (s145, 2026-08-26)
   The contact sheet that REPLACED the .flow spine rail (Oleg killed
   the rail s145; C was his pick from a 5-candidate lab —
   04-WORKSHOP/block-labs/levelup-flow-lab.html, brief § Round 4).
   Self-contained at the foot of the file WITH its own phone layer —
   the .bcf precedent above; nothing below this banner reaches any
   other block. 4-up strip: blank plate, numeral, label, title, desc
   per cell; ≤1023 the row is the house swipe strip; ≤767 centred (🅲).
   ✅ Plates FILLED s149 (2026-08-27) with Oleg's session footage; they
   shipped empty s145-s149 on purpose (the Balance precedent). The empty
   state is STILL LIVE CODE, not history: calm cream + the house diamond,
   value-copied from .bcf__inner — NOT selector-shared (§10 law). An
   <img> in .sheet__frame covers it and :has(img) removes the diamond,
   so deleting a tag returns that one plate to owed.
   ═══════════════════════════════════════════════════════════════════ */
/* s145b (Oleg, 2026-08-26 21:13): the head anchors to the CENTRE — the
   .bcf-section recipe one section up, selector shape value-copied. The
   whole text-section centres (label rides along, same as the Balance). */
.sheet-section .text-section{text-align:center}

.sheet{margin-top:56px}
.sheet__row{
  display:grid;
  grid-template-columns:repeat(4,1fr);
  gap:22px;
}
/* ── THE PLATE WEARS THE CARD (Oleg, 2026-08-27, candidate A of the card
   lab: "A 100%"). He asked for the homepage card on this block and he now
   has all of it: the 12px corner, the cream body, the gilded rim, the
   three-layer depth, and — added 90 minutes later on his word — the hover
   and the entrance too (see THE HOVER and the .reveal-card wiring below).
   ⚠️ This banner said "no hover, no glint, no link" for exactly those 90
   minutes. Two of the three are now false and the correction is the point:
   the link is the one that stayed. A phase still goes nowhere, so the card
   never became an <a> and never grew a "Learn More".
   Candidate B (the literal .swipe-card, 3:4 portrait) was benched on
   height: +52% at 1440 and +64% at 402, which walked Pricing off the
   first screen. It is still in
   04-WORKSHOP/block-labs/levelup-format-card-lab.html.
   ⚠️ Rim, glint and shadow are VALUE-COPIED from .swipe-card, never
   selector-shared (COMPONENTS §10); @property --glint-angle and
   @keyframes glint-spin stay declared ONCE in styles.css and are used
   here, never redefined.
   Superseded state (flat plate on bare satin, s145-s149): the whole file
   is byte-exact in 05-SYSTEM/scratchpad/2026-08-27-s149c-preship-backup/.
   Revert word: sheetflat. */
.sheet__cell{
  position:relative;                     /* anchors the rim pseudo (§10) */
  /* the column is what lets .sheet__body own a region of the card rather
     than float in it; the equal card BOTTOMS come from the grid's own
     default stretch, not from anything declared here */
  display:flex;flex-direction:column;
  background:var(--cream-dark);
  border-radius:12px;                    /* the .swipe-card corner, NOT --radius-card */
  overflow:hidden;
  border:1px solid rgba(196,162,101,.1); /* the no-mask-support fallback (§10) */
  box-shadow:
    0 1px 2px rgba(17,24,39,.05),
    0 6px 16px rgba(17,24,39,.06),
    0 16px 40px rgba(27,40,56,.08);
  /* ⚠️ LAWS ③, THE PROJECT'S TOP REPEAT OFFENDER, CAUGHT BEFORE IT SHIPPED.
     The cell carries `.reveal-card`, whose 2200ms opacity+transform entrance
     is declared in styles.css. A `transition:` on `.sheet__cell` in THIS file
     is the same weight (0,1,0) and loads LATER, so it REPLACES that list and
     the entrance would silently run at whatever this rule says. So this list
     is the `.swipe-card` list, verbatim (proven equal by reading both rules
     off the live page) — the entrance keeps 2200ms and the fast hover comes
     from initCardTilt writing an INLINE transition on mouseenter, exactly as
     on the homepage. ⛔ Never shorten these two durations to "fix" a
     slow-feeling hover: that is the entrance, and the hover is not CSS. */
  transition:
    opacity 2200ms cubic-bezier(0.16,1,0.3,1),
    transform 2200ms cubic-bezier(0.16,1,0.3,1),
    box-shadow 400ms var(--ease-fluid);
}
.sheet__cell::before{
  content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;
  background:conic-gradient(from -90deg,
    #C4A265 0%, #ffffff 2%, #D4B97A 6%,
    rgba(196,162,101,.35) 18%, rgba(138,124,94,.45) 30%,
    rgba(196,162,101,.30) 42%, #D4B97A 52%,
    rgba(154,138,104,.45) 62%, rgba(196,162,101,.35) 72%,
    #C4A265 80%, #ffffff 84%, #D4B97A 88%, #C4A265 100%);
  -webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);
  mask-composite:exclude;
  pointer-events:none;
  z-index:3;                             /* rides ABOVE the full-bleed photo (§10) */
}
.sheet__body{padding:20px 20px 24px}
.sheet__frame{
  position:relative;
  aspect-ratio:4/3;
  /* ⛔ no radius, no bottom margin any more — the CARD owns the corner and
     the BODY owns the gap. A radius here would draw a second, smaller
     curve inside the card's own (superseded s149c). */
  overflow:hidden;
  background:#E4DCD2;
}
/* the empty-plate diamond (value-copied from .bcf__inner, s136) */
.sheet__frame::after{
  content:'';position:absolute;left:50%;top:50%;
  width:11px;height:11px;transform:translate(-50%,-50%) rotate(45deg);
  background:rgba(196,162,101,.5);
}
.sheet__frame:has(img)::after{content:none}
.sheet__frame img{width:100%;height:100%;object-fit:cover;display:block;
  transition:transform 900ms var(--ease-soft)}
/* ── THE FRAMING KNOB (Oleg's call, s149 2026-08-27) ──────────────────
   The plates ship UNCROPPED — his Photoshop cut IS the master and no
   build step re-crops it. The 4:3 window does the last bit of framing,
   and this is the only place it is aimed: one line per plate. `50% 50%`
   is the house default and is inherited by any plate not listed.
   ⚠️ These are STARTING VALUES, tuned live with Oleg, not a lock — plate
   04 walked 30 → 60 → 72% on his eye. Plates 01 and 03 carry a data-plate
   with NO rule on purpose: the attribute is the handle, so a future tune
   is one line here and zero markup edits.
   The phone half of this knob is at the foot of this section and is EMPTY
   on purpose — the window is the same 4:3 at 74vw, so every anchor held
   at 402 and restating them would have been noise (LAWS ㉓). */
.sheet__frame img{object-position:50% 50%}
.sheet__frame[data-plate="02"] img{object-position:60% 50%}
.sheet__frame[data-plate="04"] img{object-position:50% 72%}
/* ── THE HOVER, THE WHOLE HOMEPAGE ONE (Oleg, 2026-08-27 10:49) ───────
   History, because the two halves look contradictory and are not.
   09:xx he killed the plate zoom off the live page: the plates were bare
   photos then, and on his PHONE a tap latched `:hover` and the photo crept
   with nothing to explain why. 10:49, once the plates were cards, he asked
   for the homepage hover back — "I want to hover over the card and I expect
   the card to have this beautiful zoom as on a home page." I had argued a
   plate is not a door; he overruled, and this is his site.
   What changed between the two is REAL: a bare photo answering a touch is
   noise, a CARD answering a mouse is the house language.
   ⚠️ The whole machine is fenced inside `@media (hover:hover)`, so a phone
   never reaches it and the thing he killed cannot come back. The ≤1023
   tap-guard below is the second lock, for hybrids that claim hover.
   ⚠️ TRANSFORM HAS TWO OWNERS AND THEY DO NOT FIGHT. CSS owns the resting
   hover (lift + scale) so the card still answers without JS; initCardTilt
   then writes an INLINE transform that supersedes it and adds the
   mouse-follow rotation. On mouseleave JS clears its inline value and the
   cursor has already left, so `:hover` no longer matches either.
   ⚠️ Safe from LAWS ㊸ (the hover that restarts the entrance): `.reveal`
   lives on `.sheet`, the container — never on `.sheet__cell`. The hovered
   element and the animated element are different elements.
   Revert word: platehover. */
.sheet__cell::after{
  content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;
  background:conic-gradient(from var(--glint-angle,0deg),
    transparent, var(--gold-lt) 9%, rgba(255,253,248,.95) 11%,
    var(--gold-lt) 13%, transparent 22%);
  -webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);
  mask-composite:exclude;
  pointer-events:none;z-index:3;opacity:0;
  transition:opacity 500ms var(--ease-smooth);
  /* @property --glint-angle and @keyframes glint-spin are declared ONCE in
     styles.css beside .swipe-card — USED here, never redefined (§10) */
  animation:glint-spin 11s linear infinite;
  animation-play-state:paused;
}
@media (hover:hover){
  .sheet__cell:hover{
    transform:translateY(-6px) scale(1.01);   /* the initCardTilt values */
    box-shadow:
      0 2px 3px rgba(17,24,39,.06),
      0 14px 30px rgba(27,40,56,.11),
      0 30px 60px rgba(27,40,56,.12);
  }
  .sheet__cell:hover .sheet__frame img{transform:scale(1.04)}
  .sheet__cell:hover::after{opacity:1;animation-play-state:running}
}
.sheet__num{
  margin:0 0 6px;
  font-family:var(--font-serif);font-weight:300;
  font-size:28px;line-height:1;color:var(--gold);
}
.sheet__label{
  margin:0 0 8px;
  font-size:10.5px;font-weight:500;letter-spacing:.18em;text-transform:uppercase;
  color:var(--brown);
}
.sheet__title{
  margin:0 0 8px;
  font-family:var(--font-serif);font-weight:400;
  font-size:21px;line-height:1.25;color:var(--navy);
  text-wrap:balance; /* LAWS ㉘ — a new block ships balanced */
}
.sheet__desc{
  margin:0;
  font-size:14px;font-weight:300;line-height:1.65;color:var(--text-light);
}
@media (max-width:1023px){
  /* the house swipe row — one container reveal, cells snap per card */
  .sheet__row{
    display:flex;overflow-x:auto;scroll-snap-type:x mandatory;gap:16px;
    margin-inline:-32px;padding-inline:32px;scroll-padding-inline:32px;
    -webkit-overflow-scrolling:touch;scrollbar-width:none;
    /* §10 shadow-budget arithmetic: the deepest layer reaches ~36px below
       the card, and an overflow-x scroller clips the other axis too. Book
       the room here or the row guillotines its own depth. */
    padding-block:16px 44px;   /* 16 not 8: the card lifts 6px on a hybrid
                                 that reports hover, and the lift must not
                                 clip against the scroller's top edge */
  }
  .sheet__row::-webkit-scrollbar{display:none}
  .sheet__cell{flex:0 0 58vw;max-width:400px;scroll-snap-align:center}
}
@media (max-width:767px){
  .sheet{margin-top:40px}
  .sheet__row{margin-inline:-20px;padding-inline:20px;scroll-padding-inline:20px}
  .sheet__cell{flex:0 0 74vw;max-width:340px;text-align:center}
  /* phone half of the framing knob — EMPTY ON PURPOSE. The desktop values
     above apply until a plate is listed here; the cell is 74vw and the
     window is the same 4:3, so the same anchors held at 402. Add one line
     per plate that needs a different anchor down here; a plate that reads
     correctly at both widths stays out rather than being restated (LAWS ㉓). */
}
/* ⚠️ SOURCE ORDER IS THE WHOLE POINT — this guard MUST stay below the
   `@media (hover:hover)` block above. The homepage's own tap-guard sat ~750
   lines ABOVE its hover rules and was dead for months (§10 trap). iOS latches
   `:hover` on tap, so ≤1023 a touched card re-asserts its REST state: no
   lift, no photo creep, no glint. That is the thing Oleg killed this morning,
   and it stays killed on touch. */
@media (max-width:1023px){
  .sheet__cell:hover{
    transform:none;
    box-shadow:
      0 1px 2px rgba(17,24,39,.05),
      0 6px 16px rgba(17,24,39,.06),
      0 16px 40px rgba(27,40,56,.08);
  }
  .sheet__cell:hover .sheet__frame img{transform:none}
  .sheet__cell:hover::after{opacity:0;animation-play-state:paused}
}
@media (prefers-reduced-motion:reduce){
  .sheet__cell,.sheet__frame img{transition:none}
  .sheet__cell::after{animation:none;opacity:0}
}
