/* ==========================================================================
   4 — THE CURRENT LANDSCAPE / THE COLLECTIVE
   TWO MATCHING PANELS — R7d, by direction. The problem column now takes the
   answer panel's exact treatment: same ground, same hairline, same padding,
   same separator, same badge-and-row list.

   ⚠ §14's SIX-SIGNAL REQUIREMENT STANDS AT THREE OF SIX — R7e, R7f, R7w.
   That rule exists for this section specifically: "Six non-colour signals
   separate the sides. Colour alone must never carry it."

     1. GLYPH SHAPE   cross vs tick                                  ✓ survives
     3. GROUND        bare column vs panel       ✓ RESTORED at R7f,
                      and at R7x it became the strongest signal in the
                      section: 1.17:1 on the ✗ side against 19.26:1 on the ✓
     7. ROW STRUCTURE boxed rows vs rows in a box ✓ NEW at R7w
     2. CELL COUNT    36 vs 0                    ✗ spent at R7e — crowd removed
     4. TYPE COLOUR   muted vs white             ✗ spent at R7d — same rows
     5. TYPE SIZE     body vs body-lg            ✗ spent at R7d — same rows
     6. ITEM SPACING  24 vs 32                   ✗ spent at R7d — same rows

   §18's HARD FLOOR HOLDS with real room now: colour is not the sole carrier,
   and there are three shape/structure differences rather than one.
   Spending BOTH — same badge glyph on each side, and the box back on the
   problem column — would make the opposition hue-only and fail §18 outright.

   TO BUY A SIGNAL BACK, cheapest first:
     · type colour   .contrast__row on the problem panel → --color-text-muted
     · type size     that same row → --font-size-body
     · the crowd     restore the 36-cell band (also restores §4 to the
                     cell-motif budget, which fell from five sections to four
                     when it went)

   §16 IS HALF-REPAIRED. Narrative sections 3–7 may never be carded; one side
   is now bare editorial copy and only the answer is a panel. That is still a
   card in a narrative section, so the section still counts against the
   three-card cap it shares with §10 and §11 — but the argument's problem half
   reads as prose again, which is what the rule was protecting.

   §16 IS STILL BROKEN, unchanged from R7c: narrative sections 3–7 may never
   be carded, and this is now two panels rather than tiles plus a panel. The
   page remains AT the three-card-section cap with §10 and §11.
   ========================================================================== */

/* --------------------------------------------------------------------------
   R8h — THE BOX IS BACK, AT #171717. By direction. The section ground stays
   black (R8g); .contrast__box returns as --color-surface-panel.

   ⚠⚠ IT COLLIDES WITH THE ✗ ROWS, AND THE FIX IS AN INVERSION, NOT A NUDGE.
   Since R7w the ✗ rows have been #171717 boxes — the SAME token. Dropped into
   a #171717 container they measure 1.00:1 and cease to exist. There is no
   neighbouring grey that rescues them either; the dark end of the palette is
   too tightly packed:

     on a #171717 box   #171717  1.00:1   #1A1A1A  1.03:1   #121212  1.04:1
                        #000000  1.17:1   ← the only value that reads

   So the ✗ side turns inside out. The rows become BLACK blocks punched into a
   grey container, where they were grey blocks raised off a black one. The
   ladder is identical in magnitude, just reversed:

     was   box #000000 -> row #171717  1.17:1 -> badge #000000  1.17:1
     now   box #171717 -> row #000000  1.17:1 -> badge #1A1A1A  1.21:1

   ✅ THE BADGE GOES BACK TO ITS ORIGINAL TOKEN. R7w had inverted it to
   --color-bg-base precisely because the row it sat on had become grey; with
   the row black again, --color-surface-hover is correct once more and the
   R7w override is deleted rather than re-tuned. The glyph on it is unchanged
   at 5.04:1.

   ✅ SIGNAL 7 SURVIVES INTACT. §14's "boxed rows vs plain rows" was never
   about which colour the boxes were — the ✗ side is still discrete blocks and
   the ✓ side is still loose rows inside one panel.

   MEASURED against the new box: answer panel 16.44:1 · ✗ title and lead
   17.93:1 · ✗ body 8.65:1 · ✗ row text on the black rows 21:1 · the box
   itself 1.17:1 off the page base.

   THE overflow CAVEAT IS RETIRED (R8s). It existed because
   .contrast__panel--answer was position: sticky inside this box, and any
   overflow other than visible on an ancestor would have turned that ancestor
   into the scroll container and killed the stick silently. The panel is no
   longer sticky, so the box may take an overflow value if one is ever needed.
   -------------------------------------------------------------------------- */
/* R17c — 48 -> 24 at mobile, by direction. §9a sets panel padding at
   48/64/80, and 48 is the value it names for the smallest step — but that
   step was written for a panel sitting inside a 1280 container, not one
   sitting inside a 342px viewport. At 390px the box was spending 96px of its
   width, more than a quarter of the screen, on its own inset.

   --space-md (24) is --gap-card-padding's mobile value, so the box now insets
   exactly as far as every card on the page does at this width. The >=992 rule
   below is untouched: nothing about the desktop panel changes. */
.contrast__box {
  background-color: var(--color-surface-panel);
  border-radius: var(--radius-control);   /* 0 — R6x */
  padding: var(--space-md);
}

@media (min-width: 992px) {
  .contrast__box { padding: var(--space-3xl); }
}

.contrast__inner { display: grid; gap: var(--space-2xl); }

