/* ==========================================================================
   R9h — REBUILT TO THE PAGE'S OWN STYLE, by direction: "refer the page style
   from hero to the section above the plate, redo everything after it."

   THE SOURCE IS THE BUILT PAGE (§2–§6), NOT THE DOCUMENTS. What §2–§6
   actually do, read off the stylesheets rather than the plan:

     ground      #000000, always
     container   --color-surface-panel #171717, SQUARE, no border
     accent      violet #6146ED — filled banners, filled circles, 2px rules
                 gold #BE9D4A for the negative side only
     headings    Loos 500, white, section titles CENTRED
     body        Mundial, weight 300, --color-text-body
     rules       1px --color-border-strong
     radius      0 everywhere; 50% only where a circle has no corners
     gradients   NONE. §2–§6 carry no blend shape and no flat fill.

   Every change below follows from that list.
   ========================================================================== */

/* ==========================================================================
   10 — SPEAKERS
   Sticky heading column + scroll-stacking entries · medium · violet ·
   ONE blend shape (G4) · SIGNATURE: the sticky stack.

   NO TIMES. NO RUNNING ORDER. NO NUMBERING. The content document carries an
   explicit instruction — "IMPORTANT: NO TIMELINES" — and it is honoured by
   omission: nothing here is ordinal, nothing is sequenced, and the entries
   carry no index. A "01 / 02 / 03" set would reintroduce a running order
   through the back door.

   THE STACK. Six cards, all pinning on the SAME line at 120px, with z-index
   rising so each covers the previous COMPLETELY (R9z, by direction). It used
   to stagger by 20px per card and leave the previous top edge showing — that
   sliver was the stagger, so full coverage and a stagger cannot coexist.
   R9y had already fixed a worse fault: the offsets cycled every four, which
   sent card five back under card four.

   This is the page's only scroll-linked effect and it is gated to ≥992px.
   Below that it is a static list with standard reveals — the interaction
   needs a tall viewport and a pointer to be worth anything.
   ========================================================================== */

/* R19b — .speakers__inner IS GONE, class and rules. It carried the 4fr / 8fr
   split and the sticky column; the head is centred on top now and the grid
   runs the full container, so the section's wrapper is a plain .container with
   nothing to add. The class came off the markup rather than being left with
   an empty rule.

   --sticky-offset-title and --measure-sticky-column lose their only consumer
   here. Neither is deleted — both are general tokens — but they are no longer
   spent, and CLAUDE.md's unused list says so. */

/* THE HEAD — .section__title--center supplies the centring and the 32px
   title-group gap; this nests one level in so the heading binds to its subtext
   at 16 instead. §5 and §6 are built the same way, for the same reason: one
   flex column gives one interval, and this group needs two. */
.speakers__head {
  display: grid;
  gap: var(--gap-heading-to-paragraph);
}

/* R19c — MATCHED TO §6's SUBTEXT EXACTLY, by direction. .members__sub is one
   declaration and so is this: `margin-inline: auto`, nothing else. Everything
   visible comes from .t-lead — 22px, weight 400, --line-height-lead,
   --letter-spacing-lead, --color-text-body, and a --measure-prose-wide cap.

   ⚠ THE max-inline-size CAME OFF, AND THAT IS THE CHANGE. R19b set
   --measure-prose here, which OVERRODE .t-lead's own --measure-prose-wide and
   made this line narrower than §6's while looking identical in every other
   respect. Matching a section means not re-deciding the things it left to the
   utility. */
.speakers__sub { margin-inline: auto; }

/* The break is gated — see index.html. */
.speakers__break { display: none; }

@media (min-width: 768px) {
  .speakers__break { display: inline; }
}

/* ⚠ R19p — THE margin-block-end IS GONE, AND IT WAS A DOUBLE GAP. This
   eyebrow is a child of .section__title, which is a flex column with
   gap: --gap-title-group. Flex gaps and margins do not collapse, so the
   margin ADDED to the gap and §10's eyebrow sat at 64 where every other
   section's sat at 32 — the difference visible in the supplied screenshot.

   It was correct at R19a, when the eyebrow was outside .section__title in a
   sticky aside; R19b moved it in and left the margin behind. The wrapper needs
   display: flex and nothing else, exactly as .principles__eyebrow,
   .members__eyebrow and .venue__eyebrow have it. */
.speakers__eyebrow { display: flex; }

