/* ============================================================
   KDB system — the component layer
   ------------------------------------------------------------
   What a page is allowed to be built from. Consumes Tier 3
   tokens and nothing else: no literal here is a decision, every
   one is a reference.

   This is the file that makes "zero exceptions" achievable
   rather than aspirational. A proof page loads tokens.css,
   type.css and this, and writes no CSS of its own. If it cannot
   be built from what is here, the system has a hole and the fix
   lands in this file — never in the page.

   Pairs with js/audit.js, which fails the page if the rule is
   broken anyway.
   ============================================================ */

/* ---------- shell -------------------------------------------- */

.k-page {
  max-width: var(--kdb-page-max);
  margin: 0 auto;
  padding: 0 var(--kdb-page-margin);
}

/* Clearance for the floating nav. Declared AFTER .k-page because
   that rule uses the `padding` shorthand, which resets
   padding-block-start — the same specificity earlier in the file
   was silently overwritten and the headline still sat under the
   mark. Opt-in, so the home page's full-bleed hero is untouched. */
.k-page--offset { padding-block-start: var(--kdb-nav-height); }

/* Sixteen columns, D5. The grid is declared once; every layout
   below is a placement on it rather than a new grid, which is
   what stops a page inventing a seventeenth column. */
.k-grid {
  display: grid;
  grid-template-columns: repeat(var(--kdb-grid-columns), minmax(0, 1fr));
  gap: var(--kdb-page-gutter);
}

/* D5d, the asymmetry law. These four are the sanctioned splits.
   8/8 is deliberately absent — a layout with no lead has no
   opinion, and it is banned rather than discouraged. */
/* WIDTH ONLY. These set the end of the span and never the start,
   so they compose with .k-body below instead of fighting it.

   The first version wrote `grid-column: span 10`, which sets the
   START to auto as well — and .k-body's `2 / -1` came later in the
   file and won outright. Every "major" column silently became the
   full width from column 2, so the sidebar beside it could not fit
   on the row and wrapped underneath at column 1. That is what the
   ragged left edge on the identity page was: not a placement
   choice, a shorthand collision. */
.k-span-major   { grid-column-end: span 10; }  /* 10/6 default — the 5:3 plate */
.k-span-minor   { grid-column-end: span 6; }
.k-span-lead    { grid-column-end: span 11; }  /* 11/5 emphatic */
.k-span-trail   { grid-column-end: span 5; }
.k-span-third   { grid-column-end: span 5; }   /* three-up, from column 2 */
.k-span-quarter { grid-column-end: span 4; }
.k-span-full    { grid-column: 1 / -1; }

/* PROSE IS SEVEN COLUMNS, and that is not a coincidence: at the
   1440 container seven columns measure 563px against the 576px
   body measure from D1. The grid enforces the measure.

   This exists because a 72-character cap inside a ten-column
   field stops 235px short of its own column edge, and that gap is
   what reads as text "wrapping early" — the cap fighting the grid
   rather than being carried by it. Display type may take ten or
   eleven columns. Running prose takes seven. */
.k-prose > * + * { margin-block-start: var(--kdb-space-snug); }

/* The annotation margin. Column 1 of sixteen — the home for a
   year, a unit, a source, a scoreline. It exists because the
   position above a headline is reserved for nothing. */
/* START AT COLUMN 1 — and step aside only where an annotation is
   actually present.

   Reserving column 1 on every row cost 83px of indent at 1600px on
   rows that had nothing in the margin, which with the 1440 cap and
   the page margin put 227px — 14% of the viewport — in front of
   every headline. Once the category words and the roman numerals
   were removed the margin was empty almost everywhere, so the page
   was paying rent on a column it had stopped using.

   You pay for the annotation margin on the rows that use it. */
.k-body { grid-column-start: 1; }
.k-grid:has(> .k-annotation) .k-body { grid-column-start: 2; }
.k-body:not([class*="k-span"]) { grid-column-end: -1; }

/* Same rule for prose, which is 7 columns wherever it starts. */
.k-prose { grid-column: 1 / span 7; }
.k-grid:has(> .k-annotation) .k-prose { grid-column: 2 / span 7; }
.k-annotation {
  grid-column: var(--kdb-annotation-col);
  font-size: var(--kdb-annotation-size);
  letter-spacing: var(--kdb-annotation-track);
  text-transform: uppercase;
  color: var(--kdb-annotation-ink);
  align-self: start;
  line-height: var(--kdb-leading-flat);
}

/* Display type may overhang one column left of its field. One of
   exactly two sanctioned breaks in D5d; the other is media
   bleeding right, below. */
.k-overhang { margin-left: calc(-1 * (100% / var(--kdb-grid-columns))); }
.k-bleed-right { margin-right: calc(-1 * var(--kdb-page-margin)); }

/* ---------- rhythm ------------------------------------------- */

/* Four gaps, D5e. Nothing between them, and no page adds a fifth. */
.k-section { margin-block-start: var(--kdb-section-gap); }
.k-group   { margin-block-start: var(--kdb-group-gap); }
.k-block   { margin-block-start: var(--kdb-block-gap); }
.k-chapter { margin-block-start: var(--kdb-chapter-gap); }
.k-section:first-child,
.k-group:first-child,
.k-block:first-child { margin-block-start: 0; }

.k-stack > * + * { margin-block-start: var(--kdb-space-snug); }
.k-stack-tight > * + * { margin-block-start: var(--kdb-space-tight); }

/* ---------- rules -------------------------------------------- */

/* The D6 ladder, CSS side. Non-scaling is an SVG guarantee, so
   these are for document furniture only — never for a drawing. */
.k-rule-hair   { border: 0; border-block-start: var(--kdb-rule-hair) solid var(--kdb-rule-hair-ink); }
.k-rule-divide { border: 0; border-block-start: var(--kdb-rule-divide) solid var(--kdb-rule-divide-ink); }
.k-rule-strong { border: 0; border-block-start: var(--kdb-rule-strong) solid var(--kdb-rule-strong-ink); }

/* ---------- drawn surfaces ----------------------------------- */

/* A plot is a drawing. It gets a frame only where the drawing
   would otherwise float, and never a background fill — D9 puts
   the marks on the page's own ground. */
.k-plot { display: block; width: 100%; height: auto; }
/* A drawing that has an intrinsic size — the mark, a badge — is
   capped rather than stretched to its column. */
.k-plot-mark { max-width: var(--kdb-mark-max); }
.k-plot-figure { margin: 0; }
.k-plot-figure + .k-plot-figure { margin-block-start: var(--kdb-plot-gap); }
/* Two drawings in one figure are still two drawings and take the
   same gap. They do not share a frame — see identity.js, where a
   nodule row inside a bar chart's viewBox collided with the bars. */
.k-plot + .k-plot { margin-block-start: var(--kdb-plot-gap); }

/* Captions sit beneath the thing they describe. Explicitly not an
   eyebrow — one of the three cases the ban does not touch. */
.k-caption {
  margin-block-start: var(--kdb-media-caption-gap);
  font-size: var(--kdb-media-caption-size);
  line-height: var(--kdb-leading-tight);
  color: var(--kdb-media-caption-ink);
  max-width: var(--kdb-measure-caption);
}

/* ---------- media -------------------------------------------- */

/* SQUARE IS THE DEFAULT CROP. A photograph is a rectangle unless
   something earns otherwise.

   D7 said containers are how photography is cropped and therefore
   "everywhere by definition", which licensed sculpting every
   frame on the page. Reviewed and reversed: a sculpted crop that
   appears on every image is not a signature, it is wallpaper —
   the same failure as a stadium drawing behind every beat. The
   curve has to stay an event. */
.k-media { margin: 0; }
.k-media img {
  display: block;
  width: 100%;
  height: auto;
}

/* The earned crop. One per beat, and only where the shape is
   doing something the rectangle cannot — a portrait lifted out of
   a run of rectangles, a single subject framed. The path comes
   from KDB_CONTAINERS via js/shapes.js, so a page never hand-rolls
   a corner radius. */
.k-media-shaped img { clip-path: var(--k-shape, none); }

/* ---------- photo layouts ------------------------------------- */

/* Five, and no more without a decision. Each one exists because a
   beat needs it, not because it is available.

   Every one below except BLEED is a modifier, not a base — pair it
   with .k-media (width: 100%; height: auto) on the same element.
   Without it a plate has no width rule of its own, a grid-item
   parent defaults to shrinking no further than the image's
   intrinsic size, and the crop's own aspect-ratio never gets a
   chance to run: first real use (the diptych hero) rendered a
   1920px-intrinsic image at 1920px wide on a 1280px viewport. */

/* FULL BLEED — edge to edge, past the page margin. The only place
   a photograph escapes the grid, and it costs the whole viewport,
   so a page gets one. */
.k-photo-bleed {
  margin-inline: calc(-1 * var(--kdb-page-margin));
  width: calc(100% + 2 * var(--kdb-page-margin));
  max-width: none;
}
.k-photo-bleed img { width: 100%; }

/* PLATE — the 5:3 of the mark, held inside the grid. The default
   for a beat opener, because it carries the mark's proportion
   without needing the mark. */
.k-photo-plate img { aspect-ratio: 5 / 3; object-fit: cover; }

/* PORTRAIT — 3:4, one figure. */
.k-photo-portrait img { aspect-ratio: 3 / 4; object-fit: cover; }

/* BANNER — 16:5, a horizontal rail. Montage stills, a pinned run. */
.k-photo-banner img { aspect-ratio: 16 / 5; object-fit: cover; }

/* INSET — media bleeding right off the container while the type
   holds the left. One of exactly two sanctioned grid breaks in
   D5d, and the only one that involves a photograph. */
.k-photo-inset {
  margin-inline-end: calc(-1 * var(--kdb-page-margin));
}
.k-photo-inset img { aspect-ratio: 4 / 3; object-fit: cover; }

/* ---------- cards -------------------------------------------- */

/* A CARD IN THIS SYSTEM IS NOT A BOX.

   The reflex is a rounded rectangle with a fill, a border and a
   shadow. All three are already banned by rules locked long
   before anyone needed a card: there is no shadow anywhere; D6
   says space separates by default and a rule has to be argued
   for; D14 says a photograph is a rectangle unless something
   earns otherwise; and a fill would put a surface behind content,
   which is the habit D4 exists to break.

   So a card is a photograph, a name and a figure, held together
   by proximity and separated from its neighbours by space. What
   makes it read as one object is the gap around it, not a line
   drawn about it.

   The exception is a card that is CLICKABLE AS A WHOLE. Then the
   reader needs to know where the target begins and ends, and a
   hairline is argued for — that is the argument D6 asks for. */

.k-cards {
  display: grid;
  grid-template-columns: repeat(var(--kdb-grid-columns), minmax(0, 1fr));
  gap: var(--kdb-card-gap) var(--kdb-page-gutter);
  grid-column: 1 / -1;
}
.k-card { grid-column: span 4; margin: 0; }          /* four-up, lands exactly */
.k-cards--three .k-card { grid-column: span 5; }     /* from column 2 */
.k-cards--two .k-card { grid-column: span 8; }

.k-card img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
}
.k-card__body { margin-block-start: var(--kdb-card-label-gap); }

/* A card whose subject is a number, not a photograph. The figure
   has to be a BLOCK: written as an inline <span> the three totals
   in beat 04 each opened their own line box inside their own
   card, so a row that is meant to read as one measurement landed
   on three different baselines — visible the moment the section
   was screenshotted, invisible to every check before that. */
.k-card__figure { display: block; }
.k-card__name {
  font-size: var(--kdb-size-small);
  color: var(--kdb-text-primary);
}
.k-card__meta {
  font-size: var(--kdb-size-caption);
  color: var(--kdb-card-meta-ink);
  font-variant-numeric: lining-nums tabular-nums;
}

/* The clickable card. A hairline, because the reader has to know
   where the target is — and nothing else: no fill, no lift, no
   shadow, and no scale on hover. The hover is a DRAW, which for a
   still object means the rule arriving at full strength. */
a.k-card {
  text-decoration: none;
  display: block;
  padding: var(--kdb-space-tight);
  margin: calc(-1 * var(--kdb-space-tight));
  border: var(--kdb-rule-hair) solid transparent;
  transition: border-color var(--kdb-motion-draw) var(--kdb-motion-curve);
}
a.k-card:hover { border-color: var(--kdb-card-rule); }
a.k-card:hover .k-card__name { text-decoration: underline;
  text-decoration-color: var(--kdb-link-underline); text-underline-offset: 0.18em; }

@media (max-width: 919px) {
  .k-card,
  .k-cards--three .k-card { grid-column: span 4; }   /* two-up on eight */
  .k-cards--two .k-card { grid-column: 1 / -1; }
}
@media (max-width: 599px) {
  .k-card,
  .k-cards--three .k-card { grid-column: span 2; }   /* two-up on four */
}

/* ---------- tag ---------------------------------------------- */

/* The one container permitted to hold type alone. A club
   identifier, never a legend swatch — legends are banned at D9d,
   and a row of these beside a chart is a legend whatever it is
   called. */
.k-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--kdb-space-hair);
  padding: var(--kdb-tag-pad-y) var(--kdb-tag-pad-x);
  font-size: var(--kdb-tag-size);
  letter-spacing: var(--kdb-tag-track);
  text-transform: uppercase;
  line-height: var(--kdb-leading-flat);
  border: var(--kdb-rule-hair) solid var(--kdb-rule-divide-ink);
  color: var(--kdb-text-secondary);
}

/* A parallel series leaves the ordered run rather than ranking
   inside it — D9b. In a table that is a second <tbody> with a gap
   above it, which is the structural version of the gap the
   drawings put beside Belgium. */
.t-table tbody + tbody tr:first-child td {
  padding-block-start: var(--kdb-space-base);
}

/* ---------- links -------------------------------------------- */

/* A link is Ice at the one step that is safe for text, with an
   underline that is present but not shouting. Ice earns this
   because a link IS the accent doing work — it is the D4 mark
   mode in prose, and the subtractive test passes: remove the
   colour and you lose the affordance, not the styling. */
a {
  color: var(--kdb-link-ink);
  text-decoration-line: underline;
  text-decoration-color: var(--kdb-link-underline);
  text-underline-offset: 0.18em;
  text-decoration-thickness: from-font;
}
a:hover,
a:focus-visible {
  color: var(--kdb-link-hover);
  text-decoration-color: currentColor;
}

/* ---------- controls ----------------------------------------- */

/* Added in Phase 8 for the greyscale test, which D9h makes an
   acceptance criterion rather than a claim — so the page needs a
   real control to run it with. Deliberately the only interactive
   component in the system so far: nothing else on these two pages
   needed one, and a button set invented before it is needed is a
   token set nobody validates. */
.k-control {
  font: inherit;
  font-size: var(--kdb-size-label);
  letter-spacing: var(--kdb-track-caps);
  text-transform: uppercase;
  padding: var(--kdb-space-tight) var(--kdb-space-snug);
  color: var(--kdb-text-primary);
  background: none;
  border: var(--kdb-rule-divide) solid var(--kdb-rule-strong-ink);
  cursor: pointer;
}
.k-control[aria-pressed="true"] {
  background: var(--kdb-rule-strong-ink);
  color: var(--kdb-text-inverse);
}

/* The subject of the greyscale test. The filter goes on a wrapper
   rather than into the marks, so one drawing answers both states
   and there is no second version to drift. */
.k-desaturable {
  transition: filter var(--kdb-motion-draw) var(--kdb-motion-curve);
}
.k-desaturable[data-grey="on"] { filter: grayscale(1); }

/* ---------- motion ------------------------------------------- */

/* A reveal is a draw — D9g. Never a bounce, never a fade up from
   below, never a spring. Reduced motion is handled in tokens.css
   by zeroing the durations, so the drawing still arrives; it just
   arrives complete on the first frame. */