/* 5/7 SINCE R8i, by direction — the answer panel takes the wider column.

   ⚠ 5/7 IS THE ONLY LEGAL WAY TO DO THIS. §15's layout vocabulary is full ·
   halves · thirds · 7-5 asymmetric and nothing else; an arbitrary 4/8 or 55/45
   would add a fifth pattern, and the vocabulary is what stops fifteen sections
   drifting into fifteen bespoke grids. This is the existing 7-5, mirrored.

   ⚠ IT DILUTES THE PAGE'S ASYMMETRIC PAIR. §15 records §2 (7/5) and §9 (5/7)
   as a deliberate mirrored pair, seven sections apart. There are now THREE
   asymmetric splits — 2 (7/5), 4 (5/7), 9 (5/7) — and two of them are the
   same split, five apart. No consecutive repeat, so §15's letter holds; the
   "mirrored pair" reading does not, because a pair with a third member is not
   a pair. Worth deciding rather than inheriting.

   ✅ IT DOES NOT COST AN ADJACENCY. Built 3 is thirds, built 5 is uncontained
   thirds, so 4 sitting between them as an asymmetric split still breaks the
   run — the same job halves was doing.

   ⚠ THE LEFT COLUMN'S TRAILING INSET NOW WORKS AGAINST IT. See the note on
   .contrast__panel--problem: 48px of padding-inline-end was added to a
   HALF-width column to stop its copy crowding the panel edge. Measured in the
   box at a 1280 container (1056px inside the box's own 64px padding, minus
   the 48px gutter):

     halves   left 504px   right 504px
     5/7      left 420px   right 588px

   The left column loses 84px. Taking a further 48px on top would set the copy
   in 372px. The inset is removed below.

   GUTTER 32 → 48 (R7i) → 64 (R8j), by direction.

   THE OLD ARGUMENT FOR 32 WAS THAT IT SAT NARROWER THAN THE SECTION'S OWN
   48px SIDE PADDING, so the two halves bound together and read as ONE
   opposition rather than two adjacent blocks. That reasoning was written when
   BOTH sides were bare columns and the gutter was the only thing holding
   them. It no longer applies the same way: since R7f the answer carries a
   panel edge and the problem does not, so the pairing is now read from the
   contrast between the two grounds, not from how tight the gap is.

   What 32 was actually costing: the problem column's text runs the full width
   of its cell and stopped 32px short of the panel's border, with the panel's
   own 32px of padding behind that. The left copy crowded an edge it had no
   relationship to.

   R7i CALLED 48 THE CEILING, and the reasoning was sound for the layout it
   was written about: a gutter should not exceed the margin enclosing it, or
   the two halves stop binding and read as two adjacent blocks. At the time
   the enclosing margin was the SECTION's 48px side padding, so 48 was the
   ceiling and 64 was over it.

   ⚠ THE ENCLOSING MARGIN MOVED AT R8h. The panels no longer sit against the
   section's side padding — they sit inside .contrast__box, whose own padding
   is 64px at ≥992. The gutter is measured against that now, so the same rule
   that made 48 the ceiling makes 64 the ceiling: it matches the box's inset
   exactly, which is where R7i's argument actually pointed.

   THE RULE IS UNCHANGED; ONLY WHAT ENCLOSES IT CHANGED. 80 would be over the
   new ceiling the way 64 was over the old one, and there is no scale step
   between 48 and 64 in any case.

   Column widths at a 1280 container, inside the box:

     gutter 48   left 420px   right 588px
     gutter 64   left 413px   right 579px  ← 7px each, the cost of the change */
@media (min-width: 992px) {
  .contrast__inner {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: var(--space-3xl);          /* 64 — R8j, matching the box's own inset */
    align-items: start;
  }
}

/* --------------------------------------------------------------------------
   THE COLUMNS — R7f. The box is now the ANSWER's alone.

   ✅ THIS BUYS BACK A SIGNAL. R7d made both sides the same panel and spent
   signal 3 (ground) doing it; the problem column losing its box restores it.
   §14 goes from ONE surviving signal back to TWO — see the count at the top
   of this file. It also puts the section back in the editorial register §16
   asks of sections 3–7 on at least one side.

   Type, rows and badges stay shared. Only the ground differs, which is
   exactly what a signal should be.
   -------------------------------------------------------------------------- */
/* R7x — THE ANSWER PANEL IS WHITE, by direction, with black and grey type.
   Was #171717 (R7g), which is now the ✗ rows' colour instead (R7w).

   ✅ IT MAKES SIGNAL 3 THE STRONGEST THING IN THE SECTION. §14's "bare column
   vs panel" was a 1.17:1 distinction — #171717 blocks on a black ground,
   which is a difference you have to look for. The two sides now sit at
   opposite ends of the luminance range inside one black box: 1.17:1 on the ✗
   side, 21:1 on the ✓ side. And it is a LUMINANCE inversion, not a hue one,
   so it strengthens the section against §18's "colour is never the sole
   carrier" rather than leaning on it.

   THE BORDER GOES TRANSPARENT, NOT AWAY. It was rgba(255,255,255,0.10),
   tuned for a dark panel on a dark ground, and it is invisible on white
   anyway. A white panel against a black box needs no edge — it is already at
   maximum contrast. Transparent rather than `border: none` so the 1px box
   metrics the grid was laid out against do not move.

   ✅ #F5F5F5, NOT #FFFFFF — R7z, by direction, and it tidies the palette
   rather than adding to it. The panel briefly took --color-surface-invert,
   which made four light surfaces across two values. The page now holds two
   light values: #F5F5F5 here, and #FFFFFF reserved to §2a's plate — the only
   pure white, and therefore still the brightest thing on the page.

   ✅ R7z's OPEN RISK IS CLOSED, not triggered. It warned that this panel, the
   black box and the §4 ground shared enough that "if that box ever loses its
   fill, the panel and the ground merge into one shape with no boundary at
   all." R8g removed the box — and took the ground dark in the same move, so
   this panel now reads 19.26:1 against it. The dependency is retired.

   ⚠ IT IS THE ONLY CONSUMER OF --color-surface-light left. If §4 ever goes
   fully dark, that token, --color-surface-light-tile, --color-surface-light-
   block, --color-border-on-light and both on-light type colours all lose
   their last consumers together. */