/* --------------------------------------------------------------------------
   THE GRID — R19a
   auto-fit RATHER THAN A COLUMN COUNT PER BREAKPOINT, and that is deliberate:
   this list sits in a column that is full width below 992 and 8fr above it,
   so a viewport-keyed count would be wrong on one side of that line or the
   other. A 220px floor gives 1 column on a phone, 2 across a tablet, and 3 in
   the 8fr column at 1280 — roughly the card proportion the reference draws,
   which fits 4 only because its canvas is far wider and its cards carry two
   fewer lines of copy.

   auto-FIT, not auto-fill: with six cards a wide viewport would otherwise
   keep empty tracks and leave the row visibly short of its own edge.
   -------------------------------------------------------------------------- */
.speakers__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  /* R19d — THE TWO AXES SPLIT, by direction: "slightly increase the space
     between the 2 rows of speakers."

     Columns keep --gap-grid — it is the page's gutter and every other grid on
     the page uses it, so changing it here would put §10 out of step
     horizontally with §5, §6 and §9.

     Rows take --space-lg (32), one step up the scale from the gutter's 24. A
     row gap and a column gap are doing different work: the gutter separates
     cards that are read side by side, the row gap separates one ROW of four
     from the next, and at an equal value the eight cards read as one field
     rather than as 4 x 2. Both values are on §4's scale; nothing new. */
  column-gap: var(--gap-grid);
  row-gap: var(--space-lg);
  list-style: none;
  margin: 0;
  /* ⚠⚠ THIS LONGHAND MUST FOLLOW THE `margin: 0` SHORTHAND, AND WITHOUT IT
     THERE WAS NO GAP AT ALL — which is what "give enough space before the
     cards show" was reporting.

     layout.css sets `.section__title + *` to --gap-heading-to-content, and
     this list IS that adjacent sibling. But `margin: 0` above resets all four
     sides, speakers.css loads after layout.css, and both selectors are
     (0,1,0) — so the shorthand won on source order and the interval silently
     vanished. Nothing errors; the cards simply sit against the subtext.

     ⚠ IT IS THE SAME FAULT members.css CARRIES A NOTE ABOUT, in the same
     shape, on .members__videos — where it was masked for months by a style=""
     attribute until R17i removed it.

     COUNTED, NOT ESTIMATED: the page has four .section__title groups, and
     THREE of their adjacent siblings declare `margin: 0` and need this
     longhand after it — .principles__grid, .members__videos and this. The
     fourth (.wall__rails) declares no margin shorthand and is unaffected.
     ⚠ .partners__rail was the fifth until R24u deleted that section. Any future list placed after a section head must be
     checked the same way; the failure is silent in both directions.

     The value is --gap-heading-to-content, 32 mobile / 48 desktop: the
     interval §6 uses between its own head and its first row, which is what
     "refer the testimonial section" resolves to. */
  margin-block-start: var(--gap-heading-to-content);
  padding: 0;
}

/* ⚠ EXPLICIT COUNTS, NOT auto-fit — R19b. R19a used repeat(auto-fit,
   minmax(220px, 1fr)) because the list sat in a column that was full width
   below 992 and 8fr above it, so no single viewport-keyed count was right on
   both sides of that line. The grid runs the full container now, so the
   ambiguity is gone and the direction is explicit: 4 x 2.

   auto-fit would ALSO NO LONGER GIVE 4. At 1184px of container it computes
   floor((1184 + 24) / (220 + 24)) = 4 today, but it is derived from a floor
   rather than stated, so the layout would silently become 5 across on a wider
   container and 3 on a narrower one. A 4 x 2 grid that is sometimes 5 x 2 is
   not the thing that was asked for.

   480 IS THE PROJECT'S SMALL BREAKPOINT (§4: <=479 small). Two 208px cards
   fit there; one 342px card is right below it. */