.k-draw-in {
  animation: k-draw var(--kdb-motion-draw) var(--kdb-motion-curve) both;
}
@keyframes k-draw {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ---------- states ------------------------------------------- */

/* D9g's four states, as the page's side of them. The drawing's
   own side lives in draw.js. */
.k-empty {
  color: var(--kdb-text-muted);
  font-size: var(--kdb-size-caption);
}

/* ============================================================
   RESPONSIVE — D16
   ------------------------------------------------------------
   The breakpoints are measured, not chosen. Nobody picked 920.

   Prose is seven columns (see .k-prose). Solve for the viewport
   at which seven columns fall below a usable 45-character
   measure of about 360px, with the margin clamping at 4vw:

     1440 -> 568px    1024 -> 401px    960 -> 375px
      920 -> 359px  <- the field stops being readable here
      880 -> 343px

   So SIXTEEN COLUMNS HOLD DOWN TO 920px and not below. Under
   that the count halves rather than the measure shrinking,
   because a narrower column is a narrower measure and the
   measure is the thing D1 locked.

   Second step at 600: at eight columns a 5/3 pair leaves the
   minor side under 200px, which is not a column, it is a
   caption pretending to be one.
   ============================================================ */

@media (max-width: 919px) {
  :root {
    --kdb-grid-columns: 8;
    --kdb-grid-gutter: 16px;
  }
  /* The asymmetry law needs sixteen columns to mean anything —
     10/6 of eight is 5/3, which is a different law. Below the
     breakpoint the splits collapse and content stacks in source
     order, which is the order it was written to be read in. */
  .k-span-major, .k-span-minor, .k-span-lead,
  .k-span-trail, .k-span-third, .k-span-quarter,
  .k-prose { grid-column: 1 / -1; }
  /* Both selectors, and the :has() one is the whole point. Written
     as `.k-body` alone this rule never fired: the annotation
     margin's own rule is `.k-grid:has(> .k-annotation) .k-body`
     at specificity (0,3,0) against this one's (0,1,0), and a
     media query adds no specificity — so every annotated section
     kept its column-2 start below the breakpoint. Measured on a
     390px viewport before the fix: 111px of 390, 28% of the
     screen, held by a margin D16 says has to dissolve. The same
     class of failure as the .k-span-* shorthand collision already
     in the log, and invisible for the same reason — nothing looks
     broken, the page is just narrower than it should be. */
  .k-grid:has(> .k-annotation) .k-body,
  .k-body { grid-column-start: 1; }

  /* The annotation margin cannot survive: one column of eight is
     12.5% of the page and the label would set the measure for the
     whole row. It goes above its block instead — permitted only
     because an annotation is DATA (a year, a unit, a chapter
     mark), and data above a heading is not an eyebrow. */
  .k-annotation {
    grid-column: 1 / -1;
    margin-block-end: var(--kdb-space-tight);
  }

  /* Rhythm compresses with the viewport. The ratios between the
     four gaps are held; only the scale moves. */
  :root {
    --kdb-section-gap: var(--kdb-space-group);
    --kdb-group-gap: var(--kdb-space-block);
    --kdb-chapter-gap: var(--kdb-space-section);
  }
}

/* A six-column ledger does not fit a phone, and the failure is
   the worst kind: the TABLE pushes the PAGE, so every other
   section inherits a horizontal scroll. Caught by the auditor,
   which names the offending element for exactly this reason.

   The table scrolls inside itself instead. Nothing is dropped —
   hiding a column on a small screen is deciding the reader on a
   phone deserves less of the record. */
@media (max-width: 919px) {
  .t-table {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  .t-table thead,
  .t-table tbody {
    display: table;
    width: 100%;
    min-width: 32rem;
  }
  .t-table th,
  .t-table td { white-space: nowrap; }
  /* The one cell that must wrap: the Chelsea row's explanation
     spans four columns and is a sentence, not a figure. */
  .t-table td[colspan] { white-space: normal; }
}

@media (max-width: 599px) {
  :root {
    --kdb-grid-columns: 4;
    --kdb-grid-gutter: 12px;
  }
  /* A full-bleed image already spans the viewport at this size,
     so the bleed class would double the margin out and overflow.
     It becomes a plain edge-to-edge frame. */
  .k-photo-inset { margin-inline-end: 0; }
  /* Two of the five photo layouts do not survive a narrow screen:
     16:5 becomes a letterbox slit and 5:3 crops the subject out.
     Both fall back to the portrait ratio, which is the one shape
     that works on a phone. */
  .k-photo-banner img,
  .k-photo-plate img { aspect-ratio: 4 / 5; }
  /* Display type may not overhang when there is no margin to
     overhang into. */
  .k-overhang { margin-left: 0; }
}

/* ============================================================
   FOCUS — D17
   ------------------------------------------------------------
   Nothing defined a focus ring, which means the system shipped
   whatever each browser draws and no contrast guarantee at all.

   One ring. Ice at the mark step, which is the accent's
   load-bearing mode and measures 3.60:1 against the ground, so
   it clears the 3:1 non-text floor the rest of the system is
   held to. Offset so it reads as a ring around the thing rather
   than a border on it.
   ============================================================ */

:where(a, button, [tabindex], input, select, textarea):focus-visible {
  outline: 2px solid var(--kdb-accent-mark);
  outline-offset: 3px;
  border-radius: 1px;
}

/* A skip link is the one control allowed to hide until focused. */
.k-skip {
  position: absolute;
  left: -9999px;
}
.k-skip:focus-visible {
  left: var(--kdb-page-margin);
  top: var(--kdb-space-tight);
  z-index: 10;
  background: var(--kdb-surface-ground);
  padding: var(--kdb-space-tight) var(--kdb-space-snug);
}

/* ============================================================
   NAVIGATION — D18
   ------------------------------------------------------------
   A header is furniture. It has to be findable and it must never
   compete with the page, which rules out the two things a header
   usually does here: going bold, and going sticky-with-a-shadow.

   No shadow anywhere in this system, so a sticky header separates
   from the page with a hairline and nothing else. Labels are at
   the label step in sentence case — the site's own no-eyebrow
   logic applies to navigation as much as to a headline.
   ============================================================ */

.k-nav {
  /* Fixed, not sticky. Sticky still occupies its 64px of flow, so
     a "floating" bar was pushing the page down and the hero began
     BELOW it — which meant a full-bleed hero could never actually
     run to the top of the screen, and the bar was never over the
     photograph it needed to invert against.

     Fixed reserves nothing. The beats that need to clear it say so
     themselves (the slide's rail, the otot and wall pad-block
     both start at --kdb-nav-height); a bar with no ground only
     has to be got out of the way where type would collide. */
  position: fixed;
  inset-inline: 0;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: var(--kdb-nav-gap);
  min-height: var(--kdb-nav-height);
  padding-inline: var(--kdb-page-margin);
  /* Floats. No ground, no rule under it — the mark and the toggle
     sit on the page rather than on a bar.

     This is a real reversal of D18's "a sticky header separates
     from the page with a hairline": the hairline was the answer
     when every beat was a contained section on the same ground.
     With full-viewport slides under it, a bar drew a line across
     a photograph.

     The cost is stated rather than designed around: over a dark
     frame — the trophy finale, a stadium plate — ink-on-ground
     furniture has nothing to sit on. It needs an inversion rule
     keyed to what is under it, which is a real piece of work and
     is NOT done here. */
  background: none;
  border-block-end: 0;
  pointer-events: none;
}
.k-nav > * { pointer-events: auto; }
.k-nav__mark {
  display: inline-flex;
  align-items: center;
  margin-inline-end: auto;
}
.k-nav__mark svg { height: 22px; width: auto; display: block; }

/* One button, not a row — D18's list-of-labels design is kept
   below for the revealed panel, but the bar itself now shows a
   single control. Rectilinear, per the D14 addendum: component
   chrome never takes a corner, only photography and the three
   literal forms earn a curve. */
.k-nav__toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--kdb-nav-height);
  height: var(--kdb-nav-height);
  margin-block: calc(-1 * var(--kdb-space-tight));
  padding: 0;
  border: 0;
  background: none;
  color: var(--kdb-nav-ink);
  cursor: pointer;
}
/* Two icons stacked in one button. Grid rather than absolute
   positioning so the button still sizes itself from its contents
   and nothing has to be measured. */
.k-nav__toggle { display: grid; place-items: center; }
.k-nav__toggle svg {
  grid-area: 1 / 1;
  display: block;
  width: 18px;
  height: 18px;
  transition: opacity var(--kdb-motion-tick) var(--kdb-motion-curve);
}
.k-nav__toggle line,
.k-nav__toggle path,
.k-nav__toggle polygon {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  /* Non-scaling, so the cloche's own 90-unit strokes in a 1395
     viewBox resolve to the same weight as the bars it replaces
     rather than to 1.16px. Two icons in one button have to be
     drawn with one pen. */
  vector-effect: non-scaling-stroke;
}
.k-nav__toggle .k-nav__cloche {
  /* Slightly larger: the cloche is a drawing with air around it,
     where the bars run edge to edge, so matched box-for-box it
     reads smaller than what it replaced. */
  width: 26px;
  height: 26px;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
}
/* THREE ICONS, ONE BUTTON, AND THE STATES CANNOT TIE.
   Hover is matched on [aria-expanded="false"] rather than left
   bare, so the closed and open rules are mutually exclusive by
   attribute instead of by source order. Bare `:hover .k-nav__cloche`
   and `[aria-expanded="true"] .k-nav__cloche` carry identical
   specificity — which icon won would have come down to which rule
   was written last, and pointing at an open menu would have shown
   the cloche fighting the close mark. */
.k-nav__toggle .k-nav__cross { opacity: 0; }

.k-nav__toggle[aria-expanded="false"]:hover .k-nav__bars,
.k-nav__toggle[aria-expanded="false"]:focus-visible .k-nav__bars { opacity: 0; }
.k-nav__toggle[aria-expanded="false"]:hover .k-nav__cloche,
.k-nav__toggle[aria-expanded="false"]:focus-visible .k-nav__cloche { opacity: 1; }

/* OPEN, THE BUTTON IS A CROSS.
   It used to stay a hamburger and collapse its third bar into the
   second — three lines becoming two, which is a tidy piece of
   economy and completely fails to say "this closes". A control
   that shuts something should look like the thing that shuts it.
   The bars and the cloche both stand down; hovering an open menu
   does not bring either back. */
.k-nav__toggle[aria-expanded="true"] .k-nav__bars,
.k-nav__toggle[aria-expanded="true"] .k-nav__cloche { opacity: 0; }
.k-nav__toggle[aria-expanded="true"] .k-nav__cross { opacity: 1; }

/* The panel. Hidden by default, an absolutely-positioned drop from
   the bar rather than a layout push, so opening it never reflows
   the page underneath. No shadow, per D4/D6 — separation is the
   hairline border alone. */
.k-nav__list {
  position: absolute;
  top: 100%;
  inset-inline-end: var(--kdb-page-margin);
  margin-block-start: 0;
  display: flex;
  flex-direction: column;
  gap: var(--kdb-space-tight);
  list-style: none;
  padding: var(--kdb-space-snug);
  background: var(--kdb-surface-ground);
  border: var(--kdb-rule-hair) solid var(--kdb-nav-rule);
  border-block-start: 0;
}
.k-nav__list[hidden] { display: none; }
.k-nav a {
  font-size: var(--kdb-size-label);
  letter-spacing: var(--kdb-track-body);
  color: var(--kdb-nav-ink-muted);
  text-decoration: none;
  white-space: nowrap;
}
.k-nav a:hover,
.k-nav a[aria-current] {
  color: var(--kdb-nav-ink);
}
/* The current page is marked by a rule under it, not by weight —
   a bolder label in a row of labels changes the row's colour. */
.k-nav a[aria-current] {
  box-shadow: inset 0 -1px 0 0 currentColor;
}

/* ============================================================
   FOOTER — D19
   ------------------------------------------------------------
   The one dark surface, permitted because it is the END of the
   page rather than a surface behind content — D4 bans BLUE as a
   background, and ink is not blue.

   The first version was a heading, three links and a legal line
   floating in a tall dark band, which is a footer that has been
   remembered rather than designed. A footer is the last thing the
   page says. It gets the mark at size, the line the site is
   about, a real set of routes, and a colophon — the system
   describing itself in tabular figures, which is what this site
   is made of.
   ============================================================ */

.k-footer {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--kdb-footer-ground);
  color: var(--kdb-footer-ink);
  margin-block-start: var(--kdb-chapter-gap);
  padding-block: var(--kdb-space-group) var(--kdb-space-base);
}

/* THE ATMOSPHERIC FOOTER — D4b, finally used.

   D4b exempts the footer from the one-field-per-page cap and
   names it as one of the three rationed atmospheric moments, and
   the first version ignored that and shipped flat ink. The field
   is the system's own recipe from js/atmos.js — gradient, blur,
   grain and streaks, every value a token — with the type anchor
   at bottom-left so the sign-off sits in the one region D8
   certifies as deep enough to carry it. */