.contrast__panel--answer {
  background-color: var(--color-surface-light);
  border: var(--border-hairline) solid transparent;
  border-radius: var(--radius-control);   /* 0 since R6x */
  padding: var(--space-lg);
}

/* Type on the white panel. The section's other half keeps the dark-ground
   values, so these are scoped rather than applied to the shared rules.

     title · lead · rows   #000000   19.26:1
     body                  #5F5F5F    5.86:1
     the ✓ badge #6146ED as a shape    5.34:1   (needs 3:1)

   ⚠ THE BODY GREY IS §3's LIGHT-GROUND BODY TOKEN, not --color-text-muted.
   Muted is #8A8A8A and measures 3.17:1 here — a FAIL for normal text at
   this size. The grey that reads as "grey" on black is not the grey that
   reads as grey on white, which is the whole reason the on-light set exists. */
.contrast__panel--answer .contrast__title,
.contrast__panel--answer .contrast__lead { color: var(--color-text-heading-on-light); }

.contrast__panel--answer .contrast__body { color: var(--color-text-body-on-light); }

/* R8b — THE ✓ ROWS TAKE §3's ITEM-BODY TREATMENT, by direction: same size,
   same weight, same colour as .silos__item-body.

     font-size    --font-size-small        16px at every breakpoint
     font-weight  --font-weight-regular    400 (§3 inherits it; set here)
     colour       --color-text-body-on-light  #5F5F5F — 5.86:1, passes AA

   ⚠ 16px IS §16's HARD MINIMUM BODY SIZE. There is no step below it, so this
   copy cannot be reduced again — and it does not scale down on mobile either,
   because --font-size-small is 16 at every breakpoint.

   ⚠⚠ IT RE-OPENS §14's SIGNALS 4 AND 5, BUT INVERTED, AND THAT IS WORTH A
   DECISION RATHER THAN A SHRUG.

     was (pre-R7d)   ✗ muted / body      ✓ white / body-lg
     now             ✗ white / body-lg   ✓ grey  / small

   The count goes from four signals to six, which satisfies §14 arithmetically
   — but §14 exists to keep the ARGUMENT legible, and the argument is that the
   ✓ side is the answer. The problem list is now the larger, whiter, louder
   half and the answer is the quiet one. The composition still favours the ✓
   side (violet banner, light panel, round badges), so it does not read as
   backwards outright. It is a deliberate cost, not an oversight: the
   instruction was explicit and the direction it points is recorded here. */
.contrast__panel--answer .contrast__row {
  /* R8d — 16 → 18 (--font-size-body), by direction. Weight and colour are
     unchanged and still match §3's item body; the SIZE match R8b made no
     longer holds, since §3 is --font-size-small.

     ⚠ SIGNAL 5 IS NEARLY GONE, and the token table is why:

                       mobile  tablet  desktop
       ✗ body-lg        17px    18px    20px
       ✓ body           17px    18px    18px

     The two are IDENTICAL below 992px. §14's type-size signal now exists at
     desktop only, and there as a 2px difference — the same trap already
     recorded on §3's item title. Signals 1, 3, 7 and 8 are structural and
     unaffected, so §18's floor is not at risk; the six-of-six count is. */
  font-size: var(--font-size-body);
  /* R8e — 400 → 300, by direction. 300 is the only step down: Mundial
     publishes 100/200/300/400/600/700/900 and there is nothing between 300
     and 400.

     ⚠⚠ THIS BREAKS §16's WEIGHT-300 FLOOR — "22px and above only" — at 18px
     desktop and 17px mobile. It is amended in design-rules first (R8e, under
     §18), not broken silently. The short version:

     The floor's own stated reason is "thin strokes on PURE BLACK thin further
     on most displays" — halation. A light stroke on a dark field blooms at
     the edges and eats into an already-thin stem. On a LIGHT ground the
     effect runs the other way: dark strokes on light gain apparent weight.
     The floor was guarding a failure mode that is not present here, so the
     carve-out is scoped to the ground, not to this section. Weight 300 below
     22px stays prohibited on every dark surface on the page.

     WHAT IS NOT WAIVED: the ratio has to carry it alone, because WCAG takes
     no account of weight below the large-text bold threshold. #5F5F5F on
     #F5F5F5 is 5.86:1 against the 4.5 needed — real headroom. A pairing near
     4.5 would not get this. And 17px mobile is the actual floor being
     tested, five under the written limit. */
  font-weight: var(--font-weight-light);
  color: var(--color-text-body-on-light);
}

/* R8c — TIGHTER ROWS AND A SMALLER TICK, by direction. ✓ SIDE ONLY: the
   instruction named "the purple tick icons", and the ✗ badges are neither
   purple nor the same shape any more.

   SPACING 32 → 24. Both values are on the scale; 24 is one step down. It
   follows R8b, which took this copy from 20px to 16px — a 32px gap was
   proportioned to the larger type and left the list reading as four separate
   statements rather than one set.

   ⚠ SIGNAL 6 COMES BACK, AND INVERTS LIKE 4 AND 5 DID. It was ✗ 24 / ✓ 48,
   the answer given the more generous rhythm. It is now ✗ 32 / ✓ 24. Same
   note as R8b: the count is satisfied, the emphasis is not. All three
   type-and-rhythm signals now point at the problem half; only the
   composition still points at the answer.

   BADGE 24 → 20. See --icon-box-size-xs: the floor is the 15px glyph, not
   taste. The ✗ badge stays 24px and square, so the two sides now differ in
   badge SIZE as well as shape. */
.contrast__panel--answer .contrast__rows { gap: var(--space-md); }

/* R8p — the ✓ badge takes the ✗ badge's 28px, by direction. The two are the
   same size again; --icon-box-size-xs (24px) has no consumer left. */
.contrast__panel--answer .contrast__badge {
  inline-size: var(--icon-box-size-badge);
  block-size: var(--icon-box-size-badge);
}