@media (min-width: 480px) {
  .speakers__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ⚠⚠ R23l — 4 COLUMNS -> 3. The section carries SIX cards since Martin Coyne
   and Lucy Harmer were removed by direction, and six in four columns is 4 + 2:
   a ragged final row with two empty cells, which reads as cards missing rather
   than as a deliberate count. Three columns is a clean 3 x 2.

   ⚠ The row gap above was tuned so "one ROW of four" reads as separate from
   the next. It is now a row of three and the reasoning is unchanged — the gap
   is doing the same job at the same value, and both axes are still on §4's
   scale.

   ⚠ 480-991 IS UNTOUCHED and needs no change: 2 columns of six is also 3 x 2.
   (The two-column rule sits at 480, not 768 — 768 in this file is a different
   query.) Below 480 it is one column and six rows. Only the desktop track
   count was ever wrong for this count. */
@media (min-width: 992px) {
  .speakers__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* THE CARD. §16 caps card-based sections at three and the page is already at
   four — this section was one of them before R19a and still is, so the count
   is unchanged. What changed is the REASON. R9z's note said the card chrome
   here "is not decoration: a stacking panel MUST be opaque or the entry
   beneath shows through". There is no stack now, so opacity is no longer
   load-bearing and the panel is doing ordinary card work.

   ⚠ THE HAIRLINE IS NEW, by direction — the reference outlines every card.
   R9h had explicitly matched §4 and §6, which "use the panel grey as the
   container and never outline it", so this is a reversal rather than an
   oversight. --color-border-strong at 0.20 is the page's border value;
   --color-border-default at 0.10 measures 1.03:1 against this panel and would
   not be there at all.

   align-content: start — the cards are content-sized and no longer need to
   match each other's height, which is what the stack required. */
.speakers__card {
  display: grid;
  gap: var(--space-md);
  align-content: start;
  padding: var(--gap-card-padding);
  background-color: var(--color-surface-panel);
  border: var(--border-hairline) solid var(--color-border-strong);
  border-radius: var(--radius-control);
}

/* --------------------------------------------------------------------------
   R19l — THE SEPARATORS LINE UP ACROSS A ROW, by direction: "the seperator
   lines regardless the text line above it will all be aligned across the
   cards."

   THE CAUSE, because the obvious fixes do not address it: Dylan Freer's title
   runs to three lines where the other seven run to two, so his identity block
   is one line taller and every block below it starts lower. Nothing is
   misaligned inside the card — the cards simply have different content
   heights.

   ⚠ A min-block-size ON .speakers__identity WOULD BE A GUESS. It would have to
   be tall enough for the longest title anyone might send, it would leave a
   hole under every shorter one, and it would break silently the first time a
   real title ran to four lines. The alignment has to be derived from the
   content, not asserted against it.

   SO THE CARD BECOMES A SUBGRID. Its three blocks — portrait, identity, talk —
   stop being rows of their own grid and become rows of .speakers__grid's. Row
   tracks are shared across the whole row of cards, so each track is as tall as
   the tallest card's version of it, and the rule above the talk group lands on
   the same line in all four. It stays true when real titles of different
   lengths arrive, which is the part a fixed height cannot promise.

   grid-row: span 3 — three children, three tracks. ADDING A FOURTH BLOCK TO
   THE CARD MEANS CHANGING THIS NUMBER; a subgrid that spans fewer tracks than
   it has children puts the overflow into implicit rows that align with
   nothing.

   ⚠ row-gap IS RE-DECLARED ON THE SUBGRID DELIBERATELY. A subgrid inherits its
   parent's gutters by default, so the card's internal spacing would jump from
   --space-md (24) to the grid's --space-lg (32) row gap, and the deliberate
   difference between "space inside a card" and "space between rows of cards"
   would collapse. A subgrid may set its own gutters; this does.

   @supports, because the fallback is graceful rather than broken: without
   subgrid the cards keep their own grids and the separators sit where the
   content puts them — which is exactly what they did before this rule. */
@supports (grid-template-rows: subgrid) {
  .speakers__card {
    /* ⚠ 4, NOT 3 — R19n added the "View profile" control as a fourth child.
       The R19l note warned this number tracks the child count: a subgrid that
       spans fewer tracks than it has children puts the overflow into implicit
       rows that align with nothing, so the eight controls would have drifted
       out of line the moment one card's copy grew. Changed with the markup,
       not after it. */
    grid-row: span 4;
    grid-template-rows: subgrid;
    align-content: normal;
    row-gap: var(--space-md);
  }
}
/* --------------------------------------------------------------------------
   THE "VIEW PROFILE" CONTROL — R19n, by direction, to the supplied card.

   ⚠⚠ ONE <button>, NOT A LABEL PLUS A CIRCLE. The reference draws two shapes
   and they are one control: splitting them would put two tab stops on one
   action and leave a focusable disc that does the same thing as the words
   beside it. The disc is a <span> inside the button.

   ⚠ IT HAS NO TICKET NOTCH, and design-rules §9 prohibits "an un-notched
   button outside the two recorded --solid exceptions". THE LIST WAS ALREADY
   INCOMPLETE: .play is a circular, un-notched <button> and has been since the
   foundation build — a third exception that the rule never recorded. This is
   the fourth, and it follows .play's precedent exactly, including the 50%
   radius that §7 permits for "shapes with no corners". Both are noted in
   design-rules rather than left to be rediscovered.

   MEASURED, all against --color-surface-panel:
     label  --color-text-body      8.65:1  ✓ (4.5 needed)
     disc   --color-accent-identity-deep  3.08:1  ✓ (3.0 needed, non-text)
     glyph  white on the disc      5.82:1  ✓
   THE REFERENCE'S VIOLET-ON-VIOLET ARROW IS NOT USED: a #865BFF glyph on a
   tinted violet disc measures around 1.4:1, and the arrow is the only thing
   in the control that says which way it goes.

   ⚠ THE GLYPH IS STATIC, NOT THE PAGE'S SMIL ARROW. That one carries <animate>
   children which, per R5t, override both presentation attributes and CSS —
   it redraws itself on load for every visitor including under
   prefers-reduced-motion, and seven controls already do. Eight more would put
   fifteen self-drawing arrows on one page. Same path data, no animation.

   margin-block-start: auto pins the control to the card's floor, so the eight
   line up across a row whatever the copy above them does. It works because the
   card is a subgrid whose last track is as tall as the row's tallest. */
/* R19o — A SEPARATOR ABOVE THE CONTROL, by direction. The same 1px
   --color-border-strong the card already uses above the talk group, so the two
   rules in a card match rather than nearly match. A border-block-start, not an
   element: it cannot appear above the first thing or below the last, and there
   is no decorative <span> to hide from assistive technology. */
.speakers__cta {
  display: flex;
  margin-block-start: auto;
  padding-block-start: var(--space-md);
  border-block-start: var(--border-hairline) solid var(--color-border-strong);
}

/* R19o — THE DISC GOES TO THE CARD'S FAR RIGHT, by direction. The button
   fills the row and space-between pushes the two apart, so the label keeps the
   card's left edge and the disc keeps its right. gap stays as the floor for
   the one case where a long label would otherwise touch the disc. */
.speakers__profile {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  inline-size: 100%;
  gap: var(--space-sm);
  padding: 0;
  background-color: transparent;
  color: var(--color-text-body);
  cursor: pointer;
  transition: color var(--duration-base) var(--ease-standard);
}

.speakers__profile-label {
  font-size: var(--font-size-meta);
  font-weight: var(--font-weight-semibold);
  letter-spacing: var(--letter-spacing-caps-wide);
  text-transform: uppercase;
}

/* ⚠ R19o — §6's PLAY CONTROL'S MATERIAL, by direction: "the circle will be
   the style of the play button in the testimonials section." The same four
   tokens play.css uses — --color-surface-glass-deep, a --color-border-strong
   hairline, --radius-avatar, and --color-accent-identity for the glyph.

   ⚠ NO backdrop-filter, AND THAT IS NOT AN OMISSION. play.css pairs the glass
   with a blur because it sits over a photograph. This disc sits on a flat
   --color-surface-panel card, and silos.css already recorded what happens
   there: "backdrop-filter blurs what is BEHIND the element, and behind it is a
   flat colour — blurring a flat colour returns the same flat colour." Adding
   it would cost a compositing layer per card for no visible difference, and
   would drag play.css's @supports fallback along with it.

   MEASURED against this card rather than assumed, because play.css's own
   numbers are taken over photographs and do not transfer:

     violet glyph on the disc   4.84:1  ✓   (play.css chose the same violet
                                             over stills, at 3.2-4.0:1)
     the disc's fill vs card    1.14:1
     its hairline vs card       1.87:1

   ⚠⚠ SO THE DISC ITSELF BARELY READS ON THIS GROUND. Over §6's photographs a
   0.82 glass is a dark plate against a bright picture; over a #171717 card it
   is near-black on near-black and only the hairline gives it an edge. THE
   CONTROL IS STILL LEGIBLE because the label and the violet glyph carry it —
   but this is a faint glass ring, not the solid violet disc the earlier
   reference drew. Worth seeing before it is signed off.

   THE 50% RADIUS IS THE DOCUMENTED CARVE-OUT: a circle has no corners to
   square. .play, §4's badges and the avatars are the others. */
.speakers__profile-icon {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--icon-box-size-sm);
  block-size: var(--icon-box-size-sm);
  background-color: var(--color-surface-glass-deep);
  border: var(--border-hairline) solid var(--color-border-strong);
  border-radius: var(--radius-avatar);
  color: var(--color-accent-identity);
  transition: border-color var(--duration-base) var(--ease-standard),
              color        var(--duration-base) var(--ease-standard);
}

/* R19w — --icon-size-list (20) -> --icon-size-inline (16), by direction. One
   step down the icon family, which has three members: 16 inline · 20 list ·
   24 feature. Not a picked number, and not a spacing value — a glyph is a box
   dimension and answers to its own scale.

   IT ALSO SUITS THE DISC BETTER. 20px inside a 40px circle leaves 10px of
   ring; 16 leaves 12 and the arrow stops crowding its own edge. .play sets the
   same relationship at a larger size — an 80px disc with a 30px glyph. */
.speakers__profile-glyph {
  inline-size: var(--icon-size-inline);
  block-size: var(--icon-size-inline);
}

/* §18 — a control needs a visible state change of its own, and every hover
   needs a non-hover equivalent. The label is what moves, so the change is
   visible to a keyboard as well as a pointer. */
.speakers__profile:hover,
.speakers__profile:focus-visible { color: var(--color-text-heading); }

/* R19q — THE GLYPH GOES WHITE TOO, by direction. That completes .play's hover
   exactly: border-color and color both move, which is the pair play.css picked
   so the change survives a reader who cannot see colour.

   ⚠ THE COLOUR HAS TO BE SET ON THE ICON, NOT INHERITED FROM THE BUTTON.
   .speakers__profile:hover already turns the BUTTON white, but
   .speakers__profile-icon declares its own `color` for the violet glyph, and a
   declared value beats an inherited one whatever the parent does. Without this
   line the label would brighten and the arrow would stay violet.

   White on the disc measures 20.5:1 — the glyph is decorative either way,
   since the label says "View profile" in words. */
.speakers__profile:hover .speakers__profile-icon,
.speakers__profile:focus-visible .speakers__profile-icon {
  border-color: var(--color-accent-identity);
  color: var(--color-text-heading);
}


/* --------------------------------------------------------------------------
   PORTRAIT
   The cell rotated to 2:3 because it holds a person. Grayscale at rest,
   colour on hover or focus, one at a time. The staircase frame and the
   gradient ground come from the .portrait component unchanged — Speakers is
   the one section permitted a gradient portrait ground (§11a).
   -------------------------------------------------------------------------- */
/* R10a — THE PORTRAIT IS CAPPED, and that is what makes the card smaller.
   The entry's height is the portrait's height: the text column is shorter
   than it at every card, so nothing else in here sets the size.

   160px wide at 2/3 gives 240px tall, against roughly 265 x 400 before. The
   cap is --space-6xl rather than a picked number.

   ⚠ THE CAP IS ALSO WHAT KEEPS THE STACK COVERING. R9z's note: card N hides
   card N-1 only if it is at least as tall, and that holds only while every
   card is portrait-led. A fixed portrait width makes every card exactly the
   same height, which turns that from a hope into a guarantee — until a talk
   title or description grows taller than 240px.
 */
.speakers__portrait {
  --portrait-ratio: 2 / 3;
  --portrait-step: var(--space-md);
  --portrait-ground: var(--gradient-portrait);   /* R10e — was G4 */
  /* R10h — inline-size, NOT max-inline-size, and this is what R10g broke.

     .portrait__frame has an aspect-ratio and no width of its own; its only
     child is position: absolute and therefore out of flow. So the frame's
     max-content width is ZERO. Under the old 4fr column that never mattered —
     the column was definite and the frame filled it — but R10g made the
     column `auto`, which asks the content how wide it is. It answered zero,
     the column collapsed, and the portrait disappeared entirely.

     A max- is a ceiling on a width that something else has to supply. With an
     auto column nothing supplies it. A definite inline-size does, and the
     aspect-ratio then gives the height as before.

     max-inline-size: 100% stays as the guard for a container narrower than
     160px. */
  /* R19a — 100%, NOT THE 160px CAP. R10a capped it so that every card was
     exactly the portrait's height and the stack was guaranteed to cover;
     there is no stack, so the cap has no job and a 160px picture in a 236px
     card reads as an inset thumbnail rather than as the card's subject.

     ⚠ R10h's REASONING STILL BINDS: this must be a definite inline-size, not
     a max-. .portrait__frame has an aspect-ratio and its only child is
     absolutely positioned, so its max-content width is ZERO — a ceiling on a
     width nothing supplies collapses the frame and the portrait disappears.
     100% resolves against the grid track, which is definite. */
  inline-size: 100%;
}

/* ⚠⚠ R19t — COLOUR AT REST, by direction: "do not make the image
   monochrome." This is an AMENDMENT TO design-rules §11a, not an oversight,
   and it is recorded there as well as here.

   THE RULE IT SUSPENDS: "a portrait in colour at rest" is prohibited
   page-wide; portraits are grayscale and take colour only on hover or focus,
   one at a time, because colour is a treatment rather than a meaning.

   WHAT IT COSTS: the hover no longer says anything. .portrait:focus-within
   flipped grayscale(0) on, and with the base filter gone that transition has
   nothing left to do — §10's portraits now have no interaction state of their
   own. The section's only remaining hover is the "View profile" control.

   ⚠ IT IS SCOPED TO §10 DELIBERATELY. .portrait__img is (0,1,0) and this is
   (0,2,0), so the component keeps grayscale everywhere else it is used and
   §11a still holds for the rest of the page. Do not lift this to the
   component.

   filter: none rather than grayscale(0) — they render identically, but `none`
   says the element has no filter rather than one set to do nothing, and it
   drops the filter layer instead of keeping an inert one. */
/* ⚠ R20a TRIED 3/4 + object-fit: contain AND WAS UNDONE AT R20b. Kept as a
   note because the measurement behind it still stands and will come up again:
   the files are exactly 2:3 and this image is anchored `center bottom`, so
   under `cover` ANY frame shorter than 2:3 crops off the TOP. Headroom above
   each person's hair, measured as the topmost opaque row:

     Mark Ovens 1.8% · Marilyn Zakhour 3.6% · Martin Coyne 5.9% ·
     Nitesh Sughnani 8.2% · Mike Bond 9.9% · Dylan Freer 10.7% ·
     Tarun Gangwar 10.7% · Lucy Harmer 10.9%

   Even a 5% reduction cuts into Mark Ovens. If the frame is ever shortened,
   either object-fit becomes contain or his file is re-exported with headroom.

   filter: none is R19t's — colour at rest, scoped to §10, unaffected. */
.speakers__portrait .portrait__img { filter: none; }

/* --------------------------------------------------------------------------
   THE TEXT COLUMN — R10i. The talk at the top, identity at the bottom.

   The order was reversed by direction: the talk group now aligns with the top
   of the portrait and the identity with its bottom edge. Done in the MARKUP,
   not with `order` — reading order and paint order stay the same that way, and
   "talk title, description, then who is giving it" reads correctly on its own.

   space-between rather than a fixed gap: the two blocks are pinned to the
   card's own top and floor, so the interval between them is whatever the
   card's height leaves. That is what makes the pair read as two ends of one
   card rather than as a stack of four lines. It only works because the body
   now stretches to the portrait's height — see R10i above.
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   THE CARD'S PARTS — R19g
   .speakers__body is gone: the card's four blocks (head, portrait, identity,
   talk) are direct grid children now, so the wrapper had nothing left to do.
   Its rule went with it rather than being left as an orphan.
   -------------------------------------------------------------------------- */

/* R19i — THE CARD HEAD IS REMOVED, by direction. The "SPEAKER" badge and the
   01-08 ordinal both went with it, and so did .speakers__card-head,
   .speakers__badge, its three eyebrow overrides and .speakers__ordinal —
   deleted rather than left as orphaned rules.

   ✅ IT CLOSES THE §10 DIVERGENCE R19g HAD TO OPEN. The content document's
   entry carries one instruction in capitals, "IMPORTANT: NO TIMELINES", and
   numbering the cards 01-08 stated a running order against it. That conflict
   is gone rather than merely recorded. "Speaker" as a per-card label is gone
   with it; what remains outside the document is the section eyebrow
   ("Speakers") and the card's "Talk title" label.

   ⚠ --color-border-strong AS A TYPE COLOUR GOES TOO, and that is worth
   keeping out. The ordinal was the only element on the page using a border
   token for type — at 1.87:1, defensible only because it was aria-hidden and
   carried no information. It is not a precedent to reach for again. */

/* R19e — THE IDENTITY STACK. A zero-gap column, which is the Hero plate's own
   construction (R15m): one more <p> IS one more line, so a title that needs
   two lines takes two <p class="speakers__org"> and needs no new rule. Dylan
   Freer's does.

   The gap is zero by declaring none — reset.css already zeroes <p> margins, so
   the lines close up on their own. The element is declared a column anyway
   rather than left as a bare <div>, because the behaviour above depends on it
   being one and a later `display` on this block would break the template
   silently. */
.speakers__identity {
  display: flex;
  flex-direction: column;
}

/* --------------------------------------------------------------------------
   THE NAME — R19j, by direction: "the names will be slightly smaller, in the
   'Loos' font." --font-size-h3 -> --font-size-h4, and the heading face.

   ⚠⚠ THE FACE COULD NOT BE CHANGED BY ITSELF — THE ELEMENT HAD TO CHANGE WITH
   IT, and this is the whole of why the markup moved. CLAUDE.md §3 and §9:
   "Loos appears on headings and nowhere else… a <p> styled at heading size
   stays Mundial — a display style class is not a heading level, so styling a
   paragraph large never licenses the heading face." The name was a <p>.
   Setting --font-family-heading on it would have been a silent breach of a
   prohibition, not a styling choice.

   SO THE CARD'S HEADING IS THE PERSON NOW. The name is an <h3> and the talk
   title is an <h4> beneath it — h2 (section) -> h3 (speaker) -> h4 (their
   talk), no level skipped, and an outline that reads correctly on its own:
   "Speakers / Dylan Freer / Talk title". Both are real headings, so both may
   legally take Loos and NOTHING IS AMENDED. Making the name a <p> with Loos,
   or leaving it a <p> and re-pointing the <h3>, would each have cost a rule.

   ⚠ WEIGHT 600 HAD TO GO, AND IT IS NOT COSMETIC. The name carried
   --font-weight-semibold, copied from .members__name where the face is
   Mundial. Loos publishes 400 and 700 only: asking it for 600 makes the
   browser SYNTHESISE a weight, which is a smeared fake bold. --font-weight-
   heading is 500 for exactly this reason — CSS weight matching descends to
   400 before it looks above, so 500 renders the real published face.

   line-height, letter-spacing and colour are unchanged from R19h.
   -------------------------------------------------------------------------- */
.speakers__name {
  /* R19k — the one interval that opens, by direction. A margin on the NAME,
     not a gap on .speakers__identity: a gap would push the role lines apart
     from each other too, and that stack is deliberately zero-gap so a title
     that needs two lines reads as one title. Only the name-to-role step
     changes. --space-xs (12) against a 26px name is about a third of its
     size, which is the proportion the reference shows. */
  margin-block-end: var(--space-xs);
  font-family: var(--font-family-heading);
  font-size: var(--font-size-h4);
  font-weight: var(--font-weight-heading);
  line-height: var(--line-height-heading);
  letter-spacing: var(--letter-spacing-heading);
  color: var(--color-text-heading);
}

/* ⚠ THE NAME IS NOT THE HEADING. .speakers__talk keeps the <h3>, as built —
   giving the name one too would put two headings in every card, and promoting
   it would leave the talk title below its own parent level. The name is the
   card's visual subject and the talk is its semantic one; if that should be
   the other way round it is a document-outline decision, not a CSS one. */
.speakers__org {
  font-size: var(--font-size-small);
  color: var(--color-text-body);             /* 8.65:1 on the card */
}

/* THE TALK GROUP — a rule and a label above the title. The rule is the page's
   separator value; a border-block-start cannot enclose anything, which keeps
   §16's ban on carding narrative sections irrelevant here and the card's own
   chrome down to one box. */
.speakers__talk-group {
  padding-block-start: var(--space-md);
  border-block-start: var(--border-hairline) solid var(--color-border-strong);
}

/* ✅ R25e — THE BADGE IS THE EYEBROW COMPONENT NOW, by direction: "the new
   badges will have the step structure the eyebrow background as earlier
   itself."

   The bespoke chip is gone — an inline-block with a RECTANGULAR fill, which
   is the one shape §10a's refusal names. The markup now carries
   `.eyebrow.eyebrow--emphasis.eyebrow--filled` around a
   `.t-eyebrow.eyebrow__label`, the same instance the page's other nine
   eyebrows use, so the twelve-point stepped silhouette, the padding, the
   uppercase, the weight and the sand label all come from the component and
   this file stops restating them.

   WHAT THIS FILE STILL OWNS: the <p> wrapper's spacing, and ONE override.

   ⚠⚠ THE FILL IS BLACK HERE, NOT THE COMPONENT'S --color-surface-hover, AND
   THAT IS THIS SECTION'S OWN RECORDED REMEDY, kept from R19m. #1A1A1A against
   this card's --color-surface-panel #171717 is 1.03:1 — the badge would
   simply not be there. --color-bg-base is 1.17:1: still only a faint step,
   THE DARK LADDER HAVING NOTHING DARKER THAN BLACK, but it is the fill the
   chip already used, so nothing on screen got quieter in the swap.
   (.ph--on-panel swaps to black for exactly this reason.)

   THE LABEL IS FINE EITHER WAY: sand on black is 15.07:1. And sand is not a
   preference — --color-accent-identity on this card is 4.23:1, under the 4.5
   that 14px text needs, so the reference's violet would be an AA failure at
   the smallest type in the card.

   If the badge should read here the way it does in §7, the CARD's ground has
   to move, not the badge's — a section-wide decision, flagged not taken.

   ⚠ TRACKING WENT +0.08em -> +0.06em, .t-eyebrow's own --letter-spacing-caps.
   R19m took the wider step for the reason §7's fact labels take it; the badge
   is a component instance now and the component sets it. Putting it back is a
   scoped override HERE, never an edit to .t-eyebrow. */
.speakers__talk-label { margin-block-end: var(--space-2xs); }

/* (0,2,0) — it has to beat .eyebrow--filled::before's (0,1,0) wherever the
   two files land in the cascade, rather than depend on link order. */
.speakers__talk-label .eyebrow--filled::before {
  background-color: var(--color-bg-base);
}

/* R19r — .speakers__talk IS GONE, rule and element. The <h4> carried
   [TALK TITLE REQUIRED] and was removed by direction; its rule went with it
   rather than being left orphaned.

   ✅ R21f — BOTH OF THE WARNINGS THAT STOOD HERE ARE CLOSED, and by the same
   change. Real talk titles landed for six of the eight speakers.

   THE SLOT IS AN <h4> AGAIN. R19r's note asked for exactly this: "if a real
   talk title returns to this slot it should return as an <h4>, not as a
   styled <p>." The outline is h2 (section) -> h3 (speaker) -> h4 (talk), no
   level skipped.

   ⚠ AND THE RENDERING DID NOT MOVE. reset.css line 23 sets font-size and
   font-weight to `inherit` on h1-h6 and never sets font-family, so every
   visual property below is still doing the work and the element swap is
   invisible. IN PARTICULAR THIS DOES NOT TAKE LOOS: the heading face follows
   --font-family-heading, which is set by .t-h* and by .speakers__name, not by
   the tag. A Mundial <h4> is correct here — §3's rule is that Loos appears
   ONLY on headings, not that every heading is Loos.

   THE LABEL IS NO LONGER ABOVE THE WRONG FIELD. It reads "TALK TITLE" and the
   line under it is now a talk title.
   -------------------------------------------------------------------------- */

/* R19r — white and one step up, by direction. --font-size-body ->
   --font-size-lead, --color-text-body -> --color-text-heading. It is the only
   line left under the label, so it carries the slot on its own now.

   ✅ WEIGHT 300 -> 400, WHICH FIXES A STANDING VIOLATION rather than adding
   one. §3 reserves --font-weight-light for "body at 22px+ · never in
   text-muted"; this line was 300 at --font-size-body, which is 17px on a
   phone. R9h set it to match §2-§6's body weight without checking the floor.
   At 400 it is legal at every step of the scale. */
.speakers__note {
  margin-block-start: var(--space-xs);
  font-size: var(--font-size-lead);
  font-weight: var(--font-weight-regular);
  line-height: var(--line-height-lead);
  color: var(--color-text-heading);
}

/* R10d — THE SUBJECT IS PUSHED DOWN so the staircase does not bite the head.

   Nothing is cutting the photograph: object-fit: cover shows its full height
   here, because the image is landscape (500 x 466) inside a 2/3 frame, so the
   crop takes the sides and not the top. What clips the head is the FRAME —
   §11a's two-step staircase cut into all four corners, and the top corners
   step inward exactly where a head sits.

   THE OFFSET IS THE FRAME'S OWN STEP, not a percentage. --portrait-step is
   the depth of one stair, which is the amount the corner actually bites; one
   step down clears it. A percentage would drift out of agreement with the
   staircase the moment the step or the ratio changed.

   The bottom overflows by the same amount and is clipped by the frame's own
   clip-path, which already clips the whole subtree. */
.speakers__portrait .portrait__img { inset-block-start: var(--portrait-step); }

/* An entry with no portrait yet still holds its shape: the frame renders as
   ground and grain with no subject. A partial set reads as a set. */
.speakers__portrait .portrait__img[hidden] { display: none; }