.k-footer__field {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
.k-footer__field svg { width: 100%; height: 100%; display: block; }

/* The photograph rides inside the field rather than beside it,
   at low presence and masked away from the anchor, so it is
   depth rather than a picture with words on top. Treated by
   js/photo.js like every other frame — an untreated photograph
   under a tokenized field is two different lights in one object. */
.k-footer__frame {
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: 0.22;
  pointer-events: none;
  mask-image: linear-gradient(105deg, transparent 0 34%, #000 72%);
  -webkit-mask-image: linear-gradient(105deg, transparent 0 34%, #000 72%);
}
.k-footer__frame img {
  width: 100%; height: 100%; object-fit: cover; object-position: 50% 30%;
  display: block;
}
.k-footer a { color: var(--kdb-footer-ink-strong); text-decoration-color: var(--kdb-footer-rule); }
.k-footer a:hover { text-decoration-color: currentColor; }
.k-footer :where(a, button):focus-visible { outline-color: var(--kdb-neutral-00); }

/* The mark, knocked out of the ink at a size that reads as a
   sign-off rather than as a favicon. */
.k-footer__mark svg { width: 100%; height: auto; display: block; }
/* display:block, or the cap does nothing. max-width has no effect
   on an inline element, so this rule was inert and the mark
   rendered at 1068x640 — the "poster rather than a specimen"
   failure the tokens file warns about, live on the page and
   invisible because the rule LOOKED right. */
.k-footer__mark { display: block; max-width: 168px; }

/* The line. Display type is the whole point of a footer that is
   the end of something. */
.k-footer__line {
  color: var(--kdb-footer-ink-strong);
  text-wrap: balance;
}

/* Routes, grouped. A single flat list of three is a nav that gave
   up; groups say what kind of thing each link is. */
.k-footer__group { break-inside: avoid; }
/* Sentence case, like every other label in the system. These
   survive the subtractive test — remove "Proven on" and you cannot
   tell one group from another, so they are labels rather than
   eyebrows — but caps plus tracking was the texture that kept
   being flagged, and a group heading does not need to shout to
   name its group. */
.k-footer__group h3 {
  font-size: var(--kdb-size-caption);
  color: var(--kdb-neutral-60);
  font-weight: var(--kdb-wght-regular);
  margin-block-end: var(--kdb-space-tight);
}
.k-footer__list { list-style: none; margin: 0; padding: 0; }
.k-footer__list li + li { margin-block-start: var(--kdb-space-hair); }
.k-footer__list a {
  font-size: var(--kdb-size-small);
  text-decoration-line: none;
  box-shadow: inset 0 -1px 0 0 transparent;
}
.k-footer__list a:hover { box-shadow: inset 0 -1px 0 0 currentColor; }

/* The colophon: the system describing itself, in the numerals the
   rest of the site is set in. */
.k-footer__colophon {
  display: flex;
  flex-wrap: wrap;
  gap: var(--kdb-space-base);
  font-size: var(--kdb-size-caption);
  color: var(--kdb-neutral-60);
}
.k-footer__colophon b {
  display: block;
  color: var(--kdb-footer-ink);
  font-variant-numeric: lining-nums tabular-nums;
  font-weight: var(--kdb-wght-regular);
  font-size: var(--kdb-size-body);
}
.k-footer__legal {
  color: var(--kdb-neutral-40);
  font-size: var(--kdb-size-caption);
  max-width: var(--kdb-measure-caption);
}
.k-footer .k-rule-divide { border-block-start-color: var(--kdb-footer-rule); }

/* ============================================================
   SCROLL MOTION — D20
   ------------------------------------------------------------
   D9g settled how a DRAWING arrives. This is how a PAGE does.

   A reveal is still a draw: opacity and a short travel along the
   axis the content already runs on, never a fade-up-from-below,
   never a spring, never a scale. The travel is 12px — under the
   hair step, so it reads as settling rather than as movement.

   Nothing reveals twice, and nothing reveals that is already on
   screen at load: a page that animates its own first viewport
   makes the reader wait to read what was already there.
   ============================================================ */

/* THE HIDDEN STATE IS OPT-IN, and JS opts in.

   Written the obvious way — [data-reveal] { opacity: 0 } — the
   page hides every section below the fold and then relies on
   script to bring them back. If the script fails to parse, is
   blocked, or the observer never fires, the content is invisible
   permanently. Content must never depend on JavaScript to be
   VISIBLE; it may only depend on it to be animated.

   So js/reveal.js sets .k-reveal-ready on <html> as its first act,
   and only then does anything hide. No script, no hiding. */
.k-reveal-ready [data-reveal] {
  opacity: 0;
  transform: translateY(12px);
  transition:
    opacity var(--kdb-motion-sweep) var(--kdb-motion-curve),
    transform var(--kdb-motion-sweep) var(--kdb-motion-curve);
}
.k-reveal-ready [data-reveal="in"] {
  opacity: 1;
  transform: none;
}
/* Reduced motion is a FALLBACK, NOT A REMOVAL — the content
   arrives complete on the first frame rather than never arriving,
   which is what display:none-ing a reveal would do. */
@media (prefers-reduced-motion: reduce) {
  .k-reveal-ready [data-reveal] { opacity: 1; transform: none; transition: none; }
}

/* A sticky beat: the trophy lift, the montage rail. The frame
   pins and the content inside it changes. */
.k-sticky {
  position: sticky;
  top: var(--kdb-nav-height);
}

/* ============================================================
   AMBIENT MOTION — D21
   ------------------------------------------------------------
   D20 covered how a page ARRIVES. This is motion that never
   resolves, and the two have opposite requirements: an arrival
   should be noticed, a float should not.

   Four rules, and each one is what stops "floating" becoming
   "bobbing".

   1 · SLOW ENOUGH TO DOUBT. A 14-second cycle. If a viewer can
       time the loop, it is an animation; if they cannot be sure
       it moved, it is air.
   2 · NEVER IN STEP. Every object takes its own delay from its
       index. Synchronised drift is a carousel — the eye locks
       onto the pulse and the illusion of independent objects is
       gone.
   3 · AMPLITUDE SCALES WITH THE OBJECT, at 3% of its own size,
       so a large jersey and a small one drift by the same
       proportion rather than the same distance.
   4 · WHAT IS BEING LOOKED AT HOLDS STILL. Hover settles the
       object in 320ms. You cannot examine a moving thing, and an
       object that keeps drifting under the cursor is telling the
       reader their attention does not matter.

   Tilt is capped at 1.5 degrees. Past about two a jersey stops
   reading as hanging and starts reading as tumbling.
   ============================================================ */

.k-drift {
  animation: k-drift var(--kdb-drift-period) ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes k-drift {
  from { transform: translate3d(0, calc(-1 * var(--kdb-drift-amp)), 0)
                    rotate(calc(-1 * var(--kdb-drift-tilt))); }
  to   { transform: translate3d(0, var(--kdb-drift-amp), 0)
                    rotate(var(--kdb-drift-tilt)); }
}
.k-drift:hover,
.k-drift:focus-visible,
.k-drift[data-held] {
  animation-play-state: paused;
  transition: transform var(--kdb-drift-settle) var(--kdb-motion-curve);
}

/* Reduced motion removes the drift entirely rather than slowing
   it. Ambient motion has no content in it — unlike a reveal,
   there is nothing to arrive — so there is nothing to preserve. */
@media (prefers-reduced-motion: reduce) {
  .k-drift { animation: none; }
}

/* ============================================================
   HOVER REVEAL — D22
   ------------------------------------------------------------
   An object that carries a second image behind it: a kit, and
   the kit being worn.

   THE REVEAL IS A DRAW (D20). Opacity, no scale, no flip, no
   crossfade-with-movement. The action frame is treated (D13) and
   takes an earned crop (D14) — this is exactly the case D14 kept
   the curve for: one subject framed, lifted out of a field of
   rectangles.

   TOUCH IS NOT A SECOND-CLASS PATH. There is no hover on a
   phone, so the reveal is bound to a real activation and the
   markup is a <button>: focusable, announceable, and working
   under keyboard without a single extra line.

   ONE REVEAL AT A TIME. Two open reveals is a contact sheet, and
   a contact sheet is not an interaction.
   ============================================================ */

.k-reveal-pair {
  position: relative;
  display: block;
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
  inline-size: 100%;
}
.k-reveal-pair img { display: block; width: 100%; height: auto; }
.k-reveal-pair__action {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--kdb-motion-draw) var(--kdb-motion-curve);
}
.k-reveal-pair__action img {
  height: 100%;
  object-fit: cover;
  clip-path: var(--k-shape, none);
}
.k-reveal-pair:hover .k-reveal-pair__action,
.k-reveal-pair:focus-visible .k-reveal-pair__action,
.k-reveal-pair[aria-expanded="true"] .k-reveal-pair__action { opacity: 1; }

/* The caption for the revealed frame sits beneath, never over it
   — a label on top of a photograph is a caption that did not
   trust the photograph. */
.k-reveal-pair__label {
  margin-block-start: var(--kdb-space-tight);
  font-size: var(--kdb-size-caption);
  color: var(--kdb-text-muted);
  text-align: start;
}

/* ============================================================
   HERO — exploration, not yet locked
   ------------------------------------------------------------
   Five directions at preview.html?c=hero&v=a..e. They differ in
   one thing only: WHAT CARRIES THE FIRST SCREEN — a photograph,
   the type, the field, or the grid.

   The full-bleed budget is relaxed for this exploration. Worth
   holding onto while judging: the reason for the limit is the
   same one behind the earned crop. If every beat bleeds, none of
   them is an event.
   ============================================================ */

.k-hero { position: relative; isolation: isolate; }

/* Base for any hero image. The width rules were scoped to the
   split variant only, so a photograph in any other hero fell back
   to its intrinsic size and pushed the page 400px sideways —
   caught by the auditor naming the <img>, which is exactly the
   case that check was written for. */
.k-hero__media img { max-width: 100%; height: auto; display: block; }

/* A · full bleed. The photograph is the page. */
.k-hero--bleed {
  min-height: min(88vh, 860px);
  display: grid;
  align-items: end;
  overflow: hidden;
  background: var(--kdb-neutral-95);
}
.k-hero--bleed .k-hero__media,
.k-hero--bleed .k-hero__field { position: absolute; inset: 0; z-index: -1; }
/* The field is a SCRIM over the anchor, not a layer over the
   photograph. Laid across the whole frame at full strength it
   simply hid him, which is the field doing the opposite of its
   job: D8's anchor exists so type can sit on a photograph, not
   so the photograph can be replaced by one. It fades out along
   the same diagonal the anchor box runs on. */
.k-hero--bleed .k-hero__field {
  mask-image: linear-gradient(to top right, #000 0 22%, transparent 62%);
  -webkit-mask-image: linear-gradient(to top right, #000 0 22%, transparent 62%);
}
.k-hero--bleed .k-hero__media img,
.k-hero--bleed .k-hero__field svg { width: 100%; height: 100%; object-fit: cover; display: block; }
.k-hero--bleed .k-hero__copy {
  position: relative;
  padding: var(--kdb-space-group) var(--kdb-page-margin) var(--kdb-space-block);
  color: var(--kdb-neutral-00);
  max-width: 52%;                      /* D8's anchor box, respected */
}
@media (max-width: 919px) { .k-hero--bleed .k-hero__copy { max-width: 100%; } }

/* B · plate. The photograph is held by the grid, at the mark's
   own 5:3, with the type beside it. */
.k-hero--plate .k-hero__media img { aspect-ratio: 5 / 3; object-fit: cover; width: 100%; }

/* C · type first. The line is the page; the photograph is a
   portrait inset that arrives second. */
.k-hero--type .k-hero__copy { padding-block: var(--kdb-space-group) 0; }
.k-hero--type .k-hero__media img { aspect-ratio: 3 / 4; object-fit: cover; width: 100%; }

/* D · field. No photograph at all — the atmosphere and the mark. */
.k-hero--field {
  min-height: min(78vh, 760px);
  display: grid;
  align-items: end;
  overflow: hidden;
  background: var(--kdb-ice-95);
}
.k-hero--field .k-hero__field { position: absolute; inset: 0; z-index: -1; }
.k-hero--field .k-hero__field svg { width: 100%; height: 100%; display: block; }
.k-hero--field .k-hero__copy {
  position: relative;
  padding: var(--kdb-space-group) var(--kdb-page-margin) var(--kdb-space-block);
  color: var(--kdb-neutral-00);
  max-width: 52%;
}
@media (max-width: 919px) { .k-hero--field .k-hero__copy { max-width: 100%; } }

/* E · split. Type holds the left, the photograph takes the right
   edge to edge — D5d's sanctioned media bleed, at full height. */
.k-hero--split {
  display: grid;
  grid-template-columns: 1fr minmax(0, 46%);
  align-items: center;
  gap: var(--kdb-space-group);
  min-height: min(80vh, 820px);
  padding-inline-start: var(--kdb-page-margin);
}
.k-hero--split .k-hero__media { align-self: stretch; }
.k-hero--split .k-hero__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
@media (max-width: 919px) {
  .k-hero--split { grid-template-columns: 1fr; padding-inline-end: var(--kdb-page-margin); }
  .k-hero--split .k-hero__media img { aspect-ratio: 4 / 3; }
}

.k-hero__eyebrowless { color: inherit; }
.k-hero__deck { color: var(--kdb-neutral-20); max-width: var(--kdb-measure-deck); }

/* F · diptych. Two related, separate photographs — a wide field
   and a close portrait, sharing one baseline — rather than one
   photograph. Promoted from explorations/hero-lab/dip.html
   ("Layout 2, wide leads"), the winner of a 24-direction round.

   D14's own named ratios turned out to fit it exactly: the wide
   frame is a plate (5:3, the mark's own proportion) and the close
   is a portrait (3:4). Nothing new was needed for the frames
   themselves, only for the apparatus around them — the lab's
   registration corners and plate captions, rebuilt on real
   tokens instead of hand-picked opacity values. The lab's tick
   rulers and 12/16-column grid overlay are deliberately NOT
   ported: a visible grid under every hero is the graph-paper
   failure D6d already ruled out for the page ground, and ticks
   without a measurement to graduate are decoration. */
/* Breaks out of .k-page's max-width and margin rather than sitting
   inside them — the standard calc(50% - 50vw) full-bleed trick,
   D14's one full-bleed allowance per page spent here. Re-applies
   its own, tighter inline padding instead of the page's wider
   responsive margin: the photos read as small specifically because
   a wide page margin plus a centred 1440 cap was leaving real
   width unused on either side. */
.k-hero--diptych {
  position: relative;
  margin-inline: calc(50% - 50vw);
  padding-inline: var(--kdb-space-base);
  padding-block: var(--kdb-space-group);
}

/* Registration corners — four ticks, not a frame. Page chrome,
   not a data drawing, so plain CSS rather than D9a's SVG rule. */
.k-hero--diptych .k-hero__reg { position: absolute; inset: var(--kdb-space-snug); pointer-events: none; }
.k-hero--diptych .k-hero__reg i { position: absolute; width: 11px; height: 11px; }
.k-hero--diptych .k-hero__reg i::before,
.k-hero--diptych .k-hero__reg i::after { content: ""; position: absolute; background: var(--kdb-rule-hair-ink); }
.k-hero--diptych .k-hero__reg i::before { left: 50%; top: 0; bottom: 0; width: var(--kdb-rule-hair); }
.k-hero--diptych .k-hero__reg i::after { top: 50%; left: 0; right: 0; height: var(--kdb-rule-hair); }
.k-hero--diptych .k-hero__reg i:nth-child(1) { left: 0; top: 0; }
.k-hero--diptych .k-hero__reg i:nth-child(2) { right: 0; top: 0; }
.k-hero--diptych .k-hero__reg i:nth-child(3) { left: 0; bottom: 0; }
.k-hero--diptych .k-hero__reg i:nth-child(4) { right: 0; bottom: 0; }

/* Head: the mark, ink-on-ground only (D23), and the two shirt
   numbers, at opposite corners — two genuinely separate facts, so
   space-between is correct here. Foot: the next fixture and the
   competition line, which are ONE D6e stamp — a fixture belongs to
   a competition, so splitting them to opposite edges argued they
   were unrelated. Connected into a single cluster below instead. */
.k-hero--diptych .k-hero__head,
.k-hero--diptych .k-hero__foot {
  position: relative;
  display: flex; align-items: baseline;
  font-family: var(--kdb-family-mono);
  font-size: var(--kdb-size-micro);
  letter-spacing: var(--kdb-track-caps);
  color: var(--kdb-text-muted);
}
/* flex-end, not space-between: the mark that used to balance this
   row against the nav's own copy lived here too until it was
   found duplicating a fixed element already on screen in the nav.
   One child now, and it keeps its former right-hand position
   rather than collapsing to the start space-between would leave
   it at. */
.k-hero--diptych .k-hero__head { justify-content: flex-end; align-items: center; margin-block-end: var(--kdb-space-base); }
.k-hero--diptych .k-hero__foot { margin-block-start: var(--kdb-space-base); }
.k-hero--diptych .k-hero__stamp { display: inline-flex; align-items: center; gap: var(--kdb-space-tight); }
.k-hero--diptych .k-hero__stamp-div { align-self: stretch; width: var(--kdb-rule-hair); background: var(--kdb-rule-hair-ink); }
.k-hero--diptych .k-hero__head b,
.k-hero--diptych .k-hero__foot b {
  font-weight: var(--kdb-wght-medium); color: var(--kdb-text-primary);
  font-variant-numeric: tabular-nums;
}
.k-hero--diptych .k-hero__nums { display: flex; gap: var(--kdb-space-base); }
/* No .k-hero__mark here any more — the mark now appears once,
   in .k-nav, not once per fixed element on screen. */

/* The pair. 10fr/6fr — D5d's default split, unchanged, spelled
   out rather than composed from .k-span-major/.k-span-minor so
   this component stays self-contained like every other .k-hero
   variant.

   Two plates each holding their own independent ratio (5:3 and
   3:4) at two different track widths almost never land on the
   same height, and a diptych with no shared baseline reads as two
   unrelated photographs rather than a pair — confirmed against the
   lab reference this was promoted from, where both frames render
   at an identical height (514px measured at 1280 wide) though
   their track widths differ 10:6.

   A first attempt tried to get there with grid stretch alone —
   .k-hero__pair left to size itself from content, the wide plate's
   5:3 ratio implicitly setting the row, the close plate stretching
   to match via align-items. It didn't hold: an aspect-ratio'd
   image only ever renders at ITS OWN ratio-derived size, so it
   never actually grows to fill a taller flexible track even once
   one exists — the two plates landed 27px apart instead of
   matching. The lab's own comment on an unrelated component
   diagnoses the same failure exactly: "with min-height only, the
   1fr track is free to grow, and an <img> with height:100% inside
   an unbounded track falls back to its intrinsic size." A shared
   height has to be a REAL height, declared once, not implied. */
.k-hero--diptych .k-hero__pair {
  container-type: inline-size;
  display: grid;
  grid-template-columns: 10fr 6fr;
  gap: var(--kdb-page-gutter);
}
/* min-width: 0 overrides the grid item default of auto, which for
   an <img> resolves to its intrinsic width — without it each plate
   refused to shrink below the source image's native size and the
   10fr/6fr split was overridden into a horizontal overflow instead
   of a proportion. Caught rendering kdb-vision.png (1920 intrinsic)
   at 1920px wide on a 1280px viewport. */
.k-hero--diptych .k-hero__plate { margin: 0; min-width: 0; }

/* The frame is the declared height, and it is derived from D14's
   5:3 rather than guessed at: the wide plate takes 10 of the
   pair's 16fr, so at 1cqw = 1% of the PAIR's own width (not the
   viewport — page margins and the 1440 cap would throw a vw-based
   guess off), wide width ≈ 62.5cqw and the height that keeps it at
   5:3 is 62.5cqw × 3/5 = 37.5cqw. A first version used min(50vw,
   500px), measured after the fact at 724×500 — 1.45:1, nowhere
   near 5:3 — because it was picked to look right rather than
   derived from the ratio it was supposed to carry. Capped so nothing
   absurd happens past the 1440 container. Both images fill the
   frame identically and crop via object-fit. No per-plate caption
   sits under either frame — a first version gave each plate its own
   "01 · Napoli · Maradona" line on a hairline, which the photos did
   not need to be read as a pair: the shirt numbers in the head and
   the fixture in the foot already carry that context once, and
   repeating it under each frame was the eyebrow failure again at
   caption scale — data that describes what the reader is already
   looking at, dressed as if it were new information. */
/* Taller than D14's 5:3 by request — 56cqw against the 37.5cqw
   that held true 5:3, so the wide plate now reads closer to
   square-ish than landscape. The ratio derivation two entries up
   is still the right way to hit a SPECIFIC ratio; this is a
   deliberate departure from it for more presence, not a return to
   guessing. object-position anchors both crops to the bottom of
   the source rather than the centre, so the extra height is spent
   revealing more of what's below him — grass, the ball — rather
   than more sky and crowd above. js/build.js's parallax shifts
   from that same bottom anchor on hover, not from 50/50. */
.k-hero--diptych .k-hero__frame { height: min(56cqw, 640px); overflow: hidden; }
.k-hero--diptych .k-hero__frame img {
  width: 100%; height: 100%; object-fit: cover; object-position: center bottom; display: block;
}

@media (max-width: 919px) {
  .k-hero--diptych .k-hero__pair { grid-template-columns: 1fr !important; }
  .k-hero--diptych .k-hero__reg,
  .k-hero--diptych .k-hero__foot { display: none; }
}

/* ============================================================
   HERO — character directions, f..j
   ------------------------------------------------------------
   a..e were photography ARCHETYPES: full bleed, plate, type
   first, field, split. Any site could run them, which is why
   they read as cookie cutter — they spend none of what this
   system actually has.

   These spend it. Each one is built on something the system owns
   and nothing else does: the mark's own literal forms, the
   drawing law, the arrival in the tiebreaker line.
   ============================================================ */

.k-hero--drawn {
  position: relative;
  min-height: min(90vh, 880px);
  display: grid;
  align-items: center;
  background: var(--kdb-surface-ground);
  overflow: hidden;
}
.k-hero--drawn .k-hero__plot { position: absolute; inset: 0; }
.k-hero--drawn .k-hero__plot svg { width: 100%; height: 100%; display: block; }
.k-hero--drawn .k-hero__copy {
  position: relative;
  padding-inline: var(--kdb-page-margin);
  max-width: 56%;
}
@media (max-width: 919px) { .k-hero--drawn .k-hero__copy { max-width: 100%; } }

/* Held by one of the mark's own literal forms. Not a rounded
   rectangle and not a generic blob — the D, at hero scale. This
   is the case D14 kept the earned crop for. */
.k-hero--held { padding-block: var(--kdb-space-block); }
.k-hero--held .k-hero__form { width: 100%; height: auto; display: block; }

/* The record as display numerals. An old build note put it
   plainly: the numbers ARE the brand. Tabular, so a column of
   them cannot wobble. */
.k-figure-stack { display: grid; gap: var(--kdb-space-base); }
.k-figure-row {
  display: flex;
  align-items: baseline;
  gap: var(--kdb-space-snug);
  border-block-start: var(--kdb-rule-hair) solid var(--kdb-rule-hair-ink);
  padding-block-start: var(--kdb-space-tight);
}
.k-figure-row b {
  font-family: var(--kdb-family-archivo);
  font-variation-settings: "wdth" var(--kdb-wdth-display);
  font-weight: var(--kdb-wght-heavy);
  font-size: var(--kdb-size-figure);
  line-height: var(--kdb-leading-figure);
  font-variant-numeric: lining-nums tabular-nums;
  min-width: 3.2ch;
}
.k-figure-row span { font-size: var(--kdb-size-small); color: var(--kdb-text-muted); }

/* The pass arriving — F's mechanic. A draw, in the strict sense:
   the line is drawn along its own length rather than faded in. */
@keyframes k-pass { to { stroke-dashoffset: 0; } }
@keyframes k-arrive { to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  [style*="k-pass"] { stroke-dashoffset: 0 !important; animation: none !important; }
  [style*="k-arrive"] { opacity: 1 !important; animation: none !important; }
}

/* ============================================================
   THE SLIDE — beat 04, the record read sideways
   ------------------------------------------------------------
   Replaces .k-rail, which was the cautious version of this: a
   contained band inside a scrolling page, bleeding right because
   D14 was rationing full bleed. D29 through D34 removed the
   reasons it was built that way, so this is the real thing —
   a full-viewport slide the reader travels sideways through.

   The composition is recovered from
   references/_archive/section-sheet/sections/02-hero-stats.html,
   which is where it was worked out and which is the best version
   of this beat anyone has drawn. What changes: one family
   (Archivo, D1) instead of Anton plus Source Sans, the design
   system's palette, no text-shadows and no drop-shadows (D33),
   captions beneath their frames, and a scrollbar that stays
   visible (D32). What is kept, deliberately: the full-screen
   slide (D29), the overlaps (D33), the wheel capture (D32), and
   the hand-tuned viewport units (D34).

   Four groups, in this order: assists, player of the year,
   Belgium, the cabinet. The grouping is the information design —
   a reader travels one subject at a time, and the group gap is
   nine times the gap inside a group so that reads as structure
   rather than as spacing.

   The slide carries [data-kdb-slide], which is how js/audit.js
   knows the rem ladder and the overflow check do not apply here.
   ============================================================ */

/* PINNED. The section is a tall scroll well; the viewport inside
   sticks, and the reader's own vertical scroll drives the track
   sideways. You cannot reach the trophies without travelling the
   record, and you cannot leave until you have.

   This replaces the native horizontal scroller and its wheel
   capture (D32 is superseded — see the decision log), and it fixes
   an overlap that was structural rather than cosmetic: the finale
   used to be an absolutely positioned stage sitting ON TOP of the
   rail at z-index 4, so its plate, figure and shelf were over the
   groups the whole time. In a track the finale is a PANEL IN THE
   ROW — the last one — and two things in a row cannot overlap.

   It also drops the dependency on scroll-driven CSS, which is what
   broke this section in Safari. A transform driven by a scroll
   position works everywhere. */
.k-slide {
  position: relative;
  margin-inline: calc(50% - 50vw);
  background: var(--kdb-surface-ground);
  /* Set from JS once the track is measured — the well has to be as
     deep as the track is wide, or the last panel never lands.
     The fallback keeps the section a sane height if JS never runs. */
  height: var(--k-slide-well, 400vh);
}

.k-slide__pin {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}

.k-slide__track {
  display: flex;
  align-items: stretch;
  height: 100%;
  width: max-content;
  padding-block: calc(var(--kdb-nav-height) + var(--kdb-space-snug)) var(--kdb-space-base);
  padding-inline: var(--kdb-slide-pad-inline);
  container-type: size;
  will-change: transform;
}

/* No JS, no pin: the track becomes an ordinary horizontal
   scroller so the content is reachable rather than stranded. */
.k-slide:not([data-pinned]) { height: auto; }
.k-slide:not([data-pinned]) .k-slide__pin { position: static; overflow-x: auto; }


.k-slide__group {
  display: flex;
  flex: 0 0 auto;
  align-items: flex-start;
  height: 100%;
  margin-right: var(--kdb-slide-group-gap);
}
.k-slide__group:last-child { margin-right: 2vw; }

/* Vertical placement inside a group. Three positions, not free
   coordinates: a cell sits on the floor, hangs from the ceiling,
   or centres. That is what keeps a hand-tuned composition from
   becoming arbitrary. */
.k-slide__floor { align-self: flex-end; }
.k-slide__ceil  { align-self: flex-start; }
.k-slide__mid   { align-self: center; }

/* Frames. Four heights, so the run has rhythm without every
   frame being its own size. */
/* --k-h defaults rather than being required. A figure that
   reaches this rule with no height class would otherwise resolve
   `height: var(--k-h)` to an invalid value, fall back to auto, and
   render at the source's natural size — which is how a 3840px
   plate ended up in a 1440px slide and pushed the run from 7
   screens to 12. A missing class should be the wrong size, not a
   different order of magnitude. */
.k-slide figure { position: relative; margin: 0; flex: 0 0 auto; --k-h: var(--kdb-slide-h-md); }
.k-slide figure img { display: block; width: auto; height: var(--k-h); max-width: none; }
/* Measured against the rail's own content box (cqh), not the
   viewport. A 58vh frame in a box that is 100dvh minus the nav
   minus padding is taller than the box it sits in, and on a short
   window it simply hangs out of the bottom. The shares are the
   same proportions the reference build used; only what they are a
   share OF is corrected. */
.k-slide__h-full { --k-h: 100%; align-self: stretch; }
.k-slide__h-lg   { --k-h: var(--kdb-slide-h-lg); }
.k-slide__h-md   { --k-h: var(--kdb-slide-h-md); }
.k-slide__h-sm   { --k-h: var(--kdb-slide-h-sm); }

/* Nudges and overlaps — D33. Type crosses image, image crosses
   image, and the negative margin is the mechanism. */
.k-slide__up    { margin-bottom: 8cqh; }
.k-slide__down  { margin-top: 9cqh; }
.k-slide__more  { margin-top: 19cqh; }
.k-slide__lot   { margin-top: 26cqh; }
.k-slide__over  { margin-left: var(--kdb-slide-overlap); z-index: 2; }
.k-slide__over-sm { margin-left: var(--kdb-slide-overlap-sm); z-index: 2; }
.k-slide__push  { margin-right: 2vw; }

/* Captions sit BENEATH the frame, in ink on the ground. The
   reference build put them inside the frame in white with a
   text-shadow; no shadow exists in this system, and contrast over
   a photograph is solved by where the type sits, never by glowing
   it (D33). */
.k-slide__cap {
  margin-block-start: var(--kdb-media-caption-gap);
  font-size: var(--kdb-size-micro);
  font-weight: var(--kdb-wght-medium);
  letter-spacing: var(--kdb-track-caps-wide);
  text-transform: uppercase;
  color: var(--kdb-media-caption-ink);
}

/* The billboard — one number per subject, at the size the subject
   deserves. Condensed (D1's family, a new width token) because the
   proportion is load-bearing: an expanded numeral here is a
   different composition, not this one restyled. */
.k-slide__billboard {
  display: flex;
  align-items: flex-end;
  gap: 1.6vw;
  z-index: 2;
  padding-bottom: 6cqh;
}
.k-slide__billboard .k-slide__n {
  font-size: min(46vh, 32vw);
  line-height: 0.66;
  letter-spacing: -0.05em;
}
.k-slide__billboard--short .k-slide__n { font-size: min(50vh, 26vw); }
.k-slide__billboard .k-slide__word b {
  display: block;
  font-size: clamp(2rem, 4.2vw, 4.6rem);
  line-height: 0.8;
  letter-spacing: -0.03em;
}

/* The stat — the same idea, one step down. */
.k-slide__stat {
  display: flex;
  align-items: flex-end;
  gap: 0.5em;
  z-index: 2;
  padding-inline: 1.2vw;
}
.k-slide__stat .k-slide__n {
  font-size: clamp(3.8rem, 8.2vw, 7rem);
  line-height: 0.72;
  letter-spacing: -0.045em;
}
.k-slide__stat .k-slide__word b {
  display: block;
  max-width: 14ch;
  font-size: clamp(1.15rem, 1.85vw, 1.75rem);
  line-height: 0.88;
  letter-spacing: -0.025em;
}

/* Shared numeral and word treatment. Tabular is already the base
   rule, so nothing restates it. */
.k-slide__n,
.k-slide__word b {
  margin: 0;
  font-family: var(--kdb-family-archivo);
  font-variation-settings: "wdth" var(--kdb-slide-figure-wdth);
  font-weight: var(--kdb-wght-heavy);
  text-transform: uppercase;
  color: var(--kdb-text-primary);
}
.k-slide__word { margin: 0; padding-bottom: 0.14em; }
/* The qualifier under a word — at text width, not condensed. The
   condensed axis is for numerals at scale; a line of small caps
   set condensed is just hard to read. */
.k-slide__word small {
  display: block;
  margin-block-start: var(--kdb-space-tight);
  font-variation-settings: "wdth" var(--kdb-wdth-text);
  font-size: var(--kdb-size-micro);
  font-weight: var(--kdb-wght-medium);
  letter-spacing: var(--kdb-track-caps-wide);
  line-height: var(--kdb-leading-flat);
  text-transform: uppercase;
  color: var(--kdb-text-muted);
}

/* A statement rather than a number. One per group at most — it is
   the group's caption, said out loud. */
.k-slide__shout {
  margin: 0;
  max-width: 16ch;
  z-index: 2;
  padding-inline: 0.6vw;
  font-variation-settings: "wdth" var(--kdb-slide-figure-wdth);
  font-weight: var(--kdb-wght-heavy);
  font-size: clamp(1.6rem, 3.4vw, 3rem);
  line-height: 0.92;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  color: var(--kdb-text-primary);
}

/* The tuck — a frame set behind a billboard so the numeral crosses
   it. The overlap that makes the pair read as one object. */
/* The pair — a billboard numeral with a plate riding back over it.

   The plate is IN FLOW, pulled left by a negative margin, and that
   is a correctness fix as much as a stylistic one. Absolutely
   positioned (as the reference build has it) the plate contributes
   no width, so it overhung the pair by ~324px and landed on top of
   the next cell — dark ink on a dark photograph, unreadable. There
   is no CSS expression for "as wide as this image happens to be",
   so the only robust answer is to let the image size the box and
   move the overlap onto a margin. Which is what D33 says the
   mechanism is. */
.k-slide__pair {
  position: relative;
  display: flex;
  align-items: flex-end;
  height: 100%;
  padding-right: 1.5vw;
}
.k-slide__pair .k-slide__billboard { z-index: 2; }
/* Qualified with the type selector deliberately. `.k-slide__tuck`
   alone is one class and loses to `.k-slide figure` (one class,
   one type) on specificity, so the tuck stayed position:relative
   and joined the flex row instead of sitting behind the numeral.
   Third time this file has been bitten by a rule that never fired
   — see the annotation-margin entry and the .k-span-* shorthand
   collision. Height goes through --k-h so there is only one rule
   sizing an image in this component. */
.k-slide figure.k-slide__tuck {
  z-index: 1;
  overflow: hidden;
  height: var(--k-h);
  margin-left: var(--kdb-slide-tuck-pull);
}
.k-slide figure.k-slide__tuck--high { --k-h: var(--kdb-slide-h-tuck); align-self: flex-start; }
.k-slide figure.k-slide__tuck--low { --k-h: var(--kdb-slide-h-tuck-low); align-self: flex-end; margin-bottom: 6cqh; }

/* Narrow. A sideways slide is already the right shape for a phone,
   so nothing reflows — the frames step down and the numerals come
   off their viewport-height cap so they stop swallowing the screen. */
@media (max-width: 599px) {
  .k-slide__group { margin-right: 10vw; }
  .k-slide__billboard .k-slide__n { font-size: min(30vh, 46vw); }
  .k-slide figure.k-slide__tuck--high { --k-h: 72cqh; }
  .k-slide figure.k-slide__tuck--low { --k-h: 54cqh; }

  /* Frames are CROPPED to a share of the screen rather than left
     to size themselves off their height. Left alone, a 42vh frame
     on a 390px screen renders ~531px wide — wider than the phone —
     and ten of those turned a 8-screen run into a 23-screen one.
     Measured before changing it, because "long" is not a number.

     A fixed width plus object-fit:cover means the frame narrows by
     losing edges, not by shrinking the subject, which is the same
     bargain every other crop in this system makes. */
  .k-slide figure img {
    width: 72vw;
    object-fit: cover;
    /* Crop from the top. Cover on a frame narrower than the source
       takes the middle by default, which on a celebration frame is
       his chest — the head goes and the photograph stops being of
       anyone. */
    object-position: center top;
  }
  .k-slide__pair { padding-right: 2vw; }

  /* The desktop pull is 34vw of a 1440px screen — a third of a
     phone, which does not tuck the plate behind the numeral, it
     files it somewhere else entirely. */
  .k-slide figure.k-slide__tuck { margin-left: -10vw; }

  /* The word beside a numeral has to be allowed to wrap. Unbounded
     it took its qualifier lines off the right edge of the phone —
     "2ND ALL-TIME · PREMIER LE…" — while the rail reported no
     overflow, because the text was inside a scrolling box. */
  .k-slide__billboard { gap: 2vw; padding-bottom: 2.5vh; }
  .k-slide__billboard .k-slide__word { max-width: 34vw; }
  .k-slide__billboard .k-slide__word b { font-size: 1.5rem; }
  .k-slide__stat .k-slide__word { max-width: 40vw; }
}

/* ============================================================
   ON PITCH / OFF PITCH — beat 05
   ------------------------------------------------------------
   Recovered from builds/kevindebruyne-v2's .kdb-otot-stage, which
   is the best single idea in that build: the site's two halves
   stated as two lines of lockup type, with a photograph slotted
   INTO one of them at cap height so the frame reads as a glyph in
   the word rather than an illustration beside it. What v3 had
   before this was two photo cards in a grid — accurate, and
   completely inert.

   Changed from v2: one family (Archivo, D1) instead of Bodoni
   Moda, the design system's palette instead of #f8f8f3 paper, and
   the inactive side goes MUTED rather than pale blue — the
   active/inactive distinction survives greyscale, so it is not
   carried by colour. The shear on the photo slot is kept: it is
   the one earned crop in this beat (D14), and it is what makes
   the frame sit in the line like a letterform.

   Kept from v2: the full-viewport stage (D29), the words shifting
   apart to make room for the frame, the hairline-and-slash divider,
   and the clip wipe.

   Both lines are real links to real pages, so they are <a> and not
   the D22 button case — a hover here reveals a destination that
   exists rather than a second state of the same object.
   ============================================================ */

.k-otot {
  position: relative;
  margin-inline: calc(50% - 50vw);
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--kdb-otot-row-gap);
  overflow: hidden;
  padding-block: var(--kdb-nav-height) var(--kdb-space-block);
  padding-inline: clamp(1.25rem, 4vw, 3rem);
  background: var(--kdb-surface-ground);
}

.k-otot__slot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--kdb-otot-slot-gap);
  /* Hugs its own words rather than spanning the viewport. At 100%
     the two links were full-width bands stacked against each
     other, so the pointer was always inside one of them and the
     lead flipped on every vertical twitch — with no neutral
     ground to rest in, a deliberate choice read as a glitch.
     Now the words themselves are the target, which is also what
     a reader would aim at. */
  width: fit-content;
  max-width: 100%;
  text-decoration: none;
  /* Ink at rest, both of them. Nothing leads until the reader
     points at something — the section is a choice, and a choice
     that has already been made for you is a signpost. */
  color: var(--kdb-text-primary);
  font-family: var(--kdb-family-archivo);
  font-variation-settings: "wdth" var(--kdb-wdth-lockup);
  font-weight: var(--kdb-wght-heavy);
  font-size: var(--kdb-otot-type);
  line-height: var(--kdb-leading-lockup);
  letter-spacing: var(--kdb-track-lockup);
  text-transform: uppercase;
  white-space: nowrap;
  /* Colour is the only thing that transitions. Nothing moves, so
     the two lines never shift under the cursor — the words are the
     hit target and a target that moves when approached is the one
     interaction failure this section could have. */
  transition: color var(--kdb-otot-shift) var(--kdb-motion-curve);
}

/* Hover, focus, or touch-activation moves the lead. :focus-visible
   is in the same selector so the keyboard sees exactly what the
   mouse sees. */
/* The pointed-at side takes the lead and the other gives it up.

   Both rules carry the :has() prefix, and that is not redundant.
   Written the obvious way — a broad mute, then a bare
   `.k-otot__slot:hover` to light one back up — the mute is
   `.k-otot:has(.k-otot__slot:hover) .k-otot__slot` at (0,4,0)
   against the highlight's (0,2,0), because :has() contributes its
   own argument's specificity. The mute wins, and hovering greys
   out BOTH halves: the section goes dead exactly when it is being
   used. Caught by reading computed colour, not by looking — the
   two states are 20px apart in grey.

   Fourth specificity collision in this file (see .k-span-*, the
   annotation margin, and .k-slide__tuck). The pattern is always
   the same: a broad structural selector silently outranking the
   narrow one meant to override it. */
.k-otot:has(.k-otot__slot:hover) .k-otot__slot,
.k-otot:has(.k-otot__slot:focus-visible) .k-otot__slot { color: var(--kdb-text-muted); }
.k-otot:has(.k-otot__slot:hover) .k-otot__slot:hover,
.k-otot:has(.k-otot__slot:focus-visible) .k-otot__slot:focus-visible { color: var(--kdb-text-primary); }

/* The frame, in the line. Height in em, so it tracks the type
   rather than the viewport and stays a glyph at every size. */
.k-otot__frame {
  position: relative;
  flex: 0 0 auto;
  width: var(--kdb-otot-slot-w);
  height: var(--kdb-otot-slot-h);
  overflow: hidden;
  transform: skewX(var(--kdb-otot-shear));
  /* Shut at rest — the words are the section until the reader asks
     for more. The wipe is a clip, not a fade: the frame is DRAWN
     open, which is D20's "a reveal is a draw" applied to a
     photograph instead of a line.

     Direction is mirrored per slot, and that is the detail that
     makes it read as one gesture: each frame opens OUTWARD, away
     from the word it belongs to. On pitch sits left of its frame,
     so that one grows left-to-right; off pitch sits right of its
     frame, so that one grows right-to-left. Opened the same way,
     the second frame appears to slide in from the wrong side. */
  clip-path: var(--k-shut, inset(0 100% 0 0));

  /* The frame takes NO WIDTH at rest — pulled back by exactly its
     own width — so the words sit centred on the stage with nothing
     beside them. On approach the margin goes to zero, the row grows
     by the frame's width, and because the slot is centred the words
     shift OUTWARD as the picture arrives between them.

     That shift is the point. Revealing the frame without it makes
     the image appear next to a line of type that never moves; with
     it, the words open to make room, which is what a door does. */
  margin-inline-end: calc(-1 * var(--kdb-otot-slot-w));
  transition: clip-path var(--kdb-otot-wipe) var(--kdb-motion-curve),
              margin-inline-end var(--kdb-otot-wipe) var(--kdb-motion-curve),
              margin-inline-start var(--kdb-otot-wipe) var(--kdb-motion-curve);
}
/* Off pitch reads frame-then-words, so it collapses from the other
   side and opens leftward. */
.k-otot__slot[data-otot="off"] .k-otot__frame {
  --k-shut: inset(0 0 0 100%);
  margin-inline-end: 0;
  margin-inline-start: calc(-1 * var(--kdb-otot-slot-w));
}

.k-otot__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Per-slot, set in the markup: both frames are letterboxes cut
     from 3:2 sources whose subject sits right of centre, and a
     default centre crop takes the empty half of each. */
  object-position: var(--k-focus, center);
  /* Counter-shear, so the photograph inside is not distorted —
     only its frame is cut on the slant. */
  /* Counter-shear so the photograph is not distorted, only its
     frame is cut on the slant. The scale pushes past the shear's
     own corner gaps AND pulls the subject up in a letterbox cut
     from a wide stadium frame — set per slot, because how far in
     to go is a property of the picture. */
  transform: skewX(calc(-1 * var(--kdb-otot-shear))) scale(var(--k-zoom, 1.12));
}

/* Crop data, as classes rather than inline styles.

   Where the subject sits in a frame and how far to push in are
   properties of the specific photograph, so they wanted to live on
   the page — but an inline style is exactly what js/audit.js exists
   to catch, and it caught these. Art direction travels as a class
   here for the same reason it does in the slide: the markup says
   which treatment, the system says what the treatment is.

   Both sources are 3:2 with the subject right of centre, cut down
   to a letterbox — a default centre crop takes the empty half. */
.k-otot__frame--action   { --k-focus: 62% 44%; --k-zoom: 1.55; }
.k-otot__frame--portrait { --k-focus: 60% 38%; --k-zoom: 1.30; }

.k-otot__slot:hover .k-otot__frame,
.k-otot__slot:focus-visible .k-otot__frame {
  clip-path: inset(0);
  margin-inline-end: 0;
  margin-inline-start: 0;
}

/* The divider — a hairline broken by a slash. The slash is the
   whole argument of the section in one glyph: not a list of two
   things, a choice between them. */
.k-otot__rule {
  display: flex;
  align-items: center;
  gap: var(--kdb-space-snug);
  width: min(46vw, 34rem);
  color: var(--kdb-text-muted);
}
.k-otot__rule i {
  flex: 1 1 auto;
  height: var(--kdb-rule-hair);
  background: var(--kdb-rule-hair-ink);
}
.k-otot__rule span {
  font-size: var(--kdb-size-deck);
  font-weight: var(--kdb-wght-regular);
  line-height: 1;
}

/* Reduced motion: the frame arrives complete, the way a drawing
   does. The state still changes — it just does not travel. */
@media (prefers-reduced-motion: reduce) {
  .k-otot__slot,
  .k-otot__frame { transition: none; }
}

/* Narrow. The frame drops out of the line and the words stack —
   at 4 columns a cap-height photograph inside a word is 60px tall
   and reads as a smudge, not a frame. The type is the section; the
   frame is what it is holding, and it is the one that goes. */
@media (max-width: 599px) {
  .k-otot { gap: var(--kdb-space-base); }
  .k-otot__slot { flex-direction: column; gap: var(--kdb-space-snug); white-space: normal; text-align: center; }
  /* No hover on a phone, so the frames are simply open — the
     reveal has nothing to reveal from. */
  .k-otot__frame {
    width: min(78vw, 24rem);
    height: 11rem;
    clip-path: inset(0);
    margin-inline: 0;
  }
  .k-otot__slot[data-otot="off"] .k-otot__frame { --k-shut: inset(0); margin-inline: 0; }
}

/* ============================================================
   THE FINALE — the last panel in the track
   ------------------------------------------------------------
   One screen wide, at the end of the row. The cabinet plate opens
   across it as the reader arrives, and that open frame is what
   beat 05 scrolls up into.

   It is a PANEL, not an overlay. As an absolutely positioned stage
   it sat on top of the whole section at z-index 4 — the figures in
   groups 1–4 were behind it the entire time, and in any browser
   without scroll-driven CSS its plate was simply open from the
   start. In the row it cannot overlap anything, and its reveal is
   driven by the same scroll progress that moves the track, so it
   works everywhere.

   Layer order inside the panel, bottom to top: cabinet plate 0,
   selected competition plate 1, trail 2, figure and label 3,
   shelf 4.
   ============================================================ */

.k-slide__finale {
  position: relative;
  flex: 0 0 auto;
  /* Exactly the viewport, minus the track's own inline padding, so
     the panel's edges ARE the screen's edges when it lands. */
  width: calc(100vw - (var(--kdb-slide-pad-inline) * 2));
  height: 100%;
  margin-left: var(--kdb-slide-group-gap);

  /* THE SHELF'S OWN BAND, below the frame rather than on it.
     The thumbnails used to sit over the photograph — across the
     subject's legs in the Premier League plate — and the caption
     collided with them: the caption's bottom edge fell 23px below
     the row's top edge, so "PL · 2018-24" ran into the thumbnails.
     Giving the row a strip of its own means the picture is never
     asked to be a background for interface, and the caption gets
     the corner back. Every layer that IS the picture stops here. */
  /* How far the chosen thumbnail rises out of the row. Named,
     because the band below has to be tall enough to contain it. */
  --k-shelf-lift: 10px;
  /* Derived, not guessed: the thumbnail, the height it lifts by,
     and a snug margin above and below it. A hand-picked band was
     4px too short for the lift, which is why the selected
     thumbnail climbed up and touched the photograph. */
  --k-shelf-band: calc(
    var(--kdb-shelf-thumb) + var(--k-shelf-lift) + (var(--kdb-space-snug) * 2)
  );
}

.k-slide__finale-plate {
  position: absolute;
  inset: 0 0 var(--k-shelf-band) 0;
  overflow: hidden;
  z-index: 0;
  /* Opens from nothing, collapsed against the right edge. Driven
     by --k-finale-open (0..1), which build.js sets from the same
     scroll progress that moves the track. */
  clip-path: inset(
    calc(50% - (50% * var(--k-finale-open, 0)))
    0%
    calc(50% - (50% * var(--k-finale-open, 0)))
    calc(100% - (100% * var(--k-finale-open, 0)))
  );
}
.k-slide__finale-plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
}