/* --------------------------------------------------------------------------
   R8a — THE ANSWER PANEL BECOMES THREE STACKED BLOCKS, from the supplied
   reference: a violet banner, a grey brief with a row of faces, then the
   list. The ✗ side is untouched and stays three boxes in a bare column.

   ✅ IT ADDS A NON-COLOUR SIGNAL. §14 wants six separating the sides and the
   count stood at three. The two halves now have different INTERNAL STRUCTURE
   — the ✗ side is a flat list of equal blocks, the ✓ side is a composed stack
   of three unlike ones. That is a fourth, and it is structural, so §18's
   floor holds with room to spare.

     1. GLYPH SHAPE     cross vs tick                        ✓
     3. GROUND          bare column vs light panel           ✓
     7. ROW STRUCTURE   boxed rows vs plain rows             ✓ R7w
     8. COMPOSITION     flat list vs banner + brief + list   ✓ R8a
     4. TYPE COLOUR     white vs #5F5F5F                     ✓ R8b — INVERTED
     5. TYPE SIZE       body vs body — IDENTICAL              ✗ SPENT at R8k
     6. ITEM SPACING    32 vs 24                             ✓ R8c — INVERTED

   FIVE of six on the count after R8k spent size again. What is left:
     · 1 and 8 are structural — glyph shape, and the ✓ side's composed stack
       against the ✗ side's flat list
     · 3 and 7 are ground and row structure
     · 4 is colour, white vs #5F5F5F — the ONLY type signal still standing

   §18's floor ("colour is never the sole carrier") holds comfortably on 1, 3,
   7 and 8. The count does not. Read the R8b/R8c/R8k notes below: the two
   signals that survived among type and rhythm both point at the PROBLEM half
   as the louder one, which is backwards for the argument.

   ⚠ THE BADGE GOES CIRCULAR, and that is the FOURTH consumer of
   --radius-avatar, after the avatars it is named for, §3's play control and
   §3's list marks. It runs against R6x the same way those did, on the same
   carve-out: a circle has no corners to square. Radius values page-wide still
   stand at three (0 · 32 · 50%). But two of the four circles are in §3 and
   two are here, in a page that is otherwise strictly rectangular — that is
   the count to watch, not the token list.

   ✅ ONE THING IT BUYS: the ✗ badge stays square and the ✓ badge is now
   round, so signal 1 is carried by the BADGE shape as well as the glyph
   inside it. Two shape differences where there was one.
   -------------------------------------------------------------------------- */

/* THE BANNER. Type on it is white — 5.82:1 on the violet, and the title is
   H3 (26/30/34px) so it clears the large-text floor at every width regardless.
   The violet reads 5.34:1 against the panel behind it. */
.contrast__banner {
  background-color: var(--color-accent-identity-deep);
  border-radius: var(--radius-control);   /* 0 — R6x */
  padding: var(--space-md);
}

.contrast__panel--answer .contrast__banner .contrast__title {
  color: var(--color-text-heading);
}

/* THE BRIEF. --color-surface-light-tile, the middle rung of the light ladder.
   24px padding, not the panel's 32: a nested block padded like its own
   container flattens the nesting (same reasoning as R7w's boxed rows). */
.contrast__brief {
  margin-block-start: var(--space-sm);
  background-color: var(--color-surface-light-tile);
  border-radius: var(--radius-control);
  padding: var(--space-md);
}

/* .contrast__lead carries a top margin for the dark-side stack; inside the
   brief it is the first line and must not. */
.contrast__brief .contrast__lead { margin-block-start: 0; }

/* R8t — the CTA sits at the foot of the answer panel. --gap-paragraph-to-cta
   is the scale's own value for "content -> call to action" (32px) and is what
   the Hero uses, so the interval is the page's, not this section's. */
.contrast__cta { margin-block-start: var(--gap-paragraph-to-cta); }

/* THE FACES — an avatar stack, overlapped and staggered in on entry.

   ⚠ R13c — THE z-index THE OLD NOTE CLAIMED WAS "deliberate" DID NOT EXIST.
   It described the intent and no rule implemented it, so the later circles
   painted over the earlier ones exactly as the note said they must not. With
   grey placeholders that was invisible; with faces in them it is the whole
   problem — the first three showed only their LEFT halves and a centred face
   was cut down the middle. The rule is written now, and z-index applies to a
   flex item without position: relative, which is why nothing else changed.

   ⚠ THE OVERLAP GOES FROM HALF TO A THIRD, and that is a real change to the
   look, not a fix. At -50% each face gave up half of itself; at -33% it gives
   up a third, which is what makes four 40px faces legible. THE SIZE IS
   UNCHANGED at 40px, as directed — the overlap was the other lever and it is
   the one that did not have a stated value attached to it. If the faces are
   still too small, --icon-box-size-sm is shared with §3's list marks and
   needs a §4-scoped override rather than a token edit. */
.contrast__faces {
  margin-block-start: var(--space-md);
  display: flex;
  list-style: none;
  padding: 0;
}

.contrast__face {
  inline-size: var(--icon-box-size-sm);
  block-size: var(--icon-box-size-sm);
  border-radius: var(--radius-avatar);
  background-color: var(--color-surface-light-block);
  /* The ring is the tile showing through, so the circles read as separate
     objects rather than one lumpy shape. */
  box-shadow: 0 0 0 var(--border-emphasis) var(--color-surface-light-tile);
  /* R13b — overflow:hidden is what makes the photograph a circle. The radius
     is on the LI, and without this the square img simply paints over it.
     The --color-surface-light-block fill above is kept deliberately: it is
     what shows while the image loads and if it ever 404s. */
  overflow: hidden;
}

/* R13b — THE IMAGE FILLS THE CIRCLE.

   block-size: 100% is not optional here, for the same reason it was not on
   the Hero cards: the `height="500"` attribute is a presentational hint, and
   a hint wins wherever no CSS sets a height. Without this the img would
   render 40px wide by 500px TALL inside a 40px circle, and object-fit would
   crop a thin vertical sliver of the face.

   object-position: center top — a 500x500 headshot has the face in the upper
   half, and a 40px circle is small enough that a centred crop takes the chin
   and leaves the forehead out. */