/* The swapped competition plate — its own layer, above the cabinet
   and below the trail. A child of the button it belongs to would
   sit inside the shelf's stacking context and paint over the trail. */
/* One definition only — this rule used to be stated twice, once as
   a fade and again as a lift, and the second copy won the transform
   while the first still set opacity. See the lift below. */

/* Figure and shelf arrive after the plate is under them — the
   white "19" is knocked out of the frame, so it cannot read before
   the frame exists. */
.k-slide__finale .k-slide__billboard,
.k-slide__finale .k-trophies {
  opacity: calc((var(--k-finale-open, 0) - 0.55) / 0.35);
  transition: opacity 120ms linear;
}

.k-slide__finale .k-slide__billboard {
  position: absolute;
  z-index: 3;
  left: clamp(1.5rem, 3vw, 3.5rem);
  bottom: calc(var(--k-shelf-band) + clamp(1.5rem, 5cqh, 3.5rem));
  padding: 0;
}
.k-slide__finale .k-slide__n,
.k-slide__finale .k-slide__word b { color: var(--kdb-text-inverse); }
.k-slide__finale .k-slide__word small { color: var(--kdb-neutral-20); }

@media (max-width: 599px) {
  .k-slide__finale { margin-left: 10vw; }
  .k-slide__finale .k-slide__billboard { left: var(--kdb-space-base); }
}

/* ============================================================
   THE TROPHY SHELF — along the bottom, and the lift
   ------------------------------------------------------------
   Eight groupings plus the cabinet, in a row across the foot of
   the finale, arriving after the main plate has opened. Taking
   one RAISES it: the frame comes up from below to full bleed,
   over the cabinet, carrying the cup, the count and the name.

   The raise is the whole idea. These are trophies, and the one
   gesture every one of them has in common is being lifted — so
   the interaction is that gesture rather than a crossfade.
   --kdb-shelf-lift is deliberately slower than a draw: a lift has
   weight, and something heavy that moves at UI speed reads as
   weightless.

   THUMBS ARE MASKED IN THE MARK'S OWN CONTAINERS. Squares in a
   semi-transparent black box was interface furniture sitting on
   top of a photograph — the shape said "control panel" while
   everything around it said otherwise. KDB_CONTAINERS gives six
   organic containers derived from the mark's own curves; the
   shelf cycles tile / round / portrait so the row reads as a set
   of objects rather than a strip of buttons, and no scrim is
   needed because the shapes are doing the separating.

   THE CABINET GETS ITS OWN THUMBNAIL, first in the row. It is the
   answer to "how do you get back": making the way back a peer of
   the eight ways forward, rather than a close button or a second
   click on the active item, means there is one kind of control on
   this section and nothing to learn. It is also honest — the
   cabinet is a nineteenth thing to look at, not a null state.
   ============================================================ */

.k-trophies {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: 0;
  height: var(--k-shelf-band);
  z-index: 4;
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  /* Sat on the floor of the band, so every pixel of slack is above
     the row — which is the only direction a thumbnail ever moves.
     Centred, half the slack sat underneath doing nothing while the
     lift ran out of room at the top. */
  align-items: flex-end;
  padding-block-end: var(--kdb-space-snug);
  gap: var(--kdb-shelf-gap);
  max-width: calc(100% - (var(--kdb-space-base) * 2));
  /* Arrives with the figure, off the same scroll progress. */
  opacity: calc((var(--k-finale-open, 0) - 0.55) / 0.35);
}