.contrast__face img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  object-position: center top;
}

.contrast__face + .contrast__face {
  margin-inline-start: calc(var(--icon-box-size-sm) / -3);
}

/* First on top, descending. Four faces today; the range covers six so a fifth
   or sixth does not silently invert the stack. */
.contrast__face:nth-child(1) { z-index: 6; }
.contrast__face:nth-child(2) { z-index: 5; }
.contrast__face:nth-child(3) { z-index: 4; }
.contrast__face:nth-child(4) { z-index: 3; }
.contrast__face:nth-child(5) { z-index: 2; }
.contrast__face:nth-child(6) { z-index: 1; }

/* --------------------------------------------------------------------------
   R13c — THE STACK ASSEMBLES ITSELF, one face at a time.

   It uses the page's own reveal, not a new animation: .reveal supplies the
   opacity and the transition, .reveal-stagger supplies the 70ms step, and
   initReveal() already observes `.reveal-stagger > *`. All that is overridden
   here is the DIRECTION — the standard reveal rises 24px, and a face sliding
   up into a horizontal stack reads as four unrelated things arriving. Coming
   in from the left is what makes it read as stacking.

   THE DISTANCE IS THE OVERLAP, not --reveal-distance. Each face starts
   exactly where its neighbour's edge is and slides out from under it, so the
   movement describes the stack it is building rather than being a generic
   entrance that happens to be sideways.

   ⚠ BOTH STATES HAVE TO BE DECLARED. base.css's `.js .reveal.is-revealed
   { transform: none }` is (0,3,0) and so is `.js .contrast__face.reveal` —
   equal specificity, and contrast.css is linked LAST, so the resting state
   would have won and the faces would never have arrived. The revealed rule
   below is (0,4,0) and settles it.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .js .contrast__face.reveal {
    transform: translateX(calc(var(--icon-box-size-sm) / -3));
  }

  .js .contrast__face.reveal.is-revealed { transform: none; }
}


/* No ground, no border, no leading padding — the problem reads as page copy.

   THE BLOCK PADDING STAYS and is not decoration: it is what keeps the two
   headings on one line. Without it the left column starts 32px above the
   right and the opposition reads as a mistake rather than a pairing.

   THE TRAILING PADDING IS R7j — the block's measure comes in by 48px.
   Padding rather than max-inline-size on purpose: the block still occupies
   its full grid cell, so the two columns stay half-and-half and only the TEXT
   pulls back. A max-width would have shrunk the cell and left a gap the grid
   did not know about.

     column         568px
     text measure   520px   ← 48 off the trailing edge
     to the panel   96px    (48 padding + 48 gutter)

   This was the lever to reach for rather than widening the gutter again: 48px
   is already the section's side padding, and past it the two halves stop
   reading as a pair. Narrowing the measure buys the same air and costs
   nothing structural. */
.contrast__panel--problem {
  padding-block: var(--space-lg);
}

/* THE 48px TRAILING INSET IS REMOVED AT R8i.

   It was added when this column was a HALF — 504px at a 1280 container — and
   the inset bought air that width could afford. At 5/12 the column is 420px,
   84px down, and the 48px gutter alone now does that job. Keeping the inset
   would set the copy in 372px, which is a caption width rather than a
   measure (§4 puts prose at 720–820).

   The rule is deleted rather than reduced: there is no smaller value worth
   having here, and a 24px inset would be a difference nobody could name. */

/* R8s — THE STICKY ANSWER PANEL IS REMOVED, by direction. It was
   position: sticky at --sticky-offset-title (140px), ≥992 only.

   ✅ IT SIMPLIFIES A CONSTRAINT §15 HAD BEEN CARRYING. Two sections used a
   sticky column — this one and §10 — and §15 permitted the repeat only
   because they are six apart and differentiated (§4's released cleanly at the
   section end and carried no stacking; §10's is accompanied by the stack).
   §10 is now the page's ONLY sticky section, so that justification is retired
   rather than merely satisfied.

   ⚠ §17's MOTION TABLE STILL LISTS "Sticky title column · Contrast (§4)".
   That row now has no implementation. §4's SIGNATURE interaction is
   unaffected — that is the sequential ✓ reveal (.reveal-sequence), which is a
   separate mechanism and stays.

   --sticky-offset-title keeps one consumer, §10. */

.contrast__title {
  font-family: var(--font-family-heading);
  font-size: var(--font-size-h3);
  font-weight: var(--font-weight-heading);
  line-height: var(--line-height-heading);
  letter-spacing: var(--letter-spacing-heading);
  color: var(--color-text-heading);
  text-wrap: balance;
}

.contrast__lead {
  margin-block-start: var(--gap-heading-to-paragraph);
  font-size: var(--font-size-body-lg);
  line-height: var(--line-height-lead);
  color: var(--color-text-heading);
}

/* R8q — small (16) -> body (18 desktop), by direction: slightly bigger, but
   not bigger than the lead above it. --font-size-body is the only step
   between them; the scale has nothing else.

   ⚠ IT HOLDS AT DESKTOP AND COLLAPSES BELOW IT. The lead is --font-size-body-lg
   and the two tokens converge:

                mobile  tablet  desktop
     lead         17      18       20
     body         17      18       18
                  =       =      2px apart

   At ≤991px the lead and the body are the SAME SIZE. Neither carries a weight
   of its own either — both inherit 400 — so below desktop the only thing
   separating a section's lead from its body is COLOUR: white against #B4B4B4
   on the ✗ side, #000000 against #5F5F5F on the ✓ side. That is one signal
   holding a hierarchy that had two.

   It is the same convergence R8d hit on the rows, from the same pair of
   tokens. If the lead needs its distance back below 992, the cheap fix is
   weight on .contrast__lead rather than size — the scale has no step left to
   give it. */
.contrast__body {
  margin-block-start: var(--space-2xs);
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
  color: var(--color-text-body);
}

/* --------------------------------------------------------------------------
   THE ROWS — identical on both sides
   -------------------------------------------------------------------------- */
/* NO SEPARATOR — removed at R7e with the crowd. It ran across BOTH panels
   (one rule, both sides, which is what R7d made it), so taking it off the
   problem panel took it off the answer too. The 40px interval below is what
   marks the break between the intro block and the list now. */
.contrast__rows {
  margin-block-start: var(--space-xl);
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  list-style: none;
  margin-inline: 0;
  padding-inline: 0;
}

.contrast__row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  font-size: var(--font-size-body-lg);
  line-height: var(--line-height-body);
  color: var(--color-text-heading);
}

/* --------------------------------------------------------------------------
   SIGNAL 1 — THE BADGE
   Square, not the reference's circle: R6x squared the page, and a circular
   badge would reintroduce the geometry that was just removed.
   The GLYPH inside is the signal; the ground colour is the seventh signal,
   never the first.
   -------------------------------------------------------------------------- */
.contrast__badge {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  /* R8n — --space-md (24px) -> --icon-box-size-badge (28px). Larger by
     direction, and off the spacing scale onto the box-dimension family where
     it always belonged; see the token for why 28 rather than 32.
     R8p brought the ✓ badge onto the same token, so the pair matches again. */
  inline-size: var(--icon-box-size-badge);
  block-size: var(--icon-box-size-badge);
  border-radius: var(--radius-control);
  margin-block-start: 0.15em;          /* optical, to the cap height */
}

/* R8o — THE GLYPH'S INHERITED MARGIN IS ZEROED. This is the actual cause of
   the off-centre mark, and it was never in the path.

   .marker__glyph carries `margin-block-start: 0.28em` from marker.css. That
   value is correct where the component was written for — a glyph sitting
   INLINE beside text in the Hero, where it aligns to the first line's cap
   height instead of the line box. Inside a flex badge it does something else
   entirely: align-items: center centres the item's MARGIN BOX, so a 0.28em
   top margin displaces the glyph itself by half its own length.

   At the row's 18px font size that is 5.04px of margin, which lands the mark
   2.52px BELOW the circle's centre — in a 28px circle, plainly visible.

   IT AFFECTED BOTH BADGES. The ✓ tick was off by the same 2.52px and had been
   since the badges were built; the gold cross in a larger circle is simply
   where it became obvious.

   The three earlier passes at "centring" measured the PATH — which is centred
   to 0.006px, and remains the reason not to add a translate. The offset was
   one inherited declaration above it. */
.contrast__badge .marker__glyph {
  inline-size: var(--icon-size-pixel-sm);
  block-size: var(--icon-size-pixel-sm);
  shape-rendering: crispEdges;         /* 15px is 1:1 with the glyph grid */
  margin-block-start: 0;               /* R8o — see above. Not optional. */
}

/* R8l — THE ✗ GLYPH IS A ROUNDED CROSS, by direction, in gold.

   ⚠ IT IS NOT A PIXEL GLYPH, and three things follow from that.

   1. §13 SPECIFIES THE SET AS "filled pixel glyphs on a 15-unit grid —
      stepped, axis-aligned, NO CURVES". This one is filled (so it is not the
      stroke-icon violation the menu and close marks are) but it is built from
      64-unit arc joins on a 1024 viewBox. It is a FIFTH icon language on the
      page, after the pixel glyphs, the CTA's SMIL arrow, the unconverted
      menu/close marks and the star. The consolidation pass §13 wants has one
      more item on it.

   2. crispEdges MUST COME OFF. It is correct for a stepped glyph at 15px,
      where it snaps every edge to the pixel grid and keeps the steps equal.
      On arcs it does the opposite: it disables antialiasing on curves and
      returns a visibly jagged cross. Reset to `auto` here only — the ✓ tick
      is still a pixel glyph and still needs it.

   3. THE INERT BACKGROUND PATH WAS DROPPED from the supplied source, and the
      data: URI was INLINED rather than used. §13: a data: URI in
      background-image or <img> cannot resolve currentColor and would render
      black on every ground — which for a glyph that is now the section's only
      gold element would mean an invisible mark.

   ⚠ GOLD RE-OPENS WHAT R7b CLOSED. R7b retired orange from this section
   precisely so the ✗ side "reads as the receding half, which a saturated
   accent would undo". Gold is a saturated accent doing the same job in a
   different hue. Violet is still the section's DOMINANT accent — the banner
   and four ✓ badges against three small marks — so §16's one-dominant-accent
   rule holds and this sits inside the "supporting accents on a handful of
   elements" allowance. But the receding half now carries the page's only
   warm colour outside §5 and §13, and that is the thing to look at.

   MEASURED: --color-accent-emphasis-dim #BE9D4A on the #1A1A1A badge is
   6.72:1, well past the 3:1 a non-text glyph needs. If it reads too dim
   against the black rows behind it, --color-accent-emphasis (sand #F2D98A)
   is 12.49:1 and is the same accent family. */