.k-trophy {
  display: block;
  width: var(--kdb-shelf-thumb);
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  opacity: 0.62;
  transition: opacity var(--kdb-motion-tick) var(--kdb-motion-curve),
              transform var(--kdb-motion-draw) var(--kdb-motion-curve);
}
.k-trophy img.k-trophy__thumb {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  /* The organic container, set per item from KDB_CONTAINERS. */
  clip-path: var(--k-thumb-shape, none);
}
.k-trophy:hover,
.k-trophy:focus-visible { opacity: 1; transform: translateY(-4px); }
/* Selected sits raised, so the row shows which one is up. */
.k-trophy[aria-pressed="true"] {
  opacity: 1;
  transform: translateY(calc(var(--k-shelf-lift, 10px) * -1));
}

/* ---- the raised plate ------------------------------------- */

/* THE BOX HOLDS STILL AND CLIPS; THE PICTURE IS WHAT RISES.
   Lifting the box itself moved the whole frame down past the
   bottom of the panel at rest, where it lay across the shelf —
   only an opacity:0 on the other copy of this rule was hiding it,
   which is why the lift read as a fade appearing in place. With
   the travel on the image and overflow:hidden on the frame, the
   picture climbs up INTO the frame from underneath and is cut off
   cleanly by the frame's own edge. That is the gesture: a trophy
   coming up, not a photograph fading in. */
.k-trophy-plate {
  position: absolute;
  inset: 0 0 var(--k-shelf-band) 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}

/* HINGED AT THE BOTTOM EDGE. Before it arrives the panel is below
   the frame AND tipped away — the top pushed back into the screen,
   so its face is angled down and away from the viewer. Rising, it
   swings up on that hinge until it is square on.

   The origin is what makes it a hinge rather than a tumble: the
   pivot is where the panel would meet the floor, so the top edge
   travels further than the bottom and the panel is briefly shorter
   than full height. That foreshortening is the whole tell — take
   it away and this is a slide with a rotation bolted on. */
.k-trophy-plate img {
  /* Stacked, because two plates are in here whenever one is
     replacing another — the outgoing one is still travelling. */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform-origin: 50% 100%;
  transform: perspective(var(--kdb-shelf-depth))
             translateY(100%)
             rotateX(calc(-1 * var(--kdb-shelf-tip)));
  transition: transform var(--kdb-shelf-lift) var(--kdb-motion-curve);
  will-change: transform;
}
.k-trophy-plate[data-on] img {
  transform: perspective(var(--kdb-shelf-depth)) translateY(0) rotateX(0deg);
}

/* AND OUT THE TOP, ON THE HINGE AT THE OTHER END.

   THE PIVOT MOVES TO THE TOP EDGE, and that is the whole point.
   Keeping the entry's bottom-edge pivot for the exit is what made
   the departure read as a plain slide: on the way out the top
   leaves first, so the part still on screen is the bottom — which
   is sitting ON the pivot. Points at a hinge do not move relative
   to it, so the remnant the reader can actually see barely rotates
   and every bit of foreshortening happens above the frame line,
   out of sight. All that is left to watch is vertical travel.

   The rule is that the pivot belongs at the edge that leaves
   FIRST, so the remnant is the far end of the lever and swings
   through a real arc. Coming in, the panel rises off the floor and
   its top swings up. Going out, it lifts off the ceiling and its
   bottom swings up after it — the same gesture, the other end of
   the panel, the other end of the frame.

   Changing transform-origin costs nothing here, which is the part
   I got wrong the first time and blamed for a jump. The plate is
   at translateY(0) rotateX(0) — plain identity — at the instant it
   is marked leaving, and identity renders the same about any
   origin. There is nothing to teleport from.

   It leaves ON TOP of whatever is arriving. Passing behind would
   put the thing being taken away underneath the thing replacing
   it, and the exit is the half of the gesture the reader was
   promised.

   Both states are named, because `.k-trophy-plate[data-on] img`
   and `.k-trophy-plate img[data-leaving]` carry identical
   specificity: without the [data-on] variant spelled out, whether
   a departing plate left or snapped back would come down to which
   rule was written last. */
.k-trophy-plate img[data-leaving],
.k-trophy-plate[data-on] img[data-leaving] {
  z-index: 2;
  transform-origin: 50% 0%;
  transform: perspective(var(--kdb-shelf-depth))
             translateY(-100%)
             rotateX(calc(-1 * var(--kdb-shelf-tip)));
}

@media (prefers-reduced-motion: reduce) {
  .k-trophy-plate img { transition: none; transform: none; }
  .k-trophy-plate img[data-leaving] { display: none; }
}

/* The caption rides the plate up, one beat behind it, so the
   frame arrives and then says what it is. */
.k-trophy-caption {
  position: absolute;
  z-index: 3;
  /* CENTRED ON THE PICTURE. Not the corner: the cup, the count and
     the name are the subject once a trophy is up, and the plates
     they sit on are centre-weighted compositions. The lockup fills
     the frame the picture occupies — the shelf band is excluded, so
     nothing can drift down onto the thumbnails.

     The cup stacks ABOVE the type rather than beside it. Centred,
     a graphic hung off the left of a text block reads as a stray
     element; over the middle of a photograph it reads as a crest. */
  inset: 0 0 var(--k-shelf-band) 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: clamp(0.5rem, 1.2vw, 1rem);
  color: var(--kdb-text-inverse);
  pointer-events: none;
  opacity: 0;
  transform: translateY(24px);
  transition: opacity var(--kdb-motion-draw) var(--kdb-motion-curve),
              transform var(--kdb-motion-draw) var(--kdb-motion-curve);
}
.k-trophy-caption[data-on] {
  opacity: 1;
  transform: translateY(0);
  transition-delay: var(--kdb-shelf-lift);
}

/* Three arrivals in order, not one event with everything on it.
   The plate stands up, then the trophy lifts, then the words come
   up under it. The cup moves first and furthest because the cup is
   the thing being raised. */
.k-trophy-caption__cup,
.k-trophy-caption__text {
  opacity: 0;
  transition: opacity var(--kdb-motion-draw) var(--kdb-motion-curve),
              transform var(--kdb-motion-draw) var(--kdb-motion-curve);
}
.k-trophy-caption__cup { transform: translateY(34px); }
.k-trophy-caption__text { transform: translateY(20px); }
.k-trophy-caption[data-on] .k-trophy-caption__cup {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(var(--kdb-shelf-lift) + 80ms);
}
.k-trophy-caption[data-on] .k-trophy-caption__text {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(var(--kdb-shelf-lift) + 210ms);
}
@media (prefers-reduced-motion: reduce) {
  .k-trophy-caption__cup,
  .k-trophy-caption__text { transition: none; opacity: 1; transform: none; }
}

/* The cup itself. Flat ink-on-ground rather than the lab's
   gradient — D23 keeps the identity monochrome, and a gilded cup
   would be the only gradient in the system. */
.k-trophy-caption__cup { flex: 0 0 auto; width: clamp(38px, 4.4vw, 70px); }
.k-trophy-caption__cup svg { display: block; width: 100%; height: auto; }
.k-trophy-caption__cup path { fill: var(--kdb-text-inverse); }

.k-trophy-caption__text {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--kdb-space-hair);
}
.k-trophy-caption__count {
  font-variation-settings: "wdth" var(--kdb-slide-figure-wdth);
  font-weight: var(--kdb-wght-heavy);
  font-size: clamp(2.6rem, 6vw, 5rem);
  line-height: 0.9;
  letter-spacing: -0.03em;
}
.k-trophy-caption__name {
  font-variation-settings: "wdth" var(--kdb-wdth-head);
  font-weight: var(--kdb-wght-heavy);
  font-size: clamp(1rem, 1.9vw, 1.7rem);
  line-height: 1.05;
  letter-spacing: var(--kdb-track-head);
  text-transform: uppercase;
}
.k-trophy-caption__meta {
  font-size: var(--kdb-size-micro);
  font-weight: var(--kdb-wght-medium);
  letter-spacing: var(--kdb-track-caps-wide);
  text-transform: uppercase;
  color: var(--kdb-neutral-20);
}

/* While a trophy is up, the 19 steps aside.

   Keyed to the PLATE, not to aria-pressed. The 19 belongs to the
   cabinet frame — him sitting with the whole City haul — and to
   nothing else. Pressed-state meant it only stood down on a click,
   so every hover put a Champions League photograph under a
   nineteen-trophy headline that was not counting what was on
   screen. The plate is on whenever something is shown, previews
   included, which is exactly when the total stops being true of
   the picture. */
.k-slide__finale:has(.k-trophy-plate[data-on]) .k-slide__billboard > * {
  opacity: 0;
  transition: opacity var(--kdb-motion-draw) var(--kdb-motion-curve);
}
.k-slide__finale .k-slide__billboard > * {
  transition: opacity var(--kdb-motion-draw) var(--kdb-motion-curve);
}

@media (prefers-reduced-motion: reduce) {
  .k-trophy-plate,
  .k-trophy-caption,
  .k-trophy { transition: none; }
  .k-trophy-plate { transform: none; opacity: 0; }
  .k-trophy-plate[data-on] { opacity: 1; }
}

@media (max-width: 599px) {
  /* Nine thumbs across 390px is 36px each — too small to read as
     objects, which is the whole point of the organic masks. They
     keep a usable size and the shelf scrolls itself instead. */
  .k-trophies {
    gap: var(--kdb-space-tight);
    max-width: calc(100% - (var(--kdb-space-base) * 2));
    overflow-x: auto;
    scroll-snap-type: x proximity;
    padding-block-end: var(--kdb-space-hair);
    scrollbar-width: none;
  }
  .k-trophies::-webkit-scrollbar { display: none; }
  .k-trophies li { scroll-snap-align: center; flex: 0 0 auto; }
  .k-trophy { width: 52px; }
  /* bottom stays 0: the plate already reserves --k-shelf-band for
     the shelf, so zero puts it exactly in its own band rather than
     on the photograph. Read as a bug once and "fixed" — it was the
     system working. */
  .k-trophy-caption { left: var(--kdb-space-base); bottom: 7.5rem; }
}

/* ============================================================
   THE MOUSE TRAIL — after a trophy is chosen
   ------------------------------------------------------------
   Once a competition is selected, moving the pointer across the
   open plate drops frames from that competition where the pointer
   has been, and takes them away again a moment later.

   It is gated on SELECTION, which is the whole reason the shelf
   moved from hover to click: a trail that fired on any pointer
   movement would be constant noise on a section the reader is
   only passing through. Choosing a trophy is the reader saying
   they want to look, and the trail is what looking does.

   Frames spawn on distance travelled, not on time — a pointer
   held still drops nothing, and a fast sweep leaves an even
   ribbon rather than a clump. That is the difference between
   this and a particle effect.

   D21 does not cover it (ambient motion never resolves; this
   resolves and is entirely trigger-driven) and neither does D24.
   It is the same interactive, pointer-following category the
   hero's parallax was filed under in D26 and still unnamed.
   ============================================================ */

.k-trail {
  position: absolute;
  inset: 0 0 var(--k-shelf-band) 0;
  z-index: 2;
  overflow: hidden;
  pointer-events: none;
}

.k-trail__frame {
  position: absolute;
  width: var(--kdb-trail-w);
  /* A KNOWN height, at the mark's own 5:3. With height:auto a frame
     had no height until its image decoded, so there was nothing to
     hold it inside the field with — and frames dropped near an edge
     were half outside it, sliced by the trail's own overflow. Fixed
     ratio plus object-fit:cover crops the photograph rather than
     distorting it, and the frames now read as one set. */
  aspect-ratio: 5 / 3;
  height: auto;
  /* Centred on where the pointer was, not hung off it. */
  translate: -50% -50%;
  object-fit: cover;
  opacity: 0;
  scale: 0.86;
  will-change: opacity, scale;
  animation: k-trail-life var(--kdb-trail-life) var(--kdb-motion-curve) forwards;
}

@keyframes k-trail-life {
  0%   { opacity: 0; scale: 0.86; }
  14%  { opacity: 1; scale: 1; }
  62%  { opacity: 1; scale: 1; }
  100% { opacity: 0; scale: 1.04; }
}

/* Reduced motion removes the trail outright. There is no content
   in it — every frame it drops is already in the shelf — so
   unlike a reveal there is nothing to preserve by showing it
   without the movement. */
@media (prefers-reduced-motion: reduce) {
  .k-trail { display: none; }
}

/* ============================================================
   HERO — SOLO PLATE (a trial, alongside the diptych)
   ------------------------------------------------------------
   One photograph, the whole screen, a narrow margin. The diptych
   (D26, promoted from the hero lab) is NOT deleted — its rules
   are still above and index.html can be swapped back with one
   block. This is a trial run at real size, which is the only way
   a hero can honestly be judged.

   Why this frame earns it: a lone figure in a field of mown
   stripe, subject low and right, everything else empty. That is
   DIRECTION's own argument made literally — "the subject is the
   gap, not the ball", "the empty part of the page is the
   product". A hero that is mostly grass is the thesis, not a
   compromise.

   Furniture goes inverse because the frame is dark throughout.
   ============================================================ */

/* ONE reference frame. The image, the registration corners and the
   nav all measured differently before — the frame was inset 28px,
   the corners 24px from the SECTION, and the bar 57.6px from the
   viewport with its mark floating in a band above all of it. Three
   margins that never agreed, which is why it read as accidental.

   The apparatus now sits OUTSIDE the image, in the margin around
   it: the bar's band is the top margin, and the corners register
   the image's own trim. The frame's left edge and the mark's left
   edge are the same line, because they are the same token. */
.k-hero--solo {
  position: relative;
  margin-inline: calc(50% - 50vw);
  height: 100vh;
  height: 100dvh;
  /* Top margin IS the nav's band, so the bar has somewhere to be
     that is not on top of the picture. */
  padding: var(--kdb-nav-height) var(--kdb-page-margin) var(--kdb-page-margin);
  background: var(--kdb-surface-ground);
}

.k-hero--solo .k-hero__frame {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
.k-hero--solo .k-hero__frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The subject sits low and right; a centre crop on a tall
     window walks him off the bottom edge. */
  object-position: 62% 68%;
  display: block;
}

/* Head and foot sit ON the frame, in the empty half of it. */
.k-hero--solo .k-hero__head,
.k-hero--solo .k-hero__foot {
  position: absolute;
  z-index: 2;
  left: calc(var(--kdb-page-margin) + clamp(1rem, 3vw, 3rem));
  right: calc(var(--kdb-page-margin) + clamp(1rem, 3vw, 3rem));
  display: flex;
  align-items: center;
  color: var(--kdb-text-inverse);
  font-size: var(--kdb-size-label);
  font-weight: var(--kdb-wght-medium);
  letter-spacing: var(--kdb-track-caps);
  text-transform: uppercase;
}
.k-hero--solo .k-hero__head {
  top: calc(var(--kdb-nav-height) + var(--kdb-space-base));
  justify-content: flex-end;
}
.k-hero--solo .k-hero__foot {
  bottom: calc(var(--kdb-page-margin) + clamp(1rem, 3vw, 3rem));
}
.k-hero--solo .k-hero__nums { display: flex; gap: var(--kdb-space-base); }
.k-hero--solo .k-hero__head b,
.k-hero--solo .k-hero__foot b {
  font-variation-settings: "wdth" var(--kdb-wdth-head);
  font-weight: var(--kdb-wght-heavy);
  color: var(--kdb-text-inverse);
}
.k-hero--solo .k-hero__stamp { display: inline-flex; align-items: center; gap: var(--kdb-space-tight); }
.k-hero--solo .k-hero__stamp-div {
  align-self: stretch;
  width: var(--kdb-rule-hair);
  background: var(--kdb-neutral-40);
}

/* Registration corners, inverse, on the frame's own edges. */
/* The corners register the IMAGE's trim — the box is the image's
   box, and each cross is centred on a corner of it, so they read
   as marking the frame rather than as decoration floating near it. */