/* R8m — CIRCULAR BADGE, AND A SIZE CORRECTION. By direction.

   THE PATH IS CENTRED AND NOTHING HERE NUDGES IT. Verified twice, the second
   time by sampling every arc through the W3C endpoint-to-centre conversion
   rather than assuming the caps were symmetric — they are not all the same
   shape, so the assumption was worth checking. True drawn bbox is
   176.46 .. 846.89 on both axes: centre 511.67 against the viewBox's 512, an
   offset of 0.33 units, which at 18px is 0.006px. There is nothing to correct.

   ⚠ SO DO NOT ADD A TRANSLATE HERE. A nudge would introduce the error it was
   meant to remove, and it would be invisible in review — 1px of "optical
   correction" at this size is 57 viewBox units, roughly 170 times the actual
   offset.

   ✅ AND THE OFFSET WAS REAL — see R8o at .contrast__badge .marker__glyph.
   It was an inherited `margin-block-start: 0.28em` from marker.css, worth
   2.52px at this font size, one declaration ABOVE the path. Measuring the
   geometry three times found nothing because the geometry was never wrong.

   The badge's own `margin-block-start: 0.15em` is a different value and stays:
   it is badge-against-TEXT, sitting the mark on the row's cap height.

   WHAT WAS ACTUALLY WRONG WAS THE BOX. A cross whose four arms point at the
   corners of a square sets up a relationship with those corners that reads as
   a rotation error. In a circle there are no corners for it to disagree with.
   That is why the instruction is the fix.

   AND THE MASS WAS LIGHT. The cross fills 65.5% of its viewBox against the ✓
   tick's 73%, so at a shared 15px the two marks did not weigh the same:

     tick   15px box -> 11.00px drawn
     cross  15px box ->  9.83px drawn   12% light
     cross  18px box -> 11.79px drawn   matched

   18px is --icon-size-pixel, §13's DEFAULT size, so this costs no new value.
   It also happens to be the size §13 already says must be anti-aliased rather
   than snapped — which is exactly what a curved glyph needs, and why
   shape-rendering stays `auto` here while the stepped tick keeps crispEdges.

   ⚠ FIFTH CONSUMER OF --radius-avatar, after the avatars it is named for,
   §3's play control, §3's list marks and §4's ✓ badge. Same R6x carve-out
   each time — a circle has no corners to square — and radius values page-wide
   still stand at three. Five circles now, all in §3 and §4.

   ⚠⚠ IT SPENDS HALF OF SIGNAL 1. R8a had made the ✓ badge round and left the
   ✗ badge square, so §14's first signal was carried by BADGE shape as well as
   glyph shape. Both badges are circles now. Signal 1 survives on the glyph
   alone — cross against tick — reinforced by colour, gold against white. The
   count stays at five of six; the margin inside signal 1 does not. */
.contrast__badge--fail { border-radius: var(--radius-avatar); }

/* SCOPED TO .contrast__badge DELIBERATELY. `.contrast__badge .marker__glyph`
   (line ~537) sets the 15px size at specificity (0,2,0); a bare
   .marker__glyph--cross is (0,1,0) and would lose to it, leaving the glyph at
   15px with no error anywhere. Matching the specificity and sitting below it
   in the file is what makes this take. */
.contrast__badge .marker__glyph--cross {
  /* R8o — 18px -> 15px, by direction. 15 is the next legal size down; §13
     fixes the set at 15 · 18 · 30 and there is nothing between. The cross
     draws 9.83px of the 28px circle, 35.1% fill, against the ✓ tick's 45.8%.
     shape-rendering stays `auto`: crispEdges is for STEPPED glyphs, and at
     any size it turns these arcs jagged. */
  inline-size: var(--icon-size-pixel-sm);
  block-size: var(--icon-size-pixel-sm);
  shape-rendering: auto;
  color: var(--color-accent-emphasis-dim);   /* gold #BE9D4A */
}

/* ✗ — neutral. Not orange: R7b retired orange from this section and the ✗
   side reads as the receding half, which a saturated accent would undo. */
.contrast__badge--fail { background-color: var(--color-surface-hover); }
/* The cross carries its own colour (R8l) via currentColor on
   .marker__glyph--cross, so `fill` is left to inherit rather than pinned to
   --color-text-muted as it was. */
.contrast__badge--fail .marker__glyph { fill: currentColor; }

/* --------------------------------------------------------------------------
   R7w — THE ✗ ROWS BECOME #171717 BOXES, by direction.

   ✅ IT BUYS BACK A NON-COLOUR SIGNAL. §14 wants six separating the two
   sides and the count has stood at TWO since R7e/R7f. This is a third, and a
   structural one: the same material, #171717, appears on both sides in
   OPPOSITE roles — the answer is one container holding loose rows, the
   problem is discrete blocks with no container. One side is a room, the other
   is three separate objects, which is close to what the copy actually says.

     1. GLYPH SHAPE   cross vs tick                     ✓
     3. GROUND        bare column vs panel              ✓
     7. ROW STRUCTURE boxed rows vs rows in a box       ✓ NEW at R7w

   ⚠ THE BADGE HAD TO MOVE, and this is the part worth reading. The ✗ badge
   is --color-surface-hover #1A1A1A, chosen when it sat on pure black — a
   1.21:1 lift, faint by design. On a #171717 box it measures 1.03:1 and
   simply is not there. The glyph inside still reads (5.04:1), so §14's
   signal 1 survives either way, but an invisible badge box is a silent
   failure, not a simplification.

   THE FIX SPENT NO NEW VALUE: the badge inverted to --color-bg-base, black on
   #171717 at 1.17:1 — the same faintness in the other direction.

   ⚠ THAT OVERRIDE IS GONE AGAIN AT R8h. The rows themselves are black now, so
   the badge is back on --color-surface-hover, its original value, at 1.21:1.
   The inversion moved up one level rather than being undone.

   PADDING 24 flat, not --gap-card-padding's 24→32. The answer panel is 32;
   a nested block at the same padding as its neighbour's container flattens
   the nesting. 24 keeps the two levels legible.
   -------------------------------------------------------------------------- */