.k-hero--solo .k-hero__reg {
  position: absolute;
  inset: var(--kdb-nav-height) var(--kdb-page-margin) var(--kdb-page-margin);
  pointer-events: none;
  z-index: 2;
}
.k-hero--solo .k-hero__reg i {
  position: absolute;
  width: 11px;
  height: 11px;
  transform: translate(-50%, -50%);
}
.k-hero--solo .k-hero__reg i::before,
.k-hero--solo .k-hero__reg i::after { content: ""; position: absolute; background: var(--kdb-neutral-20); }
.k-hero--solo .k-hero__reg i::before { left: 50%; top: 0; bottom: 0; width: var(--kdb-rule-hair); }
.k-hero--solo .k-hero__reg i::after { top: 50%; left: 0; right: 0; height: var(--kdb-rule-hair); }
.k-hero--solo .k-hero__reg i:nth-child(1) { left: 0; top: 0; }
.k-hero--solo .k-hero__reg i:nth-child(2) { left: 100%; top: 0; }
.k-hero--solo .k-hero__reg i:nth-child(3) { left: 0; top: 100%; }
.k-hero--solo .k-hero__reg i:nth-child(4) { left: 100%; top: 100%; }

/* The nav inverts while a dark hero is under it. This is the
   cost D18's reversal left owed, paid for the one case that
   creates it — and paid with an OBSERVER, which is the mechanism
   D20 already sanctions, not a scroll handler. */
.k-nav[data-over-dark] { color: var(--kdb-text-inverse); }
.k-nav[data-over-dark] .k-nav__mark svg path:first-child { fill: var(--kdb-text-inverse); }
.k-nav[data-over-dark] .k-nav__mark svg path:not(:first-child) { fill: var(--kdb-neutral-95); }

/* ============================================================
   THE FEED — beat 10, socials
   ------------------------------------------------------------
   His own posts, newest first, with his own captions.

   Called what it is. "Stay close" was a call to action for a thing
   that is not an action; "Peripheral vision" was the overcorrection
   — a clever name for a section nobody looks for under a clever
   name. A reader scanning for his feed scans for the word everyone
   uses, and the writing that earns its keep here is HIS, in the
   captions, not ours in the heading.

   Deliberately quiet. Beat 04 is a pinned sideways slide and beat
   07 is a full-viewport wall; this one is eight frames in a row on
   the page's own grid, because a site cannot be all crescendo and
   this beat is the come-down before the footer.

   Captions are verbatim and first-line only. "Off the map",
   "Recharge complete." — nothing here needs writing for him, and
   that is the whole argument for building the beat from the real
   feed rather than stubbing it.
   ============================================================ */

.k-feed {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--kdb-feed-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (max-width: 919px) { .k-feed { grid-template-columns: repeat(2, 1fr); } }

.k-feed__item { margin: 0; min-width: 0; }

.k-feed__link {
  display: block;
  text-decoration: none;
  color: inherit;
}
.k-feed__frame {
  display: block;
  overflow: hidden;
  background: var(--kdb-surface-sunken);
}
.k-feed__frame img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  /* Nothing scales on hover. The frames are square and adjacent,
     so a scale would push into its neighbours; the caption below
     is what answers the pointer instead. */
}

.k-feed__meta {
  display: flex;
  flex-direction: column;
  gap: var(--kdb-space-hair);
  margin-block-start: var(--kdb-media-caption-gap);
}
.k-feed__date {
  font-size: var(--kdb-size-micro);
  font-weight: var(--kdb-wght-medium);
  letter-spacing: var(--kdb-track-caps-wide);
  text-transform: uppercase;
  color: var(--kdb-text-muted);
}
.k-feed__caption {
  font-size: var(--kdb-size-caption);
  line-height: var(--kdb-leading-tight);
  color: var(--kdb-text-secondary);
  /* Two lines, then stop. A feed caption that runs long is not
     more information, it is a different beat. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.k-feed__link:hover .k-feed__caption,
.k-feed__link:focus-visible .k-feed__caption { color: var(--kdb-text-primary); }

/* ============================================================
   THE FOOTER — large type, on a grid, on the dark ground
   ------------------------------------------------------------
   Audited against footer.design, whose own taxonomy is the most
   useful thing on it: Typographic · Small Type · Illustrative ·
   Grid · Flat · Animated · Cards · Bright · Dark · Large Type ·
   Bold. That is the whole design space, and three of those
   eleven are already this system's positions rather than
   choices to make.

   TAKEN — Large Type, Grid, Dark.
     · The surname at lockup scale, cropped by the bottom edge.
       This is the device the gallery repeats most, and it is the
       one this site was already built for: --kdb-size-lockup is
       the largest step on D1's ramp and was used NOWHERE. A ramp
       step that nothing uses is either wrong or waiting for its
       moment; this is the moment.
     · A real column grid for the links, on D5d's sixteen, rather
       than the single stack the footer had.
     · Dark is not a style pick here — D19 already made the footer
       the one dark surface, for the reason that it is the end of
       the page rather than a surface behind content.

   REJECTED — Illustrative and Cards (no illustration in this
   system, and D14's addendum keeps chrome rectilinear, so a wall
   of cards at the quietest moment is the wellness-brand failure),
   Animated (the footer is the come-down; a moving one is
   decoration where attention is already leaving), Bright
   (contradicts D19 outright).

   The crop is what makes it ours rather than a gallery pastiche:
   the name runs off the bottom, so the page ends mid-letter. It
   is the same argument as the mark — the counter is the figure,
   and what is missing does the work.
   ============================================================ */

.k-footer--large {
  position: relative;
  overflow: hidden;
  padding-block: var(--kdb-space-group) 0;
  /* The footer sits OUTSIDE .k-page, so it inherits none of the
     page's margin — the mark and the link columns were running to
     the viewport edge while everything above them started at the
     margin. Every left edge on the page should be the same left
     edge. */
  padding-inline: var(--kdb-page-margin);
}
/* The wordmark still breaks out to the full width, and pays back
   the padding it was just given. */
.k-footer--large .k-footer__word { margin-inline: calc(-1 * var(--kdb-page-margin)); }

.k-footer__lead {
  max-width: 22ch;
  color: var(--kdb-footer-ink-strong);
}

/* The link grid. Three columns from the annotation margin, so it
   sits on the same left edge as everything above it. */
.k-footer__cols {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--kdb-space-loose) var(--kdb-page-gutter);
}
@media (max-width: 599px) { .k-footer__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.k-footer__colhead {
  font-size: var(--kdb-size-micro);
  font-weight: var(--kdb-wght-medium);
  letter-spacing: var(--kdb-track-caps-wide);
  text-transform: uppercase;
  color: var(--kdb-neutral-60);
  margin-block-end: var(--kdb-space-snug);
}

/* The wordmark. Cropped by the bottom edge — only the top third
   of it is on the page, and the rest is off. */
.k-footer__word {
  display: block;
  margin-block-start: var(--kdb-space-group);
  padding-inline: var(--kdb-page-margin);
  /* The crop: the element keeps a fraction of the line's own
     height and the glyphs overflow past it. */
  height: calc(var(--kdb-size-lockup) * var(--kdb-footer-word-crop));
  overflow: hidden;
}
.k-footer__word span {
  /* inline-block, so the box is the TEXT's width. As a block it
     was the container's width, the fit measured the box against
     itself, and the scale came out 1 every time — the word never
     grew to fill and sat two thirds across looking almost right. */
  display: inline-block;
  font-family: var(--kdb-family-archivo);
  font-variation-settings: "wdth" var(--kdb-wdth-lockup);
  font-weight: var(--kdb-wght-heavy);
  font-size: var(--kdb-size-lockup);
  line-height: 1;
  letter-spacing: var(--kdb-track-lockup);
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--kdb-footer-ink-strong);
  /* Fit the width of the page exactly, whatever the viewport —
     a wordmark that wraps or clips at the right is not a
     wordmark, it is an accident. */
  transform-origin: left top;
}

.k-footer__legal-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--kdb-space-snug);
  padding-block-start: var(--kdb-space-base);
}

/* ============================================================
   THE JERSEY WALL — beat 07
   ------------------------------------------------------------
   Six kits, each in front of the ground it was worn at. Take a
   kit and the stadium changes. Recovered from v2's .kdb-jerseys.

   Both halves work because of what the assets already are: the
   kits are real cut-outs (1122x1402 RGBA), so they hang rather
   than slide — D22's globe note is the counter-example. The
   grounds are the graphite contour drawings DIRECTION names as
   its line-discipline target, but drawn on cream paper, which is
   on the never-list; mix-blend-mode: multiply drops the paper and
   keeps only the ink, so the drawing sits on THIS system's cool
   ground rather than importing its own.
   ============================================================ */

.k-wall {
  position: relative;
  overflow: hidden;
  margin-inline: calc(50% - 50vw);
  /* A definite height: container-type needs one, and with only a
     minimum every cqh resolved to zero and the kits collapsed to
     the floor of their own clamp. */
  height: 100vh;
  height: 100dvh;
  container-type: size;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  overflow: hidden;
  padding-block: var(--kdb-nav-height) var(--kdb-space-block);
  padding-inline: clamp(1.25rem, 4vw, 3rem);
  background: var(--kdb-surface-ground);
}

.k-wall__grounds { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
.k-wall__grounds img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 72%;
  display: block;
  opacity: 0;
  mix-blend-mode: multiply;
  transition: opacity var(--kdb-wall-swap) var(--kdb-motion-curve);
}
.k-wall__grounds img.is-on { opacity: var(--kdb-wall-ground-ink); }

/* THE PLAYER BEHIND THE SHIRT.
   The wall is his kits; hovering one puts him in it. The cut-out
   stands on the ground drawing, BEHIND the rail, so the shirt you
   are pointing at hangs in front of the man wearing it rather than
   next to a picture of him.

   Anchored to the bottom and centred, because these are
   rotoscoped standing figures cut at the same crop — aligning
   them on the floor is what makes swapping one for another read
   as the same person changing clubs.

   The files are 3-6MB each and there are six, so src is set on
   first activation rather than at render: nobody who scrolls past
   this section pays 27MB for it. */
/* HE FOLLOWS THE POINTER.
   The follow is on the LAYER and the arrival is on the picture
   inside it. One element cannot hold both: the rise needs a long
   ease so a club change reads as him stepping in, and the follow
   needs a short one so he does not lag half a second behind the
   cursor. Two elements, two clocks.

   Nothing clips here any more. `overflow: hidden` on this layer
   would have moved its own clip box along with him, so the mask
   he was being cut against would drift with the thing it was
   masking. The section clips instead — it does not move. */
.k-wall__players {
  position: absolute;
  /* Not `inset: 0`. The bottom stops at the shirts' HEM LINE so he
     stands where they hang rather than on top of the caption, and
     the top clears the nav.

     --k-wall-floor is measured, not guessed. Every fixed value
     tried here drifted: cqh resolves against the container's
     CONTENT box, and .k-wall carries block padding, so a reserve
     that cleared the caption at one viewport height sat on it at
     another. The rail knows where its own hem is; build.js asks it
     and writes the answer here. The token is the fallback for the
     frame before that measurement lands.

     The follow amplitude is added on top, so the downward half of
     the throw cannot walk him back onto the type the floor was
     measured to avoid. */
  inset: var(--kdb-nav-height) 0
         calc(var(--k-wall-floor, var(--kdb-wall-player-floor)) + var(--kdb-wall-follow-y)) 0;
  z-index: 0;
  pointer-events: none;
  display: grid;
  place-items: end center;
  /* JS reports WHERE the pointer is, as a signed fraction; CSS
     owns HOW FAR that throws him. Multiplying in JS meant reading
     the amplitude token back out of computed style, and a custom
     property does not resolve its units for script — `9vw` came
     back as the number 9, which JS then used as nine pixels. The
     calc keeps the token in the one language that understands it. */
  transform: translate3d(
    calc(var(--k-wall-fx, 0) * var(--kdb-wall-follow-x)),
    calc(var(--k-wall-fy, 0) * var(--kdb-wall-follow-y)),
    0);
  transition: transform var(--kdb-wall-follow-ease) var(--kdb-motion-curve);
  will-change: transform;
}
.k-wall__players img {
  grid-area: 1 / 1;
  height: var(--kdb-wall-player-h);
  /* Never taller than the band he is standing in, however the
     clamp lands on a short viewport. */
  max-height: 100%;
  width: auto;
  max-width: none;
  opacity: 0;
  transform: translateY(var(--kdb-wall-player-rise));
  transition: opacity var(--kdb-motion-draw) var(--kdb-motion-curve),
              transform var(--kdb-motion-sweep) var(--kdb-motion-curve);
}
.k-wall__players img.is-on { opacity: 1; transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .k-wall__players { transform: none; transition: none; }
  .k-wall__players img { transition: opacity var(--kdb-motion-tick) linear; transform: none; }
}

.k-wall__rail {
  position: relative;
  z-index: 1;

  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: var(--kdb-wall-jersey-gap);
  margin: 0 0 var(--kdb-space-loose);
  padding: 0;
  list-style: none;
  max-width: 100%;
}
.k-wall__item { display: flex; }

/* The kits hang, and the hung one turns to face you.

   The tilt in the row is NOT a 3D effect — it is D21's ambient
   drift, a 2D rotate of at most 1.5deg with each kit's delay taken
   from its index, so they never move in step. That is why they sit
   at slightly different angles: they are hanging, not aligned.

   What IS 3D is the turn on selection: the chosen kit rotates on
   its Y axis from a few degrees off-square to face the viewer
   head-on, on the rail's perspective. It is the same device as the
   trophy lift and needs no WebGL — a canvas here would mean
   re-rendering six cut-outs every frame to buy a rotation the
   compositor already does for free. */
.k-wall__kit {
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  display: block;
  opacity: var(--kdb-wall-dim);
  transition: opacity var(--kdb-motion-draw) var(--kdb-motion-curve);
  animation-delay: var(--k-i, 0s);
  /* The turn lives on the image, not here: k-drift owns this
     element's transform, and one property cannot hold both. */
  perspective: var(--kdb-wall-depth);
  perspective-origin: 50% 50%;
}
.k-wall__kit img {
  display: block;
  height: var(--kdb-wall-jersey-h);
  width: auto;
  transform-origin: 50% 60%;
  transform: rotateY(var(--kdb-wall-turn));
  transition: transform var(--kdb-motion-sweep) var(--kdb-motion-curve);
}
.k-wall__kit[aria-pressed="true"] img { transform: rotateY(0deg); }
.k-wall__kit[aria-pressed="true"],
.k-wall__kit:hover,
.k-wall__kit:focus-visible { opacity: 1; }

.k-wall__cap {
  position: relative;
  z-index: 2;
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--kdb-space-tight);
  text-align: center;
  min-height: 7rem;
}
.k-wall__cap > b {
  font-variation-settings: "wdth" var(--kdb-wdth-head);
  font-weight: var(--kdb-wght-heavy);
  font-size: var(--kdb-size-head);
  line-height: var(--kdb-leading-head);
  letter-spacing: var(--kdb-track-head);
  color: var(--kdb-text-primary);
}
/* Years, sentence case and not shouted. A run of small caps under
   a headline is the eyebrow's habit wearing a different hat. */
.k-wall__cap .k-wall__years {
  font-size: var(--kdb-size-small);
  color: var(--kdb-text-muted);
  letter-spacing: var(--kdb-track-body);
  text-transform: none;
}

/* The numbers are figures, so they are set as figures. A line of
   micro caps reading "73 APPS · 20 GOALS · 37 ASSISTS" is the
   weakest possible treatment of the only hard data in the beat —
   it is the record, and the record is what this wall is for. */
.k-wall__nums {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--kdb-space-snug) var(--kdb-space-loose);
  margin-block-start: var(--kdb-space-tight);
}
.k-wall__num {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.k-wall__num b {
  /* Tabular, or every tick of the count-up changes the figure's
     width and the whole row shuffles sideways while it runs. */
  font-variant-numeric: tabular-nums;
  font-variation-settings: "wdth" var(--kdb-wdth-figure);
  font-weight: var(--kdb-wght-heavy);
  font-size: clamp(1.5rem, 2.6vw, 2.25rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--kdb-text-primary);
}
.k-wall__num span {
  font-size: var(--kdb-size-micro);
  font-weight: var(--kdb-wght-medium);
  letter-spacing: var(--kdb-track-caps-wide);
  text-transform: uppercase;
  color: var(--kdb-text-muted);
}

/* The gap note is a footnote, not a third line of the caption. */
.k-wall .k-empty {
  position: relative;
  z-index: 1;
  margin-block-start: var(--kdb-space-base);
  max-width: 52ch;
  text-align: center;
}

@media (max-width: 599px) {
  .k-wall__rail {
    justify-content: flex-start;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    padding-inline: var(--kdb-space-snug);
    scrollbar-width: thin;
    scrollbar-color: var(--kdb-neutral-40) var(--kdb-border-faint);
  }
  .k-wall__item { scroll-snap-align: center; }
  .k-wall__grounds img { object-position: center 62%; }
}

/* ============================================================
   PARTNERSHIPS — beat 06
   ------------------------------------------------------------
   The marks are DISTRIBUTED ACROSS THE WIDTH at rest — a row of
   names holding the full measure, in ink. Only when one is taken
   do they collect into a left column and hand the rest of the
   width to the media.

   That is the correction that mattered: opening with a narrow
   left-hand list and a large empty space beside it announced a
   viewer before there was anything to view, and read as a layout
   waiting to be filled rather than a wall of partners.

   THE MARKS ARE STAND-INS — no partner logo file exists in this
   repository, so each is its own name set in Archivo. That is
   recorded HERE and in data/partners.json, not printed on the
   page: a caveat for whoever builds this is not a caption for
   whoever reads it.
   ============================================================ */

.k-partners-split {
  /* ONE HEIGHT, OPEN OR SHUT. The player used to grow the section
     out of nothing, so choosing a partner shoved everything below
     it down the page — the wall you were choosing from moved under
     the cursor mid-click. The room is reserved from the start; at
     rest the marks simply have air under them, which is also what
     gives the resting motion somewhere to happen. */
  --k-partners-h: min(82vh, 900px);
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--kdb-space-loose);
  min-height: var(--k-partners-h);
  /* STRETCH, so the ledger can spread into the height the section
     reserves. At `start` its own height: 100% had nothing definite
     to resolve against and it fell back to content — the rows came
     to 537 of 738 and the rest stayed air. */
  align-items: stretch;
  transition: grid-template-columns var(--kdb-player-open) var(--kdb-motion-curve);
}
/* CLOSED, THE TWO SHARE ONE CELL.
   The player is invisible and inert when shut, but it was still a
   grid row of its own — a full --k-partners-h of nothing sitting
   under the list, which is most of what made the beat read as a
   small thing marooned in white. Stacked in the same cell it
   costs no height at all, and opening moves it into a column of
   its own. */
.k-partners-split > * { grid-area: 1 / 1; }
.k-partners-split[data-open] { grid-template-columns: 4fr 12fr; }
.k-partners-split[data-open] > .k-player { grid-area: 1 / 2; }
@media (max-width: 919px) {
  .k-partners-split[data-open] { grid-template-columns: 1fr; }
}

/* Narrow: the track moves faster than the finger, so a fifteen-
   screen run costs five viewport-heights rather than eight. Set
   as a token so the pace is adjustable without touching the
   driver — see --kdb-slide-pace. */
@media (max-width: 599px) { .k-slide { --kdb-slide-pace: 0.62; } }

/* AT REST: A TICKER, TILTED, TWO ROWS AGAINST EACH OTHER.
   Open: a plain column at the left.

   A vertical list of names is a directory — and the indices that
   gave it structure were structure it should not have needed. The
   wall now moves, and the movement is the resting state rather
   than an opacity cycle bolted onto something static.

   The clip is on the OUTER element and the tilt on the band
   inside it, so the marks are cut off at the viewport's own
   vertical edges rather than along the angle. Tilting the element
   that also clips would have put two diagonal cuts across the
   section, which is a shape, not a bleed. */
.k-ticker {
  /* Out of the page container and across the full viewport. A
     ticker that stops at the gutter is a list that happens to
     slide. */
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  overflow: hidden;
  /* Room for the corners the tilt throws above and below. */
  padding-block: clamp(1.5rem, 5vw, 5rem);
  display: flex;
  align-items: center;
}
.k-ticker__band {
  width: 100%;
  rotate: var(--kdb-ticker-tilt);
  display: flex;
  flex-direction: column;
  gap: var(--kdb-ticker-row-gap);
}
.k-ticker__row { overflow: visible; }

/* The track holds the real marks and the ghost copy. Moving it by
   exactly -50% lands the ghost where the original began, so the
   loop closes on itself with no seam to hide. */
.k-ticker__track {
  display: flex;
  width: max-content;
  animation: k-ticker-run var(--kdb-ticker-pace) linear infinite;
}
.k-ticker__row[data-row="1"] .k-ticker__track {
  animation-direction: reverse;
  /* Offset so the two rows are never showing the same phase. */
  animation-delay: calc(var(--kdb-ticker-pace) * -0.37);
}
@keyframes k-ticker-run {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
/* Pointing at the wall stops it. Reading a name should not be a
   timing exercise. */
.k-ticker:hover .k-ticker__track,
.k-ticker:focus-within .k-ticker__track { animation-play-state: paused; }

.k-partners {
  display: flex;
  align-items: baseline;
  gap: var(--kdb-ticker-gap);
  padding: 0 0 0 var(--kdb-ticker-gap);
  margin: 0;
  list-style: none;
  white-space: nowrap;
}

/* ---- OPEN: the ticker becomes the list again ----------------
   A plain column is the right thing next to media — it is being
   read down, not scanned across, and nothing beside a picture
   should be moving. So the tilt goes, the run stops, the ghost
   copy leaves, and the two rows stack into one column. */
.k-partners-split[data-open] .k-ticker {
  width: auto;
  margin-inline: 0;
  padding-block: 0;
  overflow: visible;
  align-items: start;
}
.k-partners-split[data-open] .k-ticker__band {
  rotate: none;
  gap: 0;
}
.k-partners-split[data-open] .k-ticker__track {
  animation: none;
  width: auto;
  transform: none;
}
.k-partners-split[data-open] .k-ticker__ghost { display: none; }
.k-partners-split[data-open] .k-partners {
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  padding: 0;
  white-space: normal;
}
.k-partners-split[data-open] .k-partner {
  font-size: var(--kdb-partner-mark-size-open);
  padding-block: calc(var(--kdb-partner-row-pad) * 0.5);
}

@media (prefers-reduced-motion: reduce) {
  /* Nothing runs and nothing tilts: the wall is a wrapped set of
     names, which is what a ticker is once you take the motion out
     of it. */
  .k-ticker { overflow: visible; width: auto; margin-inline: 0; padding-block: 0; }
  .k-ticker__band { rotate: none; }
  .k-ticker__track { animation: none; width: auto; }
  .k-ticker__ghost { display: none; }
  .k-partners { flex-wrap: wrap; white-space: normal; padding-left: 0; }
}

.k-partner {
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  text-align: left;
  display: block;
  font-family: var(--kdb-family-archivo);
  font-variation-settings: "wdth" var(--kdb-wdth-head);
  font-weight: var(--kdb-wght-heavy);
  font-size: var(--kdb-partner-mark-size);
  line-height: 1.1;
  letter-spacing: var(--kdb-track-head);
  text-transform: uppercase;
  /* Ink. A wall of partners is not secondary information, and a
     grey one reads as disabled before it reads as quiet. */
  color: var(--kdb-text-primary);
  transition: color var(--kdb-motion-tick) var(--kdb-motion-curve),
              font-size var(--kdb-player-open) var(--kdb-motion-curve);
}
/* NO BREATHE, NO LIFT.
   Both resting cycles are gone, and so are the tokens behind them.
   They existed to stop a static list of names reading as a
   caption; the ticker is motion already, and layering an opacity
   cycle and a bob on top of a moving wall is three things
   happening to one word. Hover is ink, and that is the only state
   here that means anything. */

/* THE MOVE TO THE SIDE. The column change itself cannot be
   animated — flex-direction snaps — so the marks carry the move:
   each one settles in from the right on its own beat, and the
   reflow reads as the set standing up rather than as a jump. */
@keyframes k-partner-settle {
  from { opacity: 0; transform: translateX(1.25rem); }
  to   { opacity: 1; transform: none; }
}
.k-partners-split[data-open] .k-partners li {
  animation: k-partner-settle var(--kdb-player-open) var(--kdb-motion-curve) both;
  animation-delay: calc(var(--i, 0) * 45ms);
}

/* Only once something is chosen does the rest step back. Holding
   still is handled where the ticker stops: a set you are reading
   through moves, a set you have chosen from does not. */
.k-partners-split[data-open] .k-partner { color: var(--kdb-text-muted); }
.k-partners-split[data-open] .k-partner[aria-pressed="true"],
.k-partner:hover,
.k-partner:focus-visible { color: var(--kdb-text-primary); }

@media (prefers-reduced-motion: reduce) {
  .k-partners-split[data-open] .k-partners li { animation: none; opacity: 1; transform: none; }
}

/* ---- the viewer -------------------------------------------
   On the page's own ground, not in a black box with pill
   buttons. The media takes the mark's container (D14's one
   earned crop for this beat) and everything around it is set in
   the register the rest of the site already uses: a micro-caps
   index, a caption beneath, and controls that are words rather
   than widgets. */

.k-player {
  position: relative;
  display: grid;
  /* One row, and a DEFINITE height for it.
     `1fr` in an auto-height grid resolved to nothing once the bar
     came out of flow — the stage measured 0 tall and the frames
     hung out of a collapsed row, which is why their stagger had
     nothing to stagger against. The section already reserves
     --k-partners-h whether this is open or shut; the stage may as
     well be told what that is.

     NO max-height, AND NO overflow. The open used to be a
     max-height reveal, which requires `overflow: hidden` — and
     that clipped the one thing this panel exists to do, run past
     the right gutter. The height is reserved either way, so there
     is nothing to reveal: the panel is always the same size and
     simply arrives. Which also means the bleed is a bleed. */
  grid-template-rows: minmax(0, 1fr);
  height: var(--k-partners-h);
  opacity: 0;
  transform: translateY(var(--kdb-space-base));
  pointer-events: none;
  transition: opacity var(--kdb-player-open) var(--kdb-motion-curve),
              transform var(--kdb-player-open) var(--kdb-motion-curve);
}
.k-player[data-open] { opacity: 1; transform: none; pointer-events: auto; }

/* NO BAR. The chosen mark is already named, in ink, in the ledger
   to the left of this — printing it again a few hundred pixels to
   the right was the weakest type on the page restating the
   strongest. What is left of the chrome is one close mark, and it
   rides the composition rather than sitting on a rule above it:
   a hairline with a glyph on it is modal furniture, and this is
   not a modal.

   The name stays in the DOM for aria-labelledby — the region still
   has to say what it is, it just does not have to say it twice on
   screen. */
.k-player__bar {
  position: absolute;
  z-index: 2;
  inset-block-start: 0;
  inset-inline-end: 0;
}
.k-player__count {
  font-size: var(--kdb-size-micro);
  font-weight: var(--kdb-wght-medium);
  letter-spacing: var(--kdb-track-caps-wide);
  color: var(--kdb-text-muted);
}

/* A SPREAD, NOT A GRID.
   Every frame the partner has, laid out at once — two is the most
   any of them carries. They used to be 1fr and 1fr with a gap: two
   identical rectangles at the same height, which is the most inert
   arrangement two pictures can be in.

   Now one leads and one follows. The lead is wider and sits low,
   the follower is narrower and rides up, and they overlap rather
   than clearing each other. Unequal sizes give the pair a reading
   order, the stagger gives it a diagonal, and the overlap is what
   makes it a composition instead of a row.

   It runs past the right gutter on purpose. Stopping at the page
   margin was most of what made this feel like a panel dropped on
   the page rather than part of it. */
.k-player__stage {
  position: relative;
  min-height: 0;
  height: 100%;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: var(--kdb-frame-lead) var(--kdb-frame-follow);
  /* NOT gap. `gap` rejects negative values outright, so the lap
     was dropped without complaint and the frames merely sat
     against each other — measured 1px apart where 36 were meant.
     A negative margin on the follower is the overlap. */
  align-items: stretch;
  margin-inline-end: calc(-1 * var(--kdb-page-margin));
}
/* min-height: 0 IS LOAD-BEARING.
   Grid items default to min-height: auto, so a frame could not be
   shorter than the picture inside it. With a portrait source
   (1080x1920) filling an 846px column, that floor was 1324px in a
   553px stage — the frame ignored its own height, hung out of the
   bottom, and `overflow: hidden` sliced it. Everything that looked
   like "the media is too low, cropped, and doesn't fit" was this
   one declaration missing. */
.k-player__frame {
  position: relative;
  min-width: 0;
  min-height: 0;
  margin: 0;
  align-self: end;
  height: var(--kdb-frame-h-lead);
}
/* Every frame after the first is shorter, rides up, and sits in
   front. Different heights as well as different widths — matching
   heights at different widths reads as a cropping accident. */
.k-player__frame + .k-player__frame {
  align-self: start;
  height: var(--kdb-frame-h-follow);
  margin-block-start: var(--kdb-frame-stagger);
  margin-inline-start: var(--kdb-frame-lap);
  z-index: 1;
}
.k-player__stage img,
.k-player__stage video {
  width: 100%;
  height: 100%;
  min-height: 0;
  display: block;
  object-fit: cover;
  clip-path: var(--k-shape, none);
}

/* A LONE FRAME KEEPS ITS OWN SHAPE.
   The spread's proportions are for two pictures; forcing a single
   one into the lead column made a 9:16 still into a letterbox and
   `cover` threw away most of the frame — the UFL entry showed a
   wall and a corner of a picture where a person was meant to be.

   Alone, the frame takes the full height and lets its width follow
   the source, pinned to the bleed edge. Nothing is cropped, and a
   portrait reads as a tall panel rather than a bad crop of a wide
   one. */
.k-player__stage[data-frames="1"] {
  grid-auto-columns: auto;
  justify-content: end;
}
.k-player__stage[data-frames="1"] .k-player__frame {
  height: 100%;
  width: auto;
  max-width: 100%;
  align-self: stretch;
}
.k-player__stage[data-frames="1"] img,
.k-player__stage[data-frames="1"] video {
  width: auto;
  max-width: 100%;
  object-fit: contain;
}
/* The frame's own index, in the same register as the ledger's.
   It sits on the picture, not beside it — a caption strip under
   two overlapping frames would have to pick one to belong to. */
.k-player__n {
  position: absolute;
  z-index: 2;
  inset-block-start: var(--kdb-space-snug);
  inset-inline-start: var(--kdb-space-snug);
  font-variation-settings: "wdth" var(--kdb-wdth-figure);
  font-weight: var(--kdb-wght-heavy);
  font-variant-numeric: tabular-nums;
  font-size: var(--kdb-size-small);
  letter-spacing: var(--kdb-track-body);
  color: var(--kdb-text-inverse);
  mix-blend-mode: difference;
  pointer-events: none;
}

@media (max-width: 919px) {
  /* Narrow: the stagger and the overlap both need width to read,
     and neither survives a single column. Stacked, plainly. */
  .k-player__stage {
    grid-auto-flow: row;
    grid-auto-columns: auto;
    gap: var(--kdb-space-snug);
    margin-inline-end: 0;
  }
  .k-player__frame,
  .k-player__frame + .k-player__frame {
    align-self: stretch;
    height: auto;
    margin-block-start: 0;
    margin-inline-start: 0;
  }
}

/* THE CLOSE MARK. The only control left in the player, so it is
   a mark rather than a word: with the counter, caption, tag line,
   paging and full screen gone, "Close" would have been the one
   piece of text competing with the brand it sits beside. */
.k-player__close {
  flex: 0 0 auto;
  width: var(--kdb-space-loose);
  height: var(--kdb-space-loose);
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: none;
  color: var(--kdb-text-muted);
  cursor: pointer;
  transition: color var(--kdb-motion-tick) var(--kdb-motion-curve);
}
.k-player__close svg { width: 62%; height: 62%; display: block; }
.k-player__close path {
  stroke: currentColor;
  stroke-width: 1.5;
  fill: none;
}
.k-player__close:hover,
.k-player__close:focus-visible { color: var(--kdb-text-primary); }

@media (prefers-reduced-motion: reduce) {
  .k-player, .k-partners-split { transition: none; }
  .k-player { transform: none; }
}
@media (max-width: 599px) {
  .k-player__foot { flex-direction: column; align-items: stretch; gap: var(--kdb-space-snug); }
}