.contrast__panel--problem .contrast__row {
  /* R8h — #171717 -> black. The container took #171717, so the rows had to
     move or vanish; see the ladder in the R8h note above. */
  background-color: var(--color-bg-base);
  border-radius: var(--radius-control);   /* 0 — R6x */
  padding: var(--space-md);

  /* R8k — matched to the ✓ rows opposite, by direction: same size, same
     weight. Colour is NOT matched and stays white, which is what keeps §14's
     signal 4 alive (white vs #5F5F5F).

     ⚠⚠ THIS IS THE CASE R8e REFUSED. That amendment suspended §16's 22px
     weight-300 floor on LIGHT grounds and said in terms: "weight 300 below
     22px stays prohibited on bg-base and on every dark surface." This is
     bg-base, and it is the worst instance on the page — white on pure black
     is the most halation-prone pairing there is. A light stroke on a dark
     field blooms at its edges; subpixel rendering and panel gamma eat inward
     from both sides of an already-thin stem.

     Contrast is not the concern: 21:1 at any weight. RENDERING is.

     ⚠ IT CANNOT BE VERIFIED FROM THE VALUES. Halation is a display effect, so
     unlike every other measurement in this file this one has to be LOOKED AT
     — ideally on a non-retina screen, where stem thinning is worst. If the
     rows shimmer or read as hairlines, the fix is --font-weight-regular on
     this side only: it costs the size match, not the colour signal.

     See design-rules R8k. The 22px floor now has exceptions on both ground
     types in this one section, which means it is no longer a floor. It should
     be replaced with ground-specific scales rather than patched a third
     time. */
  font-size: var(--font-size-body);
  font-weight: var(--font-weight-light);
}

/* ✓ — the Hero CTA's violet. The answer and the action are the same colour,
   which is the argument the page is making. White tick, 5.8:1. */
/* ROUND — R8a. The radius lives HERE, not up with the rest of the R8a block:
   `.contrast__badge` sets --radius-control at the same specificity and is
   declared further down the file, so a --win rule placed above it would lose
   on source order and the badge would silently stay square. */
.contrast__badge--win {
  background-color: var(--color-accent-identity-deep);
  border-radius: var(--radius-avatar);
}
/* R8p — THE TICK IS A STROKED POLYLINE NOW, by direction, and every line
   below exists because of that change.

   ⚠⚠ IT IS A STROKE ICON. §13 prohibits them outright — "the set is filled
   pixel glyphs" — and CLAUDE.md §9 lists stroke-based icons under Prohibited.
   The menu and close marks have violated this since the foundation build and
   the Hero CTA's arrow is a SMIL stroke draw; this is the third standing
   violation, and the first added deliberately to a component that had a
   compliant glyph. The filled pixel set now survives mainly in the buttons.
   §13's consolidation pass has one more item on it.

   COLOUR MOVES FROM fill TO color. The old glyph was filled, so
   `fill: --color-text-heading` coloured it. A stroked path with fill="none"
   ignores fill entirely — setting it would have left a white-stroked mark
   inheriting whatever `color` happened to be, which here is the row's
   #5F5F5F on a violet ground. currentColor is the only thing that works.

   crispEdges STAYS OFF (inherited `auto` from the base rule is not enough —
   .contrast__badge .marker__glyph sets crispEdges explicitly, so this must
   override it). Round caps and joins snapped to the pixel grid come back
   square and ragged.

   MEASURED at 15px: the tick draws 11.25 x 8.44px against the cross's
   9.82 x 9.82 — comparable mass, which is what "similar in size" asked for.
   The 1.5-unit stroke renders at 1.41px. White on #6146ED is 5.82:1. */
.contrast__badge--win .marker__glyph--tick {
  color: var(--color-text-heading);   /* white — 5.82:1 on the violet */
  fill: none;
}

.contrast__badge .marker__glyph--tick {
  inline-size: var(--icon-size-pixel-sm);
  block-size: var(--icon-size-pixel-sm);
  shape-rendering: auto;
}

/* ==========================================================================
   MOBILE — THE TWO SIDES BECOME TWO CARDS. R17g, by direction: "the white
   card will be independent and not within the grey box."

   THE GREY DOES NOT DISAPPEAR — IT MOVES IN. .contrast__box gives up its
   ground and its padding, and .contrast__panel--problem takes both. The
   result is two independent cards stacked, a grey one carrying the ✗ side and
   a white one carrying the ✓ side, separated by .contrast__inner's own 48px
   gap. Above 768 nothing changes: one grey box holding both halves.

   THIS IS WHY IT IS DONE IN CSS AND NOT BY MOVING THE PANEL IN THE MARKUP.
   A parent's background paints behind every descendant, so while the white
   card is inside .contrast__box no margin can get it out from over the grey —
   negative margins would only widen it across the same ground. Taking the
   ground off the parent and giving it to the sibling that still wants it is
   the only move that actually separates them, and it needs no new element.

   ⚠ IT ALSO RE-BOXES THE ✗ SIDE, which R7f deliberately un-boxed: "the
   ground, border and inline padding come off the ✗ side; the answer keeps its
   panel." That decision was about two columns sharing one container at
   desktop, where a second box inside the first read as a box in a box. Below
   768 there are no columns and no shared container — the alternative is the ✗
   side floating on pure black while the ✓ side sits on a card, which reads as
   one of them being unfinished. The ✗ side stays un-boxed at every width R7f
   was written for.

   24px both sides — --space-md, the same value .contrast__box itself now
   takes at this width, so the two cards inset their copy identically.
   ========================================================================== */
/* R17l — 767 -> 991, by direction: landscape phones and tablets take the
   portrait-phone layout, not the desktop one. A landscape phone is 667-932px
   wide, which sits in the 768-991 band, so the old boundary left it on the
   desktop composition it is far too narrow for.

   THIS IS NOW THE SAME LINE EVERY OTHER BELOW-DESKTOP RULE USES — the
   logo-only header, the sticky dock, and the panel paddings all break at 992.
   The page has one boundary between "desktop" and "everything else" again
   rather than two. */
@media (max-width: 991px) {
  .contrast__box {
    padding: 0;
    background-color: transparent;
  }

  .contrast__panel--problem {
    padding: var(--space-md);
    background-color: var(--color-surface-panel);
    border-radius: var(--radius-control);   /* 0 — R6x */
  }

  .contrast__panel--answer { padding: var(--space-md); }
}
