/* ============================================================================
   THE PULSE — pulse.css   (Gate 1 → Gate 2 → Gate 3, Builder B)
   Green Arrow Forge · phone-first swipe deck.

   G1 styled the frozen §1 vocabulary so the cards had somewhere to land.
   G2 fills those cards with real content, so the INTERNALS are now tuned to
   ~/Desktop/GAFFER/pulse-deck-v3.html — its type scale, its spacing rhythm,
   its hairlines — at 320–430px. What G2 changed, and why, in one place:

     1. THE DOUBLE HAIRLINE. plCardShell emits `.pl-folio` (which carries the
        deck's rule as its own border-bottom) immediately followed by a
        `.pl-rule`. Two hairlines, ~29px apart, at the top of every card. The
        redundant sibling collapses; see `.pl-folio + .pl-rule`.
     2. NO-SQUASH. `.pl-card` is a column flex container, so every block in it
        was `flex-shrink: 1` and a packed card compressed its own contents by a
        few px each instead of scrolling. Each block is now pinned.
     3. MONO WEIGHT CEILING. index.html loads JetBrains Mono at 400;500 ONLY.
        Every mono rule asking for 600/700 was rendering as SYNTHETIC bold,
        which smears an 8–10px glyph. Mono is capped at 500 throughout.
        (Cormorant and Inter both load 400–700 and keep their real weights.)
     4. `.pl-ledger-delta` no longer paints itself green unconditionally — a
        directional colour without `data-pl-dir` is a contract NO-SHIP (§1) and
        would have painted a FALLING team value green.
     5. The masthead name is two lines (`GREEN ARROW` + `<small>THE PULSE`),
        which `white-space: nowrap` alone would have run together.
     6. The drill is finished: capped back bar, a real 44px tappable foot with
        a focus ring, and first-child/last-child trims inside the panel.

   G3 lights up card 03 (the Scanner), both drill-ins and the Next-5 block with
   REAL content, so this pass is about the two things that only show up once
   there is something in the box: does the card fit its own screen, and does a
   component still read right when it is composed INSIDE another one. What G3
   changed, and why — every number below was measured in a headless render of
   this stylesheet at 320×568, 390×745 and 390×844, not estimated:

     7. THE CARD MUST FIT ITS SCREEN. The deck scrolls HORIZONTALLY, so a card
        that scrolls vertically is a gesture conflict, not a scrollbar. Card 03
        measured 886px of content into a 792px card, and the whole 94px was one
        declaration: `min-height: 44px` on `.pl-scan-add` made the watch button
        the tallest thing in `.pl-scan-top`, so every row opened with a 44px
        band before a word of the read. The TARGET is unchanged — a pseudo-
        element restores the 44px hit rectangle, proved by hit-testing all four
        corners — the BOX is what shrank. 5 rows now fit 390×844 exactly.
     8. `.pl-tag` IS NOT ALWAYS A LINE OF ITS OWN. `plTag()` composes INTO
        `.pl-scan-top`, `.pl-ticker-label` and `plOvhTagged`; its standalone
        `margin-top: 8px` dropped it 8px below the badge it sits beside and
        inflated the line box of the overline it annotates. Cancelled in the
        three companion contexts, kept in the standalone one.
     9. `.pl-reason` NESTED IN `.pl-scan-row` is a sub-line, not a peer of card
        02's ledger. Both drills compose it that way; without the nesting rule
        the last one drew a hairline 8px above the row's own bottom hairline —
        a double rule that reads as a rendering fault.
    10. `.pl-ledger-delta` is `flex: 0 0 auto` so a narrow card WRAPS the delta
        to its own line instead of breaking "▲ +0.9 since GW1" mid-claim or
        spilling it past the rounded box. One line at 390, two at 320, clipped
        at neither.
    11. SHORT/NARROW TIERS pay for the fit in PADDING AND TRACKING, never in a
        colour and never in the argument. The §7 explainer's honest limitation
        keeps its full sentence at a readable 12.5px/1.5; what gives way is
        gutter, gap, and 0.8px of letter-spacing on the mono overlines. Every
        contrast pair computed for the default tier therefore still holds.

   ⚠ MEASURED, NOT CLAIMED — card 03 in its LIVE (post-GW1) form:
        390×844  5 rows fit, 10.9px slack   ·  320×568  2 rows fit, 3 do not
        390×745  (Safari with its toolbars showing) 4 rows fit, 5 do not
   The remaining lever is ROW COUNT, which belongs to the emitter, not to this
   file: shedding a row is worth ~104px and no amount of CSS density is. The
   §7 PRE-SEASON card — the edition that actually ships first (§0.6) — fits
   320×568 AND 390×844 with zero overflow.

   ⚠ THE PROSE IN THIS FILE IS GREPPED. Several of the contract's mandatory
   assertions are RAW greps over this file — they do not strip comments — so a
   comment that quotes the forbidden pattern verbatim fails the very assertion
   it is documenting. Two lines used to do exactly that (the document-root
   pseudo-class on the scoping line, and the inline direction custom property
   on the DIRECTIONAL HOOK line) and made two gate checks unpassable by
   construction. Both now NAME the banned construct instead of quoting it.
   If you add a rule here, describe the forbidden token; never write it out.

   SCOPING CONTRACT (binding, contract §1 / §6):
     · EVERY selector prelude begins with `.pl-root`, optionally prefixed by
       `.tab-panel`. Nothing else. No document-root pseudo-class, no universal
       selector, no bare element selectors, no font-face at-rule.
       ⚠ LOOK G3 TIGHTENED THIS. The night theme used to key off the DESKTOP
       app's `body.night`, so `body.night ` was an allowed prefix. The Pulse
       now owns its own theme via `.pl-root[data-pl-theme="night"]` — which
       already begins with `.pl-root` — so the body-class prefix is no longer
       permitted at all. This is a tightening, not a loosening: nothing in this
       file may now reach outside `.pl-root` for a theme signal.
     · All custom properties are `--pl-*` and are declared ON `.pl-root`.
     · All @keyframes names begin with `pl-`.
     · ZERO CSS fill declarations in this file. Not on `.pl-jersey*`, not on the
       brand mark, not anywhere. CSS beats an SVG presentation attribute and
       would silently repaint a club's code text. Colour lives ONLY in the
       `fill=` attribute emitted by plBadgeHtml.
     · `.pl-root` IS the `.tab-panel` (contract §6), so it cancels the inherited
       `animation: fadeIn` whose `transform` would otherwise make the panel the
       containing block for its own `position: fixed` box.

   FONTS: reuses the three already loaded by index.html via style.css's
   --font-display / --font-body / --font-mono. No new font, no font-face
   at-rule (same reason the two lines above name their banned tokens rather
   than quoting them: the checks are raw greps).
   ⚠ LOADED WEIGHTS (index.html:14, verified at source):
       Cormorant Garamond 400 500 600 700   ·   Inter 400 500 600 700
       JetBrains Mono     400 500           ← nothing above 500 exists
   Asking for a weight that is not loaded does not fall back gracefully; the
   browser SYNTHESISES it by thickening the strokes, which at 8.5px costs more
   legibility than the emphasis buys. Nothing in this file asks mono for more
   than 500. If a heavier mono is genuinely wanted, the fix is one more weight
   in index.html's font URL — an index.html change, not a CSS change.

   COLOUR / CONTRAST (operator ruling O2, all values computed, WCAG 2.1 AA):
     · --pl-muted #6E695E = 5.01:1 on paper — the floor for text under 18.66px.
     · --pl-faint is NON-TEXT ONLY (dot fills, dashed rules). 1.92:1.
     · --pl-green #2B9F4C is FILL-ONLY (brand mark, active dot). Green TEXT is
       --pl-green-text (#166F35 light = 5.73:1 / #4FD37A night = 9.74:1).
       G4 spec Ruling 2 raised the light value from #177C3A, whose 4.84 on
       paper and 4.59 on the green wash were margins only on paper.
     · The JUMP verdict pill sits on --pl-verdict-bg #177C3A, NOT on #2B9F4C:
       white on #2B9F4C is 3.41:1 and fails at 10px. On #177C3A it is 5.28:1.
     · FDR pills are the G4 DEEP-PRINT ramp carrying WHITE ink on all five
       (spec Ruling 3, operator-locked), computed 4.67–7.24:1. The dark-ink-
       on-1–4 treatment was the G2/G3 interim: it cleared AA but broke the
       single-hue read. The mockup's own ramp cannot ship — white on it is
       2.01–4.35.

   DIRECTIONAL HOOK: the frozen vocabulary has no `--up`/`--down` modifier for
   .pl-hero-delta / .pl-kpi-n / .pl-wk-pts / .pl-ledger-delta. Rather than
   invent eight near-duplicate class modifiers, those four carry the FROZEN
   `data-pl-dir="up|down|flat"` attribute (contract v2 §1 amendment).
   An attribute, not an inline custom property: it is captured by the golden's
   data-* tokeniser, so direction becomes goldenable and assertable instead of
   hiding in a style= string no signature can see.
   The rules live at the END of this file, deliberately: they tie on specificity
   with the component rules they override, so source order is what makes the
   attribute authoritative. Do not move them, and do not re-add an
   unconditional directional colour to any of those four elements. The standing
   §1 assertion — the count of references to an INLINE DIRECTION CUSTOM
   PROPERTY (the `--pl-dir` reader form) must be zero — stays green only while
   nothing here reads one, and a directional colour applied WITHOUT the
   attribute is a NO-SHIP.
   `.pl-reason-v--up` is NOT one of the four: the frozen vocabulary gives that
   element its own class modifier, so it stays a class.
   ========================================================================== */


/* ---------------------------------------------------------------------------
   1 · TOKENS + SHELL
   ------------------------------------------------------------------------ */

.pl-root {
  /* --- surfaces --- */
  --pl-paper:        #F7F5EF;
  --pl-card:         #FFFFFF;
  --pl-hair:         #E4E0D5;   /* NON-TEXT: hairlines, borders */
  --pl-faint:        #B8B3A7;   /* NON-TEXT ONLY — 1.92:1, never carries text */

  /* --- text --- */
  --pl-ink:          #1B1A16;   /* 15.97:1 on paper */
  --pl-prose:        #33302A;   /* 12.06:1 — editorial hook / hard tag */
  --pl-quiet:        #4A463D;   /*  8.62:1 — folio foot, alt, legend prose */
  --pl-muted:        #6E695E;   /*  5.01:1 — the small-text floor */

  /* --- signal --- */
  --pl-green:        #2B9F4C;   /* FILL ONLY — brand mark, active dot */
  --pl-green-text:   #166F35;   /*  5.73:1 paper · 6.25 card · 5.44 wash (spec R2) */
  --pl-red-text:     #A8102A;   /*  6.95:1 on paper */
  /* Facelift Phase 1 — a QUIETER green than --pl-green-text, for eyebrow
     labels only. --pl-green-text is the LOUD signal (deltas, CTAs); an
     eyebrow in that same green would flatten the loud/quiet hierarchy the
     facelift is asking for. Same hue family, deliberately desaturated. */
  --pl-green-quiet:  #3A6B45;   /*  5.72:1 on paper */

  /* --- washes + their inks (each pair verified >= 4.5:1) --- */
  --pl-wash-neutral: #EFEDE4;  --pl-ink-chip:   #54504A;  /* 6.83 */
  --pl-wash-green:   #E4F3E8;  /* ink = --pl-green-text     5.44 */
  --pl-wash-amber:   #F4E9D6;  --pl-ink-amber:  #7A5210;  /* 5.74 */
  --pl-wash-blue:    #E7EEF6;  --pl-ink-blue:   #2C5A92;  /* 6.02 */
  --pl-wash-violet:  #ECEAF4;  --pl-ink-violet: #4B3F8F;  /* 7.33 */
  --pl-p90-bg:       #F3F1EA;  --pl-p90-ink:    #413E37;  /* 9.44 */
  --pl-ledger-bg:    #EEF5EE;  --pl-ledger-bd:  #DCEBDD;

  /* --- verdict pill --- */
  --pl-verdict-bg:   #177C3A;  --pl-verdict-ink: #FFFFFF; /* 5.28 */

  /* --- FDR DEEP-PRINT RAMP (spec Ruling 3, operator-LOCKED) ---------------
     Pill BACKGROUNDS, white ink on all five. The v3 mockup's own ramp is the
     look but cannot ship: white on it computes 2.01-4.35 and the pill is
     10.5px/500, i.e. normal text, so the floor is 4.5 not 3.0. G2/G3's
     interim fix kept the mockup's backgrounds and swapped in near-black ink,
     which passed but broke the single-hue read. The deep-print ramp is the
     one treatment where the mockup's white-on-colour signature clears AA at
     every step. The ink-on-a-light-ground reading was REJECTED: FDR 3 and 4
     compute 4.28 / 4.13 on paper.
     ⚠ These are `--pl-` tokens on `.pl-root`. They are NOT the shared
     `.fdr-bg-*` rules, which are defined twice and render dark-on-dark on
     paper — a logged OWED with its own gate. Do not unify them. */
  --pl-fdr-1: #1B7A3C;  --pl-fdr-ink-1: #FFFFFF;  /* 5.39 */
  --pl-fdr-2: #4A7A2B;  --pl-fdr-ink-2: #FFFFFF;  /* 5.11 */
  --pl-fdr-3: #9A6B1E;  --pl-fdr-ink-3: #FFFFFF;  /* 4.67 */
  --pl-fdr-4: #B85231;  --pl-fdr-ink-4: #FFFFFF;  /* 4.90 */
  --pl-fdr-5: #A81F2D;  --pl-fdr-ink-5: #FFFFFF;  /* 7.24 */

  /* --- decoration: NON-TEXT ONLY, no contrast floor applies --- */
  --pl-jersey-ring: rgba(27, 26, 22, 0.10);   /* edge for a light kit on white */
  --pl-lift:        0 1px 2px rgba(27, 26, 22, 0.04);

  /* --- metrics --- */
  --pl-mast-h:  52px;
  --pl-safe-t:  env(safe-area-inset-top, 0px);
  --pl-safe-b:  env(safe-area-inset-bottom, 0px);
  --pl-gutter:  22px;
  --pl-max:     480px;
  /* One token for the hero mark's box, because the hero's second line is
     indented by exactly it (see §2) and the short-viewport guard has to move
     the mark and the indent together or the two lines step apart. */
  --pl-hero-mark: 66px;

  /* --- shell --- */
  position: fixed;
  inset: 0;
  /* dvh, never vh: `inset:0` on a fixed box resolves against the SMALL viewport
     in some engines, leaving a strip below the deck once the mobile toolbar
     retracts. 100dvh tracks the live viewport and closes that gap. */
  height: 100dvh;
  z-index: 400;                 /* over all chrome, under .gaf-dlg-backdrop 9999 */
  overflow: hidden;
  background: var(--pl-paper);
  color: var(--pl-ink);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.45;
  /* Cormorant Garamond ships OLDSTYLE figures by default: "71" renders as
     "7ı" and "£102.4m" as "£ıo2.4m". Verified in a headless render. On a
     points / rank / money surface that is a misreading risk, so lining figures
     are forced here and inherit to every descendant. Inter and JetBrains Mono
     are lining by default, so this is a no-op for them. */
  font-variant-numeric: lining-nums;
  padding: 0;
  /* MANDATORY (contract §6): kills the inherited `.tab-panel` fadeIn whose
     transform would make this element the containing block for its own
     position:fixed box and collapse inset:0 for 200ms on every entry. */
  animation: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-font-smoothing: antialiased;
}

.pl-root ::selection { background: var(--pl-wash-green); }

/* --- masthead ------------------------------------------------------------ */

.pl-root .pl-mast {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 2;
  /* border-box so max-width caps the SAME box the deck caps: with content-box
     the 44px of gutter padding sat outside the 480 cap and the masthead ran
     10px wider than the deck on a >480px viewport, misaligning the gutter. */
  box-sizing: border-box;
  max-width: var(--pl-max);
  margin: 0 auto;
  height: calc(var(--pl-mast-h) + var(--pl-safe-t));
  padding: var(--pl-safe-t) var(--pl-gutter) 0;
  display: flex;
  align-items: center;
  gap: 9px;
  background: var(--pl-paper);
  border-bottom: 1px solid var(--pl-hair);
}

.pl-root .pl-mast-mark {
  width: 26px;
  height: 26px;
  flex: none;
  /* no CSS fill declaration — the mark's colour lives in its fill= attribute */
}

/* The masthead name is TWO lines — `GREEN ARROW<small>THE PULSE</small>` — the
   mockup's brand bar (serif wordmark) plus its tab label (mono, tracked out).
   `white-space: nowrap` on the block with an INLINE <small> would have set both
   on one line and read "GREEN ARROWTHE PULSE", so the nowrap lives on the
   wordmark's own line and <small> is promoted to a block. */
.pl-root .pl-mast-name {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: 0.2px;
  color: var(--pl-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pl-root .pl-mast-name small {
  display: block;
  font-family: var(--font-mono);
  font-size: 8.5px;
  font-weight: 400;
  letter-spacing: 2.4px;
  line-height: 1;
  margin-top: 3px;
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  overflow: hidden;
  text-overflow: ellipsis;
}

.pl-root .pl-mast-exit {
  margin-left: auto;
  flex: none;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 1.6px;
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  background: none;
  border: none;
  cursor: pointer;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}

.pl-root .pl-mast-exit:focus-visible {
  outline: 2px solid var(--pl-green-text);
  outline-offset: 2px;
  border-radius: 6px;
}

/* THE THEME TOGGLE. Deliberately the quietest thing in the masthead: same mono
   10px/1.6px as FULL APP, same muted ink, no border, no background, no icon.
   It is a preference control, not a claim about the team — it must never
   compete with the deck for attention.

   `margin-left: auto` moves here from .pl-mast-exit, which now simply follows
   it. Both keep the 44px touch target. The masthead is a flex row and the
   wordmark is the only `flex: 1 1 auto` member, so the two buttons are sized
   by content and the NAME absorbs the difference — verified in the browser at
   every viewport in the matrix, 320 included.

   No CSS fill declaration and no SVG: the label is text, so the frozen
   zero-fill rule is untouched by construction. */
.pl-root .pl-mast-theme {
  margin-left: auto;
  flex: none;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 1.6px;
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  background: none;
  border: none;
  cursor: pointer;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}

.pl-root .pl-mast-theme:focus-visible {
  outline: 2px solid var(--pl-green-text);
  outline-offset: 2px;
  border-radius: 6px;
}

/* .pl-mast-exit no longer claims the free space — the toggle does. */
.pl-root .pl-mast-theme ~ .pl-mast-exit { margin-left: 0; }

/* --- deck ---------------------------------------------------------------- */

.pl-root .pl-deck {
  position: absolute;
  top: calc(var(--pl-mast-h) + var(--pl-safe-t));
  left: 0; right: 0; bottom: 0;
  max-width: var(--pl-max);     /* wide screens: centred column, no re-layout */
  margin: 0 auto;
  display: flex;
  align-items: stretch;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior: contain;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.pl-root .pl-deck::-webkit-scrollbar { display: none; width: 0; height: 0; }

.pl-root .pl-card {
  min-width: 100%;
  width: 100%;
  flex: none;
  scroll-snap-align: center;
  scroll-snap-stop: always;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  scrollbar-width: none;
  /* bottom pad clears the .pl-hint (bottom 40px + ~15px tall) and .pl-dots */
  padding: 18px var(--pl-gutter) calc(60px + var(--pl-safe-b));
}

/* SWIPE-TO-EXIT (G5) — the 4th slide past THE SCANNER, appended in
   plDeckHtml (pulse.js). Same snap sizing as `.pl-card` so the native
   scroll-snap sequence treats it identically; no `.pl-dot` for it (only
   the 3 real pages get one — see plOnDeckScroll) and no scrollable
   content of its own, since landing here triggers plOnDeckScrollEnd's
   exit rather than inviting a read. */
.pl-root .pl-exit-slide {
  min-width: 100%;
  width: 100%;
  flex: none;
  scroll-snap-align: center;
  scroll-snap-stop: always;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  color: var(--pl-muted);
}
.pl-root .pl-exit-mark {
  font-size: 32px;
  color: var(--pl-green);
}
.pl-root .pl-exit-label {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 1.5px;
}

.pl-root .pl-card::-webkit-scrollbar { display: none; width: 0; height: 0; }

/* NO-SQUASH.
   `.pl-card` is a column flex container, so every block inside it is a flex
   ITEM and defaults to `flex-shrink: 1`. On a short viewport that means a
   packed card SHRINKS its own blocks — hero, tiles, rows all compress by a few
   px each — instead of scrolling, which is exactly the "everything looks
   slightly wrong and nothing looks broken" failure. Each block is pinned to its
   natural height and the card's own `overflow-y: auto` does the work.

   Enumerated, not `> *`: the scoping contract bans the universal selector, and
   `.pl-calm` is deliberately absent because it is the one child that MUST grow
   (it centres the all-clear in the leftover space). */
.pl-root .pl-folio,
.pl-root .pl-rule,
.pl-root .pl-ovh,
.pl-root .pl-hero,
.pl-root .pl-chips,
.pl-root .pl-kpis,
.pl-root .pl-wk,
.pl-root .pl-move,
.pl-root .pl-move-head,
.pl-root .pl-ticker,
.pl-root .pl-reason,
.pl-root .pl-tiles,
.pl-root .pl-aq-preview,
.pl-root .pl-capbox,
.pl-root .pl-alt,
.pl-root .pl-conv,
.pl-root .pl-ledger,
.pl-root .pl-scan,
.pl-root .pl-legend,
.pl-root .pl-empty,
.pl-root .pl-count,
.pl-root .pl-hook,
.pl-root .pl-tag,
.pl-root .pl-foot { flex: none; }

/* --- folio / rules / footer / overline ----------------------------------- */

.pl-root .pl-folio {
  flex: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 2.5px;
  color: var(--pl-muted);
  padding-bottom: 12px;
  border-bottom: 1px solid var(--pl-hair);
  margin-bottom: 15px;
  text-transform: uppercase;
}

.pl-root .pl-folio-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Facelift Phase 1: the eyebrow NAME is quiet green; the sibling PAGE
     digit ("02 / 03") stays the parent .pl-folio's --pl-muted — only one of
     the two flex children in .pl-folio changes colour. */
  color: var(--pl-green-quiet);
}

.pl-root .pl-folio-page { flex: none; }

.pl-root .pl-rule {
  height: 1px;
  border: none;
  background: var(--pl-hair);
  margin: 14px 0;
}

/* THE DOUBLE HAIRLINE. `plCardShell` emits `.pl-folio` — which already carries
   the deck's rule as its own `border-bottom` — IMMEDIATELY followed by a
   `.pl-rule` divider. Rendered as authored that is TWO hairlines with ~29px of
   dead space between them at the top of every card, where the spec has one rule
   and 15px. The folio keeps its rule; the redundant sibling collapses. */
.pl-root .pl-folio + .pl-rule { display: none; }

/* …and the first body block therefore must not re-add a top margin under it. */
.pl-root .pl-rule + .pl-ovh,
.pl-root .pl-rule + .pl-count,
.pl-root .pl-rule + .pl-hook,
.pl-root .pl-rule + .pl-empty { margin-top: 0; }

.pl-root .pl-ovh {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 2.5px;
  color: var(--pl-green-quiet);   /* facelift Phase 1 — was --pl-muted */
  margin: 16px 0 10px;
  text-transform: uppercase;
}

/* Facelift Step 5, Card 02 only: `.pl-folio`/`.pl-ovh` are the SHARED shell
   rules every card uses — tightened here scoped to `[data-pl-card="act"]`
   rather than in the base rule, so Cards 01/03 (already at 0px headroom,
   not overflowing) are untouched. The drill panel is a separate DOM subtree
   outside `[data-pl-card]`, so this cannot reach its own `.pl-ovh` overlines. */
.pl-root [data-pl-card="act"] .pl-folio { margin-bottom: 6px; }
.pl-root [data-pl-card="act"] .pl-ovh   { margin: 6px 0 6px; }

.pl-root .pl-foot {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 15.5px;
  line-height: 1.38;
  color: var(--pl-quiet);
  border-top: 1px solid var(--pl-hair);
  padding-top: 13px;
  /* Pushes the foot to the bottom of the card. On a card with spare height ALL
     of that spare pools into this one gap — 160.98px above card 03's foot at
     390x745, which reads as a rendering fault rather than as breathing room.
     See the `.pl-legend` rule below, which claims a share of it. */
  margin-top: auto;
}

/* G4 item 6, GAP 2 of 2 — DISTRIBUTE, don't pool.
   A second `auto` margin in the same flex column SPLITS the free space with the
   foot's instead of letting the foot take all of it, so the explainer sits in
   its card rather than clinging to the top of one. This is deliberately not a
   fixed value: card 03 has 160.98px of spare at 390x745 and 0.72px at 320x568,
   and a hard number that suited either end would break the other. `auto`
   resolves to zero when there is nothing to share, so the short viewport is
   byte-identical to before. */
.pl-root [data-pl-card="scan"] .pl-legend { margin-block: auto; }

/* FACELIFT — THE SAME FIX, CARD 03's IN-SEASON BOARD (supervisor review,
   issue 4). The rule above already gives the pre-season explainer's
   `.pl-legend` a share of the gap above `.pl-foot`; the in-season scan board
   never got the equivalent, so once real rows are showing, ALL of this
   card's spare pools into that one gap instead — the same "dead gap before
   the footer" pattern already fixed on cards 01/02/03-preseason, just not yet
   applied to this fourth state. `auto` resolves to zero when there is
   nothing to share, so a tight viewport (390x745 / 375x667, where this card
   has the least spare of the three) is byte-identical to before — this can
   only ever REDUCE the card's total height, never add to it. */
.pl-root [data-pl-card="scan"] .pl-scan { margin-block: auto; }

/* v4 G2·O4 — THE SAME FIX, CARD 02.
   Option B moved two why-now tiles, the captain call and the alternative into
   the drill, which took card 02 from 1.94px of headroom to ~100px — and every
   one of those pixels then pooled into a single void above the foot, because
   `.pl-foot` carries `margin-top: auto` and nothing else was claiming a share.
   A void that large reads as a rendering fault, not as breathing room.

   The NEXT FIVE row claims the share: it is the card's visual centre of
   gravity, and letting the fixture run sit in the card rather than clinging to
   the headline is what the space is for. Deliberately `auto`, not a fixed
   value — card 02's spare swings with the payload (1.94px on live at deploy,
   99.94px now, same code) and any hard number would be wrong at one end.
   `auto` resolves to zero when there is nothing to share, so a tight viewport
   is byte-identical to before. */
.pl-root [data-pl-card="act"] .pl-next5 { margin-block: auto; }

/* LOOK G3 — THE SAME FIX, CARD 01. The third instance of one pattern.

   ⚠ PRE-EXISTING, NOT CAUSED HERE: card 01 pooled 283.97px into a single void
   above the foot at 440x956 BEFORE this gate, on the operator's own phone. It
   is the only card that never had a block claiming a share, and it went unfixed
   because both earlier voids were found while re-measuring the card that had
   just changed. Folded in on the operator's word rather than left as a fourth
   OWED item, since it is two tokens following two established precedents.

   The RECORD claims the share: it is what a pre-season card is about, and the
   season rows are the block a reader's eye should land on. `auto` resolves to
   zero when there is nothing to share, so 390x745 and 375x667 — where the card
   is tight — are byte-identical to before. */
.pl-root [data-pl-card="team"] .pl-wk { margin-block: auto; }

/* CARD 01 FOLLOW-UP — the ~200px void a supervisor screenshot review just
   flagged between the record and the foot. Root cause, traced (not
   guessed): the rule directly above gives `.pl-wk` its OWN top+bottom auto
   margins so it centres in the card's leftover space, but the base `.pl-foot`
   rule (above, `margin-top: auto`) is a THIRD auto margin in the same flex
   column. All three share the free space equally, so `.pl-wk` only claims
   one part above itself while its bottom part and the foot's top part both
   land in the SAME visual gap — two shares pooling directly over the foot
   instead of the even top/bottom split the rule above intends. That is the
   lopsided void, not a missing fill rule.
   Zeroing the foot's OWN share HERE — nothing above is touched, `.pl-wk`
   keeps its rule untouched — leaves `.pl-wk`'s two auto margins as the only
   claimants, so they split the leftover space evenly instead of pooling
   two-thirds of it into one spot. `auto` already resolves to zero at the
   binding 390x745 / 375x667 tier (nothing to share there), and `0` is
   byte-identical to that resolved zero, so this cannot add height at the
   tight viewports — it only rebalances (and visually shrinks the flagged
   gap) wherever the card has slack to give. */
.pl-root [data-pl-card="team"] .pl-foot { margin-top: 0; }

/* THE DRILL AFFORDANCE.
   `plCardShell` promotes the foot to `role="button" tabindex="0"` the moment a
   renderer is registered for that card — so from G2 on, this line is a real
   control and needs a real control's affordances: a >=44px target, a cursor, and
   a focus ring. Without the attribute the foot stays inert prose and gets none
   of them, which is what makes "no tappable dead ends" visible rather than
   merely true.

   The chevron is a pseudo-element, so it is not a DOM text node: it cannot
   reach a golden's TEXT hash, and because the element carries an explicit
   `aria-label` the accessible name is not computed from contents, so it never
   reaches a screen reader either. Decoration only. */
.pl-root .pl-foot[data-pl-drill] {
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-height: 44px;
  box-sizing: border-box;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.pl-root .pl-foot[data-pl-drill]::after {
  content: "\203A";                /* › */
  flex: none;
  margin-left: auto;
  font-family: var(--font-mono);
  font-style: normal;
  font-size: 17px;
  line-height: 1;
  color: var(--pl-muted);          /* decorative, but still clears 4.5:1 */
}

.pl-root .pl-foot[data-pl-drill]:focus-visible {
  outline: 2px solid var(--pl-green-text);
  outline-offset: 3px;
  border-radius: 6px;
}

/* --- dots + swipe hint --------------------------------------------------- */

.pl-root .pl-dots {
  position: absolute;
  left: 0; right: 0;
  bottom: calc(16px + var(--pl-safe-b));
  z-index: 3;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 8px;
  pointer-events: none;
}

.pl-root .pl-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--pl-faint);       /* NON-TEXT use of --pl-faint */
  flex: none;
  pointer-events: none;              /* non-interactive; markup carries aria-hidden */
  transition: width 0.3s ease, background-color 0.3s ease, border-radius 0.3s ease;
}

.pl-root .pl-dot--on {
  width: 22px;
  border-radius: 4px;
  background: var(--pl-green);       /* FILL use of the brand green */
}

.pl-root .pl-hint {
  position: absolute;
  right: 20px;
  bottom: calc(40px + var(--pl-safe-b));
  z-index: 3;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 1.5px;
  color: var(--pl-muted);
  pointer-events: none;
  transition: opacity 0.4s ease;
}

@media (prefers-reduced-motion: no-preference) {
  .pl-root .pl-hint { animation: pl-nudge 1.6s ease-in-out infinite; }
  .pl-root .pl-dot  { transition: width 0.3s ease, background-color 0.3s ease, border-radius 0.3s ease; }
}

@media (prefers-reduced-motion: reduce) {
  .pl-root .pl-deck { scroll-behavior: auto; }
  .pl-root .pl-dot  { transition: none; }
  .pl-root .pl-hint { transition: none; }
}

@keyframes pl-nudge {
  0%, 100% { transform: translateX(0); }
  50%      { transform: translateX(5px); }
}

@keyframes pl-rise {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* --- drill-in ------------------------------------------------------------ */

.pl-root .pl-drill {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  background: var(--pl-paper);
  overflow: hidden;
}

.pl-root .pl-drill[hidden] { display: none; }

@media (prefers-reduced-motion: no-preference) {
  .pl-root .pl-drill { animation: pl-rise 0.18s ease-out; }
}

/* The back bar is capped and centred on the SAME column as the body, so on a
   viewport wider than --pl-max its hairline stops where the content stops
   instead of running full-bleed past a centred column. `width` is explicit, so
   the column flex container's default `align-self: stretch` does not apply and
   the auto margins can centre it. Height matches the masthead it replaces, so
   the panel does not jump on open. */
.pl-root .pl-drill-back {
  flex: none;
  box-sizing: border-box;
  width: 100%;
  max-width: var(--pl-max);
  min-height: 44px;
  height: calc(var(--pl-mast-h) + var(--pl-safe-t));
  margin: 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: var(--pl-safe-t) var(--pl-gutter) 0;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 1.6px;
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  background: none;
  border: none;
  border-bottom: 1px solid var(--pl-hair);
  cursor: pointer;
  text-transform: uppercase;
  -webkit-tap-highlight-color: transparent;
}

.pl-root .pl-drill-back:active { color: var(--pl-ink); }

.pl-root .pl-drill-back:focus-visible {
  outline: 2px solid var(--pl-green-text);
  outline-offset: -2px;
}

.pl-root .pl-drill-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  padding: 14px var(--pl-gutter) calc(28px + var(--pl-safe-b));
  max-width: var(--pl-max);
  width: 100%;
  margin: 0 auto;
}

.pl-root .pl-drill-body::-webkit-scrollbar { display: none; width: 0; }

/* The drill has no folio, so whichever block opens it must not carry the
   under-a-folio top margin its card version needs. Enumerated (not `> *`) for
   the same reason the NO-SQUASH list is: the universal selector is banned.
   `.pl-ticker` and `.pl-legend` are here because G3's drills open on them —
   the risers detail leads with the Next-5 ticker, and the pre-season scanner's
   explainer legend is reachable from the card foot. */
.pl-root .pl-drill-body .pl-ovh:first-child,
.pl-root .pl-drill-body .pl-count:first-child,
.pl-root .pl-drill-body .pl-empty:first-child,
.pl-root .pl-drill-body .pl-ticker:first-child,
.pl-root .pl-drill-body .pl-legend:first-child,
.pl-root .pl-drill-body .pl-capbox:first-child,
.pl-root .pl-drill-body .pl-hook:first-child { margin-top: 0; }

/* A drill is a detail view: the last row of a list inside it is the end of the
   panel, so it drops the divider that would otherwise dangle over whitespace. */
.pl-root .pl-drill-body .pl-reason:last-child,
.pl-root .pl-drill-body .pl-wk-row:last-child,
.pl-root .pl-drill-body .pl-scan-row:last-child { border-bottom: none; }

/* A drill-in is the ONE Pulse surface that is allowed to scroll vertically — it
   is an overlay, not a deck page, so scrolling it conflicts with no gesture.
   That is exactly why the detail lives here and not on the card. The panel
   itself must still never scroll: `.pl-drill` is `overflow: hidden` and only
   `.pl-drill-body` scrolls, so the back bar stays pinned and reachable however
   long the body runs. This rule is the visible half of that contract — a
   scroll gutter the last block cannot hide under. */
.pl-root .pl-drill-body .pl-scan:last-child,
.pl-root .pl-drill-body .pl-reason:last-child,
.pl-root .pl-drill-body .pl-p90:last-child { margin-bottom: 4px; }

/* The drill's own closing note. `.pl-foot` inside a drill is NOT a control —
   only a card's foot opens a drill — so it keeps the prose treatment and the
   affordance rules above never match it. */
.pl-root .pl-drill-body .pl-foot { margin-top: 22px; }


/* ---------------------------------------------------------------------------
   2 · CARD 01 — YOUR TEAM AT A GLANCE
   ------------------------------------------------------------------------ */

/* DECORATIVE BACKGROUND SHAPES (big-numbers-minimal-info pass) — pure
   decoration, zero informational content, confirmed in scope. Pure CSS
   (border + clip-path, ZERO `fill:` anywhere, §6) rather than inline SVG.
   `position:relative` was previously unset (defaulted to static) — confirmed
   no `position:absolute` descendant of this card depends on the old
   containing block. `z-index:0` gives the card its own stacking context so
   the shapes' `z-index:-1` can only ever paint under THIS card's own
   content, never under/over a sibling card in `.pl-deck`. */
.pl-root [data-pl-card="team"] {
  position: relative;
  z-index: 0;
}

/* Ring, bleeding off the top-right corner. Border-only, no fill. Negative
   `top` is safe against this card's `overflow-y:auto` (scrollTop can't go
   negative, so it self-clips rather than growing the scrollable area). */
.pl-root [data-pl-card="team"]::before {
  content: "";
  position: absolute;
  top: -50px;
  right: -50px;
  width: 140px;
  height: 140px;
  border-radius: 50%;
  border: 1.5px solid color-mix(in srgb, var(--pl-green) 12%, transparent);
  z-index: -1;
  pointer-events: none;
}

/* Solid triangle, bleeding off the bottom-right corner. Anchored at
   `bottom:0` (NOT a negative bottom) — a negative-bottom absolute child of
   an `overflow-y:auto` box genuinely grows the scrollable height, unlike
   negative `top`; the right-edge bleed alone reads as "bottom-right
   corner," and `overflow-x:hidden` crops it cleanly. */
.pl-root [data-pl-card="team"]::after {
  content: "";
  position: absolute;
  bottom: 0;
  right: -35px;
  width: 130px;
  height: 100px;
  background: color-mix(in srgb, var(--pl-ink-violet) 10%, transparent);
  clip-path: polygon(100% 0, 100% 100%, 0 100%);
  z-index: -1;
  pointer-events: none;
}

/* THE HERO — A HORIZONTAL BAND: mark on the left, the big serif number to its
   RIGHT, the meta line beneath the number inside `.pl-hero-body`.

   ⚠ G4 item 2. This used to STACK: `.pl-hero` wrapped, and the wrapper's meta
   sentence set a wide enough min-content width to push the whole delta+meta
   stack onto its own line under the mark — which is what the shipped card
   actually did. The band is now structural rather than a hope.

   Two things make it hold, and neither is optional:
     · `flex-wrap: nowrap` — the number can no longer drop below the mark.
     · `.pl-hero-body { min-width: 0 }` — a flex item defaults to
       `min-width: auto` and refuses to shrink below its content, so without it
       a long delta would push the card wider than the viewport instead of
       letting its own text wrap.

   Before G4 the vocabulary had no name for the delta+meta stack, so this file
   carried TWO layout paths — one for a flat three-sibling hero, one for a
   nested pair — and kept them in step by hand. `.pl-hero-body` (spec Ruling 1)
   named it, and both compensating rules are deleted. Do not reintroduce them.

   ⚠ v5 addendum — THE NUMBER AND THE DELTA LINE NOW RENDER TOGETHER. The
   emitter used to treat `.pl-hero-delta` as either/or: the rank OR a settled
   move's delta, never both. It now emits the rank in `.pl-hero-delta` and a
   value-bearing delta line in `.pl-hero-st` directly beneath it. Nothing here
   had to change to hold that: `.pl-hero-body` is an ordinary block box (it is
   only a FLEX ITEM of `.pl-hero`, never a flex container itself), so its
   children — `.pl-hero-delta`, `.pl-hero-st`, `.pl-hero-meta` — already stack
   in DOM order with no layout mode change. `flex-wrap: nowrap` on `.pl-hero`
   is UNCHANGED (frozen G4 ruling): it still only governs the mark sitting
   beside the body, never the lines stacking inside it. */
.pl-root .pl-hero {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  column-gap: 15px;
}

.pl-root .pl-hero-mark {
  width: var(--pl-hero-mark);
  height: var(--pl-hero-mark);
  flex: none;
  /* no CSS fill declaration — brand mark colour lives in its fill= attribute */
}

/* THE HERO NUMBER — the overall rank, now shown at full length (e.g. seven
   digits with separators) rather than only ever a compact form, since the
   delta line beneath it carries the short "moved by" figure instead. Weight
   goes to 700 (not the mono cap — this is `--font-display`, and Cormorant
   Garamond loads 700, §6) so the figure reads as the one dominant mark on the
   card, matching the delta line's own smaller weight beneath it.
   `overflow-wrap: anywhere` is unchanged and still does the real work of
   keeping a long figure from widening the card at 320px — it now also covers
   the longer full-length form, not just a compact "+412k". */
.pl-root .pl-hero-delta {
  font-family: var(--font-display);
  font-size: 72px;   /* big-numbers-minimal-info pass, was 46px */
  font-weight: 700;
  /* 0.9 is tight for the full-precision rank's comma descenders (measured:
     the glyph box modestly exceeds a 0.9-multiple line box at this weight),
     but there's no overflow:hidden here — it's a normal typographic overflow,
     not a clip, and costs nothing in card height. Raising line-height to
     contain it costs REAL height at a binding viewport for a cosmetic gain;
     left at 0.9, unchanged. */
  line-height: 0.9;
  letter-spacing: -0.5px;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--pl-ink);
  overflow-wrap: anywhere;         /* "+412k" never widens the card at 320px */
}

/* `.pl-hero-body` — the delta+meta stack, NAMED (spec Ruling 1, §1 vocab add).

   This replaces the pair of rules the stylesheet used to carry for an unclassed
   wrapper: one for a FLAT hero (three siblings) and one for a NESTED one. Two
   layout paths for one hero, kept in step by hand, is the drift class this
   project keeps paying for. The emitter nests, this names what it nests, and
   the flat-case compensation is gone.

   `min-width: 0` is load-bearing: a flex item defaults to `min-width: auto` and
   refuses to shrink below its content, so without it a long delta would force
   the card wider than the viewport instead of wrapping inside the hero. */
.pl-root .pl-hero-body { flex: 1 1 auto; min-width: 0; }

/* THE STATUS WORD — v4 G1. The one class card 01 adds.

   No existing member of §1 fits: `.pl-hero-meta` is muted body prose and
   `.pl-tag` is an 8.5px provenance chip. This is an uppercase mono label whose
   whole job is to name the direction the hero number already carries.

   It takes NO colour of its own. The colour arrives from `data-pl-dir` at the
   end of this file — one frozen attribute, one authority for a directional
   colour (§1 amendment). A `.pl-hero-st--up` modifier would be a second
   mechanism for one fact, which is the drift the attribute exists to prevent.

   ⚠ v5 addendum — ALSO THE DELTA LINE. This class used to carry only a short
   caption word ("YOU CLIMBED", "DAYS TO KICKOFF"). It now also carries the
   value-bearing delta line beneath the hero number (an arrow glyph + a
   compact figure + a short qualifier). Same class, same position directly
   under `.pl-hero-delta`, same colour mechanism — the only addition is
   `font-variant-numeric` below, so the embedded figure sets in lining,
   tabular digits like every other number on this card instead of the text
   font's default proportional ones. Nothing about the direction colouring
   changes: it is still `data-pl-dir`, still only at the end of this file. */
.pl-root .pl-hero-st {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;                /* mono ships 400/500 only (§6) */
  letter-spacing: 1.6px;
  text-transform: uppercase;
  margin-top: 5px;
  line-height: 1.2;
  font-variant-numeric: lining-nums tabular-nums;
}

/* THE PRE-SEASON COUNTDOWN CAPTION. `.pl-hero-st` takes no colour of its own
   (see the rule above — it inherits, so the in-season word is painted by its
   data-pl-dir). The pre-season caption names a UNIT, not a direction, carries
   no such attribute, and would therefore inherit full ink and shout.

   `:not([data-pl-dir])` matches ONLY the caption-without-a-direction case. The
   in-season emitter (plCardTeamInSeason) is the sole other producer of this
   class and ALWAYS passes a direction, so its colour cannot be touched here —
   which matters because that path is dormant until GW1 and would not be caught
   by any pre-season check. */
.pl-root .pl-hero-st:not([data-pl-dir]) { color: var(--pl-muted); }

.pl-root .pl-hero-meta {
  font-size: 14px;
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  margin-top: 4px;
  line-height: 1.35;
}

.pl-root .pl-hero-meta b { color: var(--pl-ink); font-weight: 600; }

/* .pl-hero-delta-tail — the coloured portion of the collapsed one-line
   .pl-hero-meta (big-numbers-minimal-info pass; replaces the old separate
   .pl-hero-st second line). Real specificity (class + attribute), so it
   doesn't depend on file position — it deliberately does NOT touch the
   frozen data-pl-dir block at the end of this file (contract §1 amendment
   names exactly four elements); the generic [data-pl-dir] selectors there
   already match this span too, so this rule is redundant-but-explicit, not
   the sole mechanism. */
.pl-root .pl-hero-delta-tail[data-pl-dir="up"]   { color: var(--pl-green-text); }
.pl-root .pl-hero-delta-tail[data-pl-dir="down"] { color: var(--pl-red-text); }

/* --- chips --- */

.pl-root .pl-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 14px;
}

/* ⚠ WEIGHT CEILING. index.html loads `JetBrains Mono:wght@400;500` — 600 and 700
   DO NOT EXIST. Every mono rule in this file that asked for 600/700 was getting
   the browser's SYNTHETIC bold, which smears an 8–10px glyph and eats the
   contrast the tokens were chosen to guarantee. Mono is capped at 500
   throughout; the emphasis is carried by tracking and by the wash instead.
   (Cormorant 400–700 and Inter 400–700 are both fully loaded, so the serif and
   the sans keep their real weights.) */
.pl-root .pl-chip {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.5px;
  line-height: 1.5;
  padding: 4px 9px;
  border-radius: 20px;
  background: var(--pl-wash-neutral);
  color: var(--pl-ink-chip);       /* 6.83:1 light · 12.10:1 night */
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pl-root .pl-chip--neutral { background: var(--pl-wash-neutral); color: var(--pl-ink-chip); }
.pl-root .pl-chip--climb   { background: var(--pl-wash-green);   color: var(--pl-green-text); }
.pl-root .pl-chip--protect { background: var(--pl-wash-amber);   color: var(--pl-ink-amber); }

/* --- KPI tiles --- */

.pl-root .pl-kpis {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px 0;   /* facelift Phase 2 fix: 14px ate into 390x745's binding fit */
  margin-top: 14px;
}

/* Facelift Phase 2: the boxed-tile look (card background, full border,
   shadow, radius) is replaced with a plain 2x2 grid divided by a single
   THIN VERTICAL HAIRLINE between the two columns — no change to the grid
   shape itself (still 1fr 1fr, still 2 rows of 2; the operator ruling on
   §-locked KPI density is untouched). The divider is a border-right on the
   left-column tile only (odd = col 1 in row-major grid flow), so the line
   runs the full height of each row without doubling at the row gap. */
.pl-root .pl-kpi {
  padding: 2px 14px;
  min-width: 0;
}

.pl-root .pl-kpi:nth-child(odd) {
  padding-left: 0;
  border-right: 1px solid var(--pl-hair);
}

.pl-root .pl-kpi:nth-child(even) {
  padding-right: 0;
}

/* ── LOOK G3 · THE PRE-SEASON INK ROW ──────────────────────────────────────
   Four un-boxed stats between two hairlines, replacing the two washed tiles.

   NOT a new class. `data-pl-phase` is frozen (§1) and rides on `.pl-deck`
   (plDeckHtml), so this rule CANNOT reach the in-season card — a different
   phase value — and CANNOT reach the `week` drill, which is appended to
   `.pl-root` as a SIBLING of the deck, not a descendant. The GW1 flip is
   therefore structural in the stylesheet as well as in the emitter: no
   redeploy, no flag, and no dormant path to remember.

   ⚠ `grid-auto-columns` rather than `repeat(4, 1fr)`. plKpi returns "" for an
   unmeasured value, so a record with no percentile yields THREE tiles. Those
   must fill the row; a fixed four-column track would leave a hole where the
   missing stat would have been, which reads as a rendering fault rather than
   as an absent measurement. */
.pl-root [data-pl-phase="preseason"] [data-pl-card="team"] .pl-kpis {
  grid-template-columns: none;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 0;
  border-top: 1px solid var(--pl-hair);
  border-bottom: 1px solid var(--pl-hair);
}

.pl-root [data-pl-phase="preseason"] [data-pl-card="team"] .pl-kpi {
  background: none;
  border: none;
  box-shadow: none;
  border-radius: 0;
  padding: 14px 6px 14px 0;
}

.pl-root .pl-kpi-n {
  font-family: var(--font-display);
  font-size: 25px;
  font-weight: 600;
  line-height: 1;
  /* £102.4m in Cormorant's DEFAULT oldstyle figures renders as "£ıo2.4m" — the
     1 has no ascender and the 0 sits on the baseline as an o. Caught in a live
     render. Lining is inherited from .pl-root; tabular is added here so the two
     tiles in a row keep their numerals on the same optical column. */
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--pl-ink);            /* 17.41:1 light · 15.02:1 night, on --pl-card */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pl-root .pl-kpi-l {
  font-family: var(--font-mono);
  font-size: 8.5px;
  font-weight: 400;
  letter-spacing: 1.3px;
  line-height: 1.35;
  color: var(--pl-muted);          /* 5.46:1 light · 6.68:1 night, on --pl-card */
  margin-top: 5px;
  text-transform: uppercase;
  /* The label carries the movement suffix ("TEAM VALUE ▲ +0.9"), so it can run
     to two lines rather than truncate the number off the end. */
  overflow-wrap: anywhere;
}

/* Big-numbers-minimal-info pass, team card only. :where() keeps this at the
   SAME specificity as the base .pl-kpi-n rule above (adds 0 weight) so it
   wins purely on source order, like every other override in this file — and,
   critically, it does NOT reach the week-drill's own reuse of .pl-kpi-n
   (plKpi/plKpisHtml render there too, outside [data-pl-card="team"]). */
.pl-root :where([data-pl-card="team"]) .pl-kpi-n { font-size: 30px; }

/* --- who did what --- */

.pl-root .pl-wk { margin-top: 2px; }

/* Facelift Phase 2 — more air per row. Was gap:12px / padding:11px 0; a touch
   more on both axes is the whole "breathing room" ask for this list. Still
   `flex:none` (NO-SQUASH, above) and still narrow-width safe: `.pl-wk-name`
   keeps `min-width:0` + `overflow-wrap:anywhere` below, so this is a pure
   spacing change, not a new layout mode. The height-tier overrides
   (`.pl-wk-row { padding: 8px/7px 0; }` further down) are unaffected — they set
   their own value and win on source order regardless of this base number. */
.pl-root .pl-wk-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 11px 0;   /* facelift Phase 2 fix: 13px ate into 390x745's binding fit */
  border-bottom: 1px solid var(--pl-hair);
}

.pl-root .pl-wk-row:last-child { border-bottom: none; }

/* `.pl-wk-name` is a <span> that is blockified by being a flex item of
   `.pl-wk-row` (flex:1 1 auto below), but it is deliberately NOT `display:flex`
   itself. Its own children — the name text, then the nested `.pl-wk-role` —
   already flow in normal inline formatting: text first, then `.pl-wk-role`
   (display:block, below) wraps to its own line, which is what gives the
   two-line "name / caption" stack. A companion JS change is expected to add one
   more small inline child beside the name (a captain marker) — that slots into
   this same inline flow with no CSS change needed, because ordinary inline
   layout already wraps gracefully at narrow widths via `overflow-wrap: anywhere`
   below.
   ⚠ DO NOT make this `display:flex` to "make room" for that marker: the
   `[data-pl-src="history.past"] .pl-wk-role` override a few lines down depends
   on `.pl-wk-name` staying a normal block-formatting context so its own
   `display: inline` takes effect and the season meta sits beside the year. A
   flex item's outer display is forced to block regardless of the value a child
   specifies, so flexifying this parent would silently defeat that override —
   the exact "silent reflow" failure its own comment warns about. */
.pl-root .pl-wk-name {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-display);
  font-size: 16.5px;
  font-weight: 600;
  line-height: 1.2;   /* facelift Phase 2 fix: 1.25 ate into binding-viewport fit */
  color: var(--pl-ink);            /* 15.97:1 light · 16.54:1 night */
  overflow-wrap: anywhere;
}

.pl-root .pl-wk-role {
  display: block;
  font-family: var(--font-mono);
  font-style: normal;                /* the deck marks this up as <em> */
  font-weight: 400;
  font-size: 8.5px;
  letter-spacing: 1.2px;
  line-height: 1.4;
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  margin-top: 2px;   /* facelift Phase 2 fix: 3px ate into 390x745's binding fit */
  text-transform: uppercase;
  overflow-wrap: anywhere;
}

/* LOOK G3 — the SEASON row's meta sits INLINE beside the year, per the design
   of record ("2024/25   RANK 4,389,817 · TOP 38%" on one line).

   ⚠ SCOPED ON data-pl-src, AND THAT IS NOT OPTIONAL. `.pl-wk-role` has TWO
   emitters: plSeasonRowsHtml (the record) and plSquadRows (the in-season WHO
   MOVED YOU list), where the text is a player's position and mechanism and MUST
   stay stacked under the name. A blanket `display: inline` would silently
   reflow that list — and it is dormant until GW1, so no pre-season check would
   ever have caught it. The frozen `[data-pl-src="history.past"]` token
   distinguishes them without minting a selector, which is the §1-blessed
   pattern.

   This deliberately reaches the `week` drill's rows too: same renderer, same
   source token, same row shape on both surfaces — and it reclaims height inside
   an overlay that carries all nine seasons. */
.pl-root .pl-wk[data-pl-src="history.past"] .pl-wk-role {
  display: inline;
  margin-top: 0;
  margin-left: 9px;
}

/* Right-aligned on a fixed minimum so four rows of scores read as a COLUMN.
   Tabular figures alone are not enough: "8" and "16" still start at different
   x, and a ragged score column is the first thing that makes a dense card look
   thrown together. 34px holds three digits at 26px Cormorant. */
.pl-root .pl-wk-pts {
  flex: none;
  min-width: 34px;
  text-align: right;
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--pl-ink);
}

/* A blank is a FACT, not a direction — it dims, it does not go red. (The deck
   used --faint here; --faint is 1.92:1 and NON-TEXT ONLY, so this is --muted.) */
.pl-root .pl-wk-pts--mute { color: var(--pl-muted); }

/* Big-numbers-minimal-info pass, team card only — same reasoning as
   .pl-kpi-n above: real specificity (attribute + class) so it wins over the
   base rule for the card, but the week-drill's own reuse of .pl-wk-pts
   (outside [data-pl-card="team"]) stays at the base 26px, untouched. */
.pl-root [data-pl-card="team"] .pl-wk-pts { font-size: 24px; }


/* ---------------------------------------------------------------------------
   3 · CARD 02 — ACTIONS & WHY
   ------------------------------------------------------------------------ */

/* DECORATIVE BACKGROUND SHAPES (big-numbers-minimal-info pass) — same pure-
   CSS mechanism as card 01 (zero `fill:`, §6), but DIALED BACK: this is the
   densest face in the deck even after this pass's cuts, so both shapes are
   ~70% the linear size and ~60% the opacity of card 01's version, not the
   same footprint shrunk only by feel. `position:relative` was previously
   unset; `z-index:0` gives the card its own stacking context. */
.pl-root [data-pl-card="act"] {
  position: relative;
  z-index: 0;
}

.pl-root [data-pl-card="act"]::before {
  content: "";
  position: absolute;
  top: -35px;
  right: -35px;
  width: 98px;
  height: 98px;
  border-radius: 50%;
  border: 1.5px solid color-mix(in srgb, var(--pl-green) 8%, transparent);
  z-index: -1;
  pointer-events: none;
}

.pl-root [data-pl-card="act"]::after {
  content: "";
  position: absolute;
  bottom: 0;
  right: -25px;
  width: 91px;
  height: 70px;
  background: color-mix(in srgb, var(--pl-ink-violet) 6%, transparent);
  clip-path: polygon(100% 0, 100% 100%, 0 100%);
  z-index: -1;
  pointer-events: none;
}

.pl-root .pl-move { margin-top: 2px; }

.pl-root .pl-move-head {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 3px;
}

/* The real two-sided swap stacks the headline above the gauge instead of
   sharing one row with it — the row form is what starved the gauge's track
   to 0 width (measured, not assumed: the two flex:none labels either side of
   it left nothing for `flex:1 1 auto` to distribute when squeezed onto one
   line with a big title). `:has()` scopes this to only the variant that
   contains `.pl-move-headline`; the pre-season draft-target case (a single
   short sentence in `.pl-move-title`) keeps the original row layout. */
.pl-root .pl-move-head:has(.pl-move-headline),
.pl-root .pl-move-head:has(.pl-move-bs) {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

.pl-root .pl-move-title {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-display);
  font-size: 38px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.5px;
  color: var(--pl-ink);            /* 15.97:1 light · 16.54:1 night */
  overflow-wrap: anywhere;         /* a long surname cannot widen the deck */
  font-variant-numeric: lining-nums;
}

/* THE THREE-LINE HEADLINE (operator's direct spec, replacing a naturally-
   wrapping sentence). "Bring in" / "{in name}," / "sell {out name}." — each
   its own block, so the break points are FIXED regardless of name length,
   never left to whatever the deck's width happens to allow. Sized well below
   `.pl-move-title`'s 38px specifically so three lines cost less height than
   the old sentence's worst case (it was wrapping to 5), and so a typical
   name does not wrap its own line at 360px — a genuinely long one still may,
   which is accepted (a 4th line beats an unpredictable 5). */
.pl-root .pl-move-headline {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.3px;
  color: var(--pl-ink);            /* 15.97:1 light · 16.54:1 night */
  font-variant-numeric: lining-nums;
}

.pl-root .pl-move-line {
  font-size: 26px;
  line-height: 1.15;
  overflow-wrap: anywhere;         /* a long surname cannot widen the deck */
}

/* THE BUY/SELL HEADLINE (big-numbers-minimal-info pass) — replaces the
   three-line "Bring in X, sell Y." sentence above for the real two-sided-swap
   case. Side by side, not stacked: stacking two blocks this size would
   roughly double the height cost of the sentence it replaces on the card
   with the least room to give; a row keeps it to one "row" of cost. */
.pl-root .pl-move-bs {
  display: flex;
  align-items: flex-start;
  gap: 16px;
}

.pl-root .pl-bs-block {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.pl-root .pl-bs-sell {
  border-left: 1px solid var(--pl-hair);
  padding-left: 16px;
}

.pl-root .pl-bs-word {
  font-family: var(--font-display);
  font-size: 64px;
  font-weight: 700;
  line-height: 0.92;
  letter-spacing: -0.5px;
  color: var(--pl-ink);            /* 15.97:1 light · 16.54:1 night */
  font-variant-numeric: lining-nums;
}

.pl-root .pl-bs-name {
  margin-top: 2px;
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -0.3px;
  color: var(--pl-ink);            /* 15.97:1 light · 16.54:1 night */
  overflow-wrap: anywhere;         /* a long surname cannot widen the deck */
}

/* SELL reads one step down the ink hierarchy — still full-strength dark body
   ink (--pl-prose: 12.06:1 light · 13.79:1 night), just not the --pl-ink the
   BUY side gets, so BUY reads as the primary action at a glance. */
.pl-root .pl-bs-sell .pl-bs-word,
.pl-root .pl-bs-sell .pl-bs-name {
  color: var(--pl-prose);
}

.pl-root .pl-verdict {
  flex: none;
  align-self: flex-start;
  margin-top: 6px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;                /* mono ships 400/500 only — see the chip note */
  letter-spacing: 0.5px;
  line-height: 1.5;
  padding: 4px 9px;
  border-radius: 6px;
  background: var(--pl-wash-neutral);
  color: var(--pl-ink-chip);       /* 6.83:1 light · 12.10:1 night */
  white-space: nowrap;
  text-transform: uppercase;
}

.pl-root .pl-verdict--jump    { background: var(--pl-verdict-bg);  color: var(--pl-verdict-ink); }
.pl-root .pl-verdict--hold    { background: var(--pl-wash-neutral); color: var(--pl-ink-chip); }
.pl-root .pl-verdict--monitor { background: var(--pl-wash-amber);   color: var(--pl-ink-amber); }

/* ── THE CONFIDENCE/EVIDENCE GAUGE — a parallel JS pass's new addition beside
   the verdict pill. Class names assumed pending that agent's landed markup
   (see pulse.css/pulse.js reconciliation note); reconcile if they differ.

   Deliberately the cheapest possible bar: a 6px track (inside the 4-8px band
   the operator set), NO new token — the track reuses `--pl-wash-neutral`
   (the same wash `.pl-verdict--hold` already sits on) and the fill reuses
   `--pl-green-text`, already theme-aware light/night. The label word rides
   `.pl-verdict`'s own type scale (10px mono/500/0.5px/1.5 line-height,
   uppercase) rather than inventing a size step, per the operator's ruling that
   this card affords no new type scale for one more label. `margin-top` is the
   ONLY new vertical cost this adds and is held to 4px — the minimum needed to
   read as its own row under `.pl-move-head` rather than crowding the verdict
   pill's own 6px top margin above it. */
.pl-root .pl-gauge {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 4px;
}

/* The visible relabel from CONFIDENCE — the `title` attribute alone is close
   to invisible on a touch deck. Same quiet-green eyebrow token as every other
   section label on this card (facelift Phase 1), not the loud CTA green. */
.pl-root .pl-gauge-k {
  flex: none;
  font-family: var(--font-mono);
  font-size: 8.5px;
  font-weight: 500;
  letter-spacing: 1.5px;
  color: var(--pl-green-quiet);
  text-transform: uppercase;
}

.pl-root .pl-gauge-track {
  flex: 1 1 auto;
  min-width: 0;
  height: 6px;
  border-radius: 3px;
  background: var(--pl-wash-neutral);
  overflow: hidden;
}

.pl-root .pl-gauge-fill {
  height: 100%;
  border-radius: 3px;
  background: var(--pl-green-text);
}

.pl-root .pl-gauge-label {
  flex: none;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;                /* mono ships 400/500 only */
  letter-spacing: 0.5px;
  line-height: 1.5;
  color: var(--pl-ink-chip);       /* 6.83:1 light · 12.10:1 night, same pair as .pl-verdict */
  text-transform: uppercase;
  white-space: nowrap;
}

/* --- FDR ticker --- */

.pl-root .pl-ticker { margin: 8px 0 4px; }   /* facelift Step 5: was 14px top */

.pl-root .pl-ticker-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 400;
  letter-spacing: 2px;
  color: var(--pl-green-quiet);    /* facelift Phase 1 — was --pl-muted */
  margin-bottom: 8px;
  text-transform: uppercase;
  overflow-wrap: anywhere;
}

.pl-root .pl-fdr {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

/* The deck spec pins each pill at 50px and lets the row end short. These GROW
   to share the gutter-to-gutter width instead — the ticker is the one full-
   bleed band on card 02 and a five-pill run that stops 70px early is what makes
   a dense page read half-finished. The cap keeps a 2- or 3-fixture run (a
   blank, or a partly-known DGW) from ballooning into slabs. */
.pl-root .pl-fdr-pill {
  flex: 1 1 auto;
  box-sizing: border-box;
  min-width: 42px;
  max-width: 68px;
  height: 34px;
  padding: 0 4px;
  border-radius: 7px;
  font-family: var(--font-mono);
  font-style: normal;                /* the deck marks these up as <i> */
  font-size: 10.5px;
  font-weight: 500;                  /* mono ships 400/500 only */
  letter-spacing: 0.3px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  white-space: nowrap;
  background: var(--pl-wash-neutral);
  color: var(--pl-ink-chip);         /* unranked fallback: 6.83 / 12.10 */
}

/* THE TICKER AT ROW SCALE. Both scanner mockups run a 3-fixture strip inside a
   card row (`.cfoot .fdr i` at 17×16), and the frozen vocabulary has no second
   pill class for it — `.pl-fdr-pill` is the only one there is. So the SIZE is
   scoped to the container instead of minted as a modifier: at card 02's 34px a
   five-pill band inside a 90px scanner row would be a third of the row.
   The hue ramp and the per-pill ink are untouched, so all five pills keep the
   contrast computed for them (4.67–7.24:1, theme-invariant) — only the box
   moves. `flex: none` because a row strip reads as a fixture RUN, left to
   right; it must not stretch gutter-to-gutter the way the card-02 band does. */
.pl-root .pl-scan-row .pl-fdr { gap: 3px; }

.pl-root .pl-scan-row .pl-fdr-pill {
  flex: none;
  min-width: 26px;
  max-width: 42px;
  height: 18px;
  border-radius: 4px;
  font-size: 8.5px;
  letter-spacing: 0.2px;
}

.pl-root .pl-fdr-pill--1 { background: var(--pl-fdr-1); color: var(--pl-fdr-ink-1); }
.pl-root .pl-fdr-pill--2 { background: var(--pl-fdr-2); color: var(--pl-fdr-ink-2); }
.pl-root .pl-fdr-pill--3 { background: var(--pl-fdr-3); color: var(--pl-fdr-ink-3); }
.pl-root .pl-fdr-pill--4 { background: var(--pl-fdr-4); color: var(--pl-fdr-ink-4); }
.pl-root .pl-fdr-pill--5 { background: var(--pl-fdr-5); color: var(--pl-fdr-ink-5); }

/* --- reason ledger --- */

.pl-root .pl-reason {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 9px 0;
  border-bottom: 1px solid var(--pl-hair);
}

.pl-root .pl-reason:last-child { border-bottom: none; }

/* One fixed key column so the four reasons read as a LEDGER — the label rail is
   the whole point of the block, and a key column that resizes per row turns it
   back into four unrelated sentences. */
.pl-root .pl-reason-k {
  flex: none;
  width: 66px;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 400;
  letter-spacing: 1.5px;
  line-height: 1.5;
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  text-transform: uppercase;
  overflow-wrap: anywhere;
}

.pl-root .pl-reason-v {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-display);
  font-size: 17.5px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--pl-ink);            /* 15.97:1 light · 16.54:1 night */
  overflow-wrap: anywhere;
}

/* `.pl-reason-v` is NOT in the §1 amendment's `data-pl-dir` set — the frozen
   vocabulary gives this one element its own `--up` modifier, so it stays a
   class here. The attribute is for the four elements the amendment names. */
.pl-root .pl-reason-v--up { color: var(--pl-green-text); }  /* 5.73 / 9.74 */

/* ── LOOK G3 · THE TILES — card 02's FACE form of the same two reasons ──────
   Two washed boxes replacing two ledger rows. The drill keeps the rows.

   NO NEW TOKEN. The design of record specifies #EEF3EC; `--pl-ledger-bg` is
   #EEF5EE — two points of green and blue apart, a contrast ratio of 1.014:1
   between them, i.e. indistinguishable — and it already carries a night value
   (#17211C) that a freshly minted token would not.

   ⚠ THE ICON SITS INLINE BESIDE THE LABEL, NOT ABOVE IT (operator ruling).
   The design of record stacks icon / label / value. Priced against the real
   budget that stack costs 37.6px per tile, and the tiles replace rows whose
   value column is 268px wide with one that is 144px — so sentences that set in
   ONE row-line need two or three tile-lines. At the specced geometry a 2-line
   tile overflows 390x745 by 1.41px and 375x667 by 22.2px, and the LIVE queue
   string is ~46 characters, i.e. three lines. Inline, the 37.6px stack becomes
   a 16px row. Everything the design calls binding survives — icon, label,
   value, washed box, dynamic selection — and the card fits at both binding
   viewports with the live payload. */
.pl-root .pl-tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: start;   /* facelift Fix 1: grid default is stretch, which
                            forces the shorter tile (THE QUEUE) to match the
                            taller one (FIXTURES)'s height — pure dead space,
                            zero information. Each tile now sizes to its own
                            content. */
  gap: 10px;
  margin-top: 8px;   /* facelift Step 5: was 12px */
}

.pl-root .pl-tile {
  background: var(--pl-ledger-bg);
  border-radius: 10px;
  padding: 12px;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* The label rail becomes a label ROW: icon and key on one baseline. */
.pl-root .pl-tile-k {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 400;
  letter-spacing: 1.5px;
  line-height: 1.4;
  color: var(--pl-muted);          /* 4.92:1 on this wash — see below */
  text-transform: uppercase;
  min-width: 0;
}

/* ⚠ 4.92:1 IS A MARGIN, NOT A CLEARANCE. `--pl-muted` is documented as the
   small-text floor at 5.01:1 on paper; on the ledger wash it loses 0.09 and
   still clears 4.5. It has no room to be tuned down — if this pair ever needs
   widening the token to reach for is --pl-p90-ink, not a new hex. */

.pl-root .pl-tile-i {
  flex: none;
  /* no CSS fill declaration and no colour of any kind: the stroke lives in the
     `stroke=` attribute plTileIconHtml emits. A `currentColor` stroke would be
     repainted by the unrestricted [data-pl-dir] colour rules, which inherit. */
}

.pl-root .pl-tile-v {
  margin-top: 7px;
  min-width: 0;
  font-family: var(--font-display);
  font-size: 15.5px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--pl-ink);            /* 15.70:1 light · 14.61:1 night */
  overflow-wrap: anywhere;
}

/* The honesty tag must not take a line of its own inside a 144px column: at
   ~44px, "DERIVED" would wrap and cost a whole extra line — one full row's
   worth of height. A standalone carve-out rather than a member of the
   `.pl-scan-top / .pl-ticker-label / .pl-legend-row / .pl-ovh` group below,
   following the `.pl-conv .pl-tag` precedent: same shape, same reason, and it
   keeps the tile's rules together. (0,3,0) beats the base `.pl-tag` rule's
   (0,2,0), so source order is not doing the work here. */
.pl-root .pl-tile-v .pl-tag { margin-top: 0; }

/* --- captain box --- */

.pl-root .pl-capbox {
  display: flex;
  align-items: center;
  gap: 11px;
  background: var(--pl-card);
  border: 1px solid var(--pl-hair);
  border-radius: 12px;
  box-shadow: var(--pl-lift);
  padding: 11px 13px;
  margin-top: 14px;
}

.pl-root .pl-capbox-body { flex: 1 1 auto; min-width: 0; }

.pl-root .pl-capbox-name {
  font-size: 16px;
  font-weight: 700;                /* Inter 700 is loaded — a real weight */
  color: var(--pl-ink);            /* 17.41:1 light · 15.02:1 night, on --pl-card */
  line-height: 1.2;
  overflow-wrap: anywhere;
}

.pl-root .pl-capbox-meta {
  display: block;
  font-size: 11.5px;
  color: var(--pl-muted);          /* 5.46:1 light · 6.68:1 night, on --pl-card */
  margin-top: 1px;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

.pl-root .pl-capbox-tag {
  flex: none;
  align-self: center;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 500;                /* mono ships 400/500 only */
  letter-spacing: 0.5px;
  line-height: 1.5;
  color: var(--pl-green-text);     /* 5.44:1 light · 6.43:1 night, on the wash */
  background: var(--pl-wash-green);
  padding: 4px 8px;
  border-radius: 6px;
  white-space: nowrap;
  text-transform: uppercase;
}

.pl-root .pl-alt {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 14.5px;
  line-height: 1.35;
  color: var(--pl-quiet);          /* 8.62:1 light · 11.55:1 night */
  margin-top: 13px;
}


/* ---------------------------------------------------------------------------
   4 · CARD 03 — THE SCANNER
   ------------------------------------------------------------------------ */

/* DECORATIVE BACKGROUND SHAPES (big-numbers-minimal-info pass) — top-right
   cluster only (a triangle + a circle together), NOT the bottom-left mark a
   reference mockup also showed: real footer text lives there, and reflow
   risk across 5 widths/2 themes wasn't judged worth it — 2 shapes here,
   matching cards 01/02's pattern, a deliberate mockup deviation in service
   of "less info." Pure CSS (clip-path + border-radius, ZERO `fill:`, §6).
   Capped so the cluster's bounding box stays inside the folio/eyebrow zone
   — the folio's own line box + padding + margin already spend ~54-60px
   before `.pl-scan-num` begins, so this never crosses into the numeral. */
.pl-root [data-pl-card="scan"] {
  position: relative;
  z-index: 0;
}

.pl-root [data-pl-card="scan"]::before {
  content: "";
  position: absolute;
  top: -16px;
  right: -16px;
  width: 64px;
  height: 64px;
  background: var(--pl-ink);
  clip-path: polygon(100% 0, 100% 100%, 0 0);
  opacity: 0.12;
  z-index: -1;
  pointer-events: none;
}

.pl-root [data-pl-card="scan"]::after {
  content: "";
  position: absolute;
  top: 6px;
  right: 40px;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--pl-ink);
  opacity: 0.12;
  z-index: -1;
  pointer-events: none;
}

/* THE VALUE LEDGER — the accounting game's one line, per the risers mock's
   `.tv` scoreboard and the deck's `.ledger`.
   `column-gap`/`row-gap` split: at 320px the delta legitimately wraps to a
   second line (measured), and an 8px row gap there is 8px of nothing between
   two halves of one sentence. Columns keep the deck's 8px; a wrap costs 4px. */
.pl-root .pl-ledger {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  column-gap: 8px;
  row-gap: 4px;
  font-size: 12.5px;
  color: var(--pl-muted);
  background: var(--pl-ledger-bg);
  border: 1px solid var(--pl-ledger-bd);
  border-radius: 11px;
  padding: 10px 13px;
  text-transform: uppercase;
  letter-spacing: 0.6px;
}

.pl-root .pl-ledger-v {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--pl-ink);
  letter-spacing: 0;
  text-transform: none;
}

/* ⚠ NO UNCONDITIONAL DIRECTION. `.pl-ledger-delta` is one of the four elements
   the §1 amendment names as carrying `data-pl-dir`, and the contract's standing
   assertion is that "a directional colour applied WITHOUT the attribute is a
   NO-SHIP". G1 hard-coded --pl-green-text here, which would have painted a
   FALLING team value green on any payload where the emitter omitted the
   attribute — a wrong claim that looks deliberate. It now inherits the ledger's
   own ink and takes its colour ONLY from the attribute block at the end of this
   file (equal specificity, later wins). A delta with no attribute makes no
   directional claim and reads neutral, which is the honest default. */
.pl-root .pl-ledger-delta {
  flex: 0 0 auto;
  margin-left: auto;
  font-weight: 700;                /* Inter 700 — .pl-ledger is the body font */
  color: inherit;                  /* --pl-muted from .pl-ledger: 4.92 / 6.50 */
  /* nowrap is deliberate: "▲ +0.9 since GW1" is one claim and a break between
     the arrow and its number would read as two. `flex: 0 0 auto` keeps it at
     its own width so the flex WRAP — not a mid-claim line break, and not an
     overflow past the rounded box — is what handles a narrow card. Measured:
     one line at 390, two at 320, clipped at neither. */
  white-space: nowrap;
  font-variant-numeric: lining-nums tabular-nums;
}

.pl-root .pl-scan { margin-top: 2px; }

/* 8px, not the deck spec's 9px. The spec was drawn against an 824px mock with
   no masthead and no dot rail; the shipped card gives up 52px to the masthead
   and 60px to the dots/hint. Five rows × 2px is the last 10px that puts a
   five-row board inside a 390×844 card without touching a single type size. */
.pl-root .pl-scan-row {
  padding: 8px 0;
  border-bottom: 1px solid var(--pl-hair);
}

.pl-root .pl-scan-row:last-child { border-bottom: none; }

/* `column-gap`/`row-gap` split rather than one `gap`: when a long name pushes
   the badge or the watch control onto a second line, a 9px row gap re-opens
   the vertical space the row was tightened to close. The columns keep the
   deck's 9px rhythm; a wrapped line costs 5px, not 9px. */
/* align-items: flex-start (not center) — anchors the crest and the badge
   pill to the TOP of the row rather than mid-height, so the badge reads as
   sitting near the row's top-right rather than floating at vertical middle.
   Height-neutral: cross-axis alignment doesn't change the line's height,
   only where children sit inside it, so this carries zero fit cost. */
.pl-root .pl-scan-top {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  column-gap: 9px;
  row-gap: 5px;
}

.pl-root .pl-scan-name {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-display);  /* serif name, matching .pl-wk-name's card 01 precedent */
  font-size: 16px;
  font-weight: 700;                /* 700 is a real loaded weight for both Inter and Cormorant */
  line-height: 1.2;                 /* same cap .pl-wk-name uses — 1.25 there "ate into binding-viewport fit" */
  color: var(--pl-ink);            /* 15.97:1 light · 16.54:1 night */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pl-root .pl-scan-own {
  flex: none;
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 400;
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  font-variant-numeric: lining-nums tabular-nums;
  white-space: nowrap;             /* "12.4%" never breaks between digit and % */
}

/* THE WATCH CONTROL — A 44px TAP TARGET WITHOUT A 44px ROW.
   ⚠ MEASURED DEFECT, not a preference. `min-height: 44px` made this button the
   tallest item in `.pl-scan-top`, so every scanner row opened with a 44px band
   before a single word of the read. Five rows × 17px of pure button padding is
   85px, and card 03 measured 886px of content into a 792px card at 390×844 —
   i.e. the button WAS the overflow. The card must not scroll vertically (the
   deck scrolls horizontally; a scrolling card is a gesture conflict).

   The TARGET DOES NOT SHRINK. The button BOX shrinks to its content and an
   absolutely-positioned `::after` restores a >= 44px-tall hit rectangle across
   its full width. Pointer hit-testing resolves to the pseudo-element's box, so
   the 44px is real and not nominal — it just stops paying rent in layout.
   Verified by elementFromPoint at all four corners of the 44px rect, in both
   the in-row and row-child placements (see the gate report).

   The pseudo carries `content: ""` and no text: it is not a DOM text node, so
   it cannot reach a golden's TEXT hash, and the button's own `aria-label` is
   what is announced — the accessible name is not computed from contents.

   The 10px of overspill above and below lands on this row's hook and the
   previous row's p90 chips, both inert prose. Two watch controls cannot
   collide: rows are ~95px apart and the expansion is 10px per side.

   `margin-left: auto` pins the control to the row's right edge in BOTH
   placements the frozen vocabulary permits — inside `.pl-scan-top`, and as a
   direct child of `.pl-scan-row` (see the `>` rule below). The vocabulary does
   not say which, so neither does this file. */
.pl-root .pl-scan-add {
  position: relative;
  flex: none;
  margin-left: auto;
  min-width: 44px;
  min-height: 24px;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 3px 2px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;                /* mono ships 400/500 only */
  letter-spacing: 1px;
  line-height: 1.5;
  color: var(--pl-green-text);     /* 5.73:1 light · 9.74:1 night */
  background: none;
  border: none;
  cursor: pointer;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}

.pl-root .pl-scan-add::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);     /* centred on the visual box, whatever its height */
}

/* The row-child placement. `margin-left: auto` only right-aligns a BLOCK-level
   box, and the base rule leaves this an inline-flex for its in-row life, so the
   display is switched here rather than everywhere. `fit-content` is the other
   half: a plain block-level flex box is `width: auto` = FULL WIDTH, which
   swallows the auto margin and leaves the label stranded in the middle of the
   row with a full-width hit strip under it. */
.pl-root .pl-scan-row > .pl-scan-add {
  display: flex;
  width: fit-content;
  margin-top: 6px;
}

/* The ring stays on the VISUAL box, not the hit rectangle: a focus ring drawn
   round an invisible 44px band reads as a stray rectangle floating in the row. */
.pl-root .pl-scan-add:focus-visible {
  outline: 2px solid var(--pl-green-text);
  outline-offset: 2px;
  border-radius: 6px;
}

.pl-root .pl-scan-add:active { color: var(--pl-ink); }

/* ⚠ FACELIFT — CARD 03 ROW LAYOUT (supervisor review, issues 1+2).
   MEASURED: `.pl-scan-top` wrapped by content length, not by design — a long
   surname pushed `.pl-badge` + `.pl-scan-add` onto their own line, a short one
   left them inline beside the name/price. Same markup, two different-looking
   rows on the same board.

   Scoped to `data-pl-src="shortlist"` — the ONE `.pl-scan-row` shape that
   emits both `.pl-badge` and `.pl-scan-add` inside `.pl-scan-top`
   (`plScanRowHtml`). The other three `.pl-scan-top` shapes in this file (the
   drill's theme row, the Next 5 subject row, the captain shortlist row)
   contain neither element and are untouched.

   RETIRED (big-numbers-minimal-info pass): the whole forced-second-line
   mechanism that used to live here. It existed to push the FORM WATCH /
   VALUE · DEFCON badge + the watch star onto a deterministic second line —
   with that badge cut entirely (`plScanBadgeHtml` no longer called from
   `plScanRowHtml`), the mechanism has nothing left to break around and was
   forcing an unconditional, wasted extra line on every row, even ones with
   room to spare on line 1. Plain flex-wrap (the base `.pl-scan-top` rule)
   already right-aligns the star via its own `margin-left:auto` and wraps
   only when a row genuinely doesn't fit — zero replacement needed. */

/* ---------------------------------------------------------------------------
   v4 G2 · CARD 02 — the conviction strip and the NEXT FIVE circles
   --------------------------------------------------------------------------- */

/* CONVICTION. A row, not a tile: a tile costs a box and this card entered G2
   with 1.94px of live headroom. The stars and the band word sit BESIDE the
   frozen `.pl-verdict` count pill, which stays the honesty carrier. */
.pl-root .pl-conv {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

.pl-root .pl-conv-stars {
  font-size: 12px;
  letter-spacing: 1.5px;
  line-height: 1;
  color: var(--pl-green-text);     /* 5.73:1 light · 9.74:1 night */
}

/* `.pl-conv .pl-tag` is the BAND WORD. It rides the frozen tag styling but must
   not inherit the standalone tag's top margin — it is a companion here, the
   same carve-out `.pl-scan-top .pl-tag` already makes. */
.pl-root .pl-conv .pl-tag { margin-top: 0; }

/* THE NEXT FIVE CIRCLES.

   ⚠ HORIZONTAL FIT WAS THE DEFECT IN THE SOURCE ARTWORK. The mockup pins each
   circle at a hard 52px: five of those plus a separate 66px FDR chip needs
   371px inside a 346px column at 390, and 331px at 375 — it overflows sideways
   before a single vertical pixel is spent. Two fixes, both structural:
     · the FDR chip folds into the ticker label, which already prints the mean;
     · the circle IS the frozen `.pl-fdr-pill`, re-boxed by its container, so it
       keeps `flex: 1 1 auto` and cannot exceed its share of the row.
   The `--1..5` background/ink rules are NOT touched — that is what the design
   spec's Ruling 3 implementation note promises. */
.pl-root .pl-next5 .pl-fdr {
  flex-wrap: nowrap;               /* five fixtures are one row, never two */
  gap: 6px;
  align-items: flex-start;
}

.pl-root .pl-fx {
  flex: 1 1 0;
  min-width: 0;                    /* a flex item defaults to min-width:auto */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

/* The circle. Width comes from the flex share, so it shrinks with the viewport
   instead of overflowing it; `aspect-ratio` keeps it round without a fixed px. */
.pl-root .pl-next5 .pl-fdr-pill {
  width: 100%;
  max-width: 46px;
  min-width: 0;
  height: auto;
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  font-size: 10.5px;
  letter-spacing: 0.2px;
  padding: 0;
}

.pl-root .pl-fx-ha {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 400;                /* mono ships 400/500 only */
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  line-height: 1;
}

/* --- badges (also used by the §7 legend) --- */

.pl-root .pl-badge {
  flex: none;
  display: inline-flex;
  align-items: center;
  font-family: var(--font-mono);
  font-size: 8.5px;
  font-weight: 500;                /* mono ships 400/500 only */
  letter-spacing: 0.5px;
  line-height: 1.5;
  padding: 3px 6px;
  /* facelift: full pill, matching .pl-chip's capsule shape — a corner-radius
     change only, zero cost to box height/width, so it does not touch the
     binding-viewport fit budget. */
  border-radius: 999px;
  background: var(--pl-wash-neutral);
  color: var(--pl-ink-chip);       /* 6.83:1 light · 12.10:1 night */
  white-space: nowrap;
  text-transform: uppercase;
}

.pl-root .pl-badge--cameo { background: var(--pl-wash-amber);  color: var(--pl-ink-amber); }
.pl-root .pl-badge--role  { background: var(--pl-wash-green);  color: var(--pl-green-text); }
.pl-root .pl-badge--value { background: var(--pl-wash-blue);   color: var(--pl-ink-blue); }
.pl-root .pl-badge--watch { background: var(--pl-wash-violet); color: var(--pl-ink-violet); }

/* `.pl-hook` does two jobs in the frozen vocabulary: the §7 explainer's
   standalone statement ("Before the crowd. Before the price.") and the one-line
   read under each scanner row. Same voice, different weight in the page, so the
   size is contextual rather than a second class — the standalone one is a pull
   quote, the in-row one is a caption. */
.pl-root .pl-hook {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 17.5px;
  line-height: 1.3;
  color: var(--pl-prose);          /* 12.06:1 light · 13.79:1 night */
  margin: 4px 0 2px;
}

.pl-root .pl-scan-row .pl-hook {
  font-size: 14.5px;
  line-height: 1.28;
  margin: 4px 0 0;
}

/* A `.pl-reason` NESTED INSIDE a `.pl-scan-row` is a sub-line of that row, not
   a peer of card 02's four ledger reasons. Both drills compose it that way —
   the board rows hang a canon citation off a scanner row, and the risers rows
   hang their price and demand lines off one — so the key rail is kept (it is
   the whole point of the block) and the card-level padding and divider are
   dropped. Without this the LAST nested reason draws a hairline 8px above the
   row's own bottom hairline: a double rule that reads as a rendering fault. */
.pl-root .pl-scan-row .pl-reason {
  padding: 5px 0 0;
  border-bottom: none;
}

/* …and at the row's density the reason value is a caption, not card 02's
   17.5px display line, which would out-shout the row's own name. */
.pl-root .pl-scan-row .pl-reason-v {
  font-family: var(--font-body);   /* Inter — 500 is a REAL loaded weight */
  font-size: 13px;
  font-weight: 500;
  line-height: 1.35;
  /* every value on these lines is a number in a stacked rail — "1st of 12",
     "+1.8 pp/day", "8.1% then, 12.4% now" — so tabular is what keeps the
     column from jittering row to row. Lining is inherited from .pl-root, but
     it is restated because this rule changes the FONT and a numeral class that
     depends on an ancestor for its figure style is one refactor from oldstyle. */
  font-variant-numeric: lining-nums tabular-nums;
}

/* G4 item 7 — READING TEXT IN A DRILL USES THE SERIF READING FACE.

   The rule above is right for the RISERS rows, where every value is a number in
   a stacked rail ("1st of 12", "+1.8 pp/day") and tabular Inter is what keeps
   the column from jittering. It was wrong for the Gaffer's board, where the
   same element carries a CANON PARAGRAPH — three lines of prose set in a
   synthetic-looking heavy sans, which reads as UI chrome in an editorial system.

   Told apart by PROVENANCE, not by a new class: a canon citation is the only
   `.pl-reason` in a scan row sourced from `gaffer.cards`, and `data-pl-src` is
   already frozen (§2) and already on the parent. No selector is minted, and the
   §1 vocabulary is untouched.

   14.5px matches `.pl-hook` and `.pl-alt`, the two other passages of reading
   prose on this surface, so the drill now has ONE reading face at ONE size.
   Tabular numerals are dropped deliberately — this is a sentence, not a rail. */
.pl-root .pl-scan-row .pl-reason[data-pl-src="gaffer.cards"] .pl-reason-v {
  font-family: var(--font-display);
  font-size: 14.5px;
  font-weight: 400;
  line-height: 1.4;
  font-variant-numeric: normal;
}

/* The honest-empty passages inside a drill are prose too — same face, same
   size. On a CARD `.pl-empty` stays as it was: it sits in a denser column and
   is often a one-line status, not a passage. */
.pl-root .pl-drill-body .pl-empty {
  font-family: var(--font-display);
  font-size: 14.5px;
  line-height: 1.4;
}

.pl-root .pl-p90 {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 6px;
}

.pl-root .pl-p90-chip {
  font-family: var(--font-mono);
  font-size: 10px;                 /* big-numbers-minimal-info pass, was 8.5px — the
                                       chip count dropped to <=2, freeing room */
  font-weight: 400;
  line-height: 1.5;
  background: var(--pl-p90-bg);
  border: 1px solid var(--pl-hair);
  /* facelift: full capsule, matching `.pl-badge`'s and `.pl-chip`'s pill shape
     elsewhere on this card — a corner-radius change only, zero cost to box
     height/width, so it does not touch the binding-viewport fit budget. */
  border-radius: 999px;
  padding: 3px 8px;                 /* was 2px 6px */
  color: var(--pl-p90-ink);        /* 9.44:1 light · 11.49:1 night */
  white-space: nowrap;
  /* the unit text ships lowercase from `plScanChipsInner` ("form", "pts",
     "GI/90") but reads as a label, not prose — uppercase matches the same
     idiom `.pl-badge` and `.pl-chip` already use for their labels. JetBrains
     Mono is a true monospace face, so this is a zero-width-cost change: every
     glyph keeps its advance width whichever case it renders. */
  text-transform: uppercase;
  /* every chip is "<number> <unit>" — lining is inherited, tabular keeps a
     column of 1.9→4.4 style deltas from jittering between rows */
  font-variant-numeric: lining-nums tabular-nums;
}

.pl-root .pl-p90-chip b {
  font-size: 13px;                  /* big-numbers-minimal-info pass — was
                                        inheriting the chip's 8.5px */
  color: var(--pl-ink);            /* 15.41:1 light · 13.16:1 night, on --pl-p90-bg */
  font-weight: 500;                /* mono ships 400/500 only */
}

/* THE WATCH/FLAG TAG (`plScanTierHtml` — already `.pl-chip.pl-chip--climb|
   neutral`, inside `.pl-p90` beside the stat chips above) is now the row's
   ONE tag, since the FORM WATCH/VALUE·DEFCON badge is cut. Scoped here, NOT
   on the shared base `.pl-chip` rule — that class also builds the unrelated
   `.pl-chips` posture-pill container on cards 01/02. Deliberately squarer
   (8px radius) than the pill-shaped stat chips beside it, so the one tag
   that means "act on this" reads visually distinct from the ones that are
   just numbers. */
.pl-root .pl-scan-row .pl-p90 .pl-chip {
  font-size: 9.5px;
  padding: 3px 7px;
  border-radius: 8px;
}


/* ---------------------------------------------------------------------------
   5 · SHARED — jersey, tags, empty, count, link, legend, calm
   ------------------------------------------------------------------------ */

/* HARD RULE: no CSS fill declaration here or anywhere in this file. The body and
   the club code colour arrive as `fill=` attributes from plBadgeHtml, sourced
   from the ONE frozen gafKitFor map. A CSS fill would beat those attributes
   and silently repaint a club. */
/* The ring is the deck spec's `inset 0 0 0 1.5px rgba(0,0,0,.08)` — the hairline
   that stops a light kit (Spurs, Fulham, Leeds) from dissolving into a white
   tile. It is drawn OUTSIDE the box rather than inset, because an inset shadow
   on a replaced element paints under its own content and would never be seen.
   It costs no layout (box-shadow is not in flow) and it is a NON-TEXT
   decoration, so no contrast floor applies.
   ⚠ If plBadgeHtml (G2) draws its own stroke on the badge circle, delete this
   one declaration rather than de-tuning the SVG — colour must stay in `fill=`. */
.pl-root .pl-jersey {
  flex: none;
  display: block;
  border-radius: 50%;
  overflow: visible;
  box-shadow: 0 0 0 1px var(--pl-jersey-ring);
}

/* An unresolved club is FLAGGED, not merely quiet: the dashed ring replaces the
   solid one rather than doubling it, so "we have no colour for this" reads as a
   deliberate state and not as a rendering artefact. (§3.2: in a populated
   fixture the count of these must be 0.) */
.pl-root .pl-jersey--unknown {
  box-shadow: none;
  outline: 1px dashed var(--pl-faint);   /* NON-TEXT use of --pl-faint */
  outline-offset: 1px;
  border-radius: 50%;
}

/* Geometry/interaction only — never colour. The club code's colour is the
   `fill=` attribute on the <text> node and must stay the only source. */
.pl-root .pl-jersey-code,
.pl-root .pl-jersey-code--light,
.pl-root .pl-jersey-code--dark {
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

/* --- honesty tags --- */

.pl-root .pl-tag {
  display: inline-block;
  font-family: var(--font-mono);
  /* EXPLICIT. `plTag()` composes into .pl-scan-top, .pl-ticker-label and
     plOvhTagged, and two of those sit beside Inter-700 text. With no weight of
     its own the tag INHERITED 700 — a weight JetBrains Mono does not ship, so
     the browser synthesised it by thickening an 8.5px glyph. Pinned to 500,
     the heaviest real mono weight index.html loads (contract §6). */
  font-weight: 500;
  font-size: 8.5px;
  letter-spacing: 0.4px;
  line-height: 1.5;
  color: var(--pl-muted);
  margin-top: 8px;
  text-transform: uppercase;
}

.pl-root .pl-tag--hard     { color: var(--pl-prose); }      /* 12.06 / 13.79 */
.pl-root .pl-tag--derived  { color: var(--pl-muted); }      /*  5.01 /  7.36 */
.pl-root .pl-tag--modelled { color: var(--pl-ink-amber); }  /*  6.33 /  9.72 */

/* ⚠ `.pl-tag` IS NOT ALWAYS A STANDALONE LINE. Verified at the emit sites:
   `plTag()` is composed INTO a `.pl-scan-top` (the drill's board rows), INTO a
   `.pl-ticker-label` and INTO a `plOvhTagged` overline — three inline/flex
   contexts — as well as sitting alone under a block. The standalone
   `margin-top: 8px` above is right for the alone case and wrong for all three
   of the others: in the flex row it drops the tag 8px below the badge it is
   supposed to sit beside, and in an inline run it inflates the line box of the
   very heading it is annotating. The margin is cancelled where the tag is a
   companion rather than a line of its own.

   `flex: none` matters only in `.pl-scan-top`; it is inert in the other two and
   is grouped here so the three contexts stay one rule and cannot drift. */
.pl-root .pl-scan-top .pl-tag,
.pl-root .pl-ticker-label .pl-tag,
.pl-root .pl-legend-row .pl-tag,
.pl-root .pl-ovh .pl-tag {
  flex: none;
  margin-top: 0;
}

/* ⚠ FACELIFT — CARD 03 · Issue 3 (supervisor review).
   `plScanCountHtml` closes with `plTag("derived")`, which DOES render a real
   `.pl-tag--derived` span — but that span's only distinction from the
   surrounding `.pl-count` text is colour, and `--pl-muted` is `.pl-count`'s
   OWN colour too, so DERIVED visually disappears into the sentence it is
   meant to flag. Every OTHER badge on this card (`.pl-badge`, `.pl-chip`,
   `.pl-p90-chip`) is a washed pill with real padding and rounded corners; this
   is the one honesty tag that never got the same treatment.

   Scoped to `.pl-count .pl-tag--derived` only — NOT the shared base `.pl-tag`
   rule, which Cards 01 and 02 also use, and NOT the many other
   `plTag("derived")` call sites elsewhere on this surface (the demand/price
   reason lines, the FT line, etc.) that are not part of this review. */
.pl-root .pl-count .pl-tag--derived {
  background: var(--pl-wash-neutral);
  color: var(--pl-ink-chip);          /* 6.83:1 light · 12.10:1 night */
  padding: 3px 6px;
  border-radius: 999px;
  margin-top: 0;                      /* it is a companion here, same carve-out
                                          the four contexts above already make */
}

/* --- honest empty / degraded state --- */

/* The honest-empty box is DASHED where every real panel is solid, so a degraded
   or not-yet-armed state is legible as a state rather than as a thin card. */
.pl-root .pl-empty {
  background: var(--pl-card);
  border: 1px dashed var(--pl-hair);
  border-radius: 12px;
  padding: 14px 15px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--pl-muted);          /* 5.46:1 light · 6.68:1 night, on --pl-card */
  margin-top: 10px;
  overflow-wrap: anywhere;
}

.pl-root .pl-count {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 2px;
  line-height: 1.5;
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  text-transform: uppercase;
  display: block;
  margin: 0 0 2px;
  overflow-wrap: anywhere;         /* "· IN 3D 14H" wraps, never clips */
}

/* THE GIANT HEADER (big-numbers-minimal-info pass) — replaces the small
   single-line `.pl-count` header on card 03's face (the old "N OF total ·
   UNDER X% ROSTERED" line, whose count text is now cut entirely). `.pl-count`
   itself is untouched — it's shared with other cards/drills. `flex:none`
   pins this new block the same way its siblings are pinned in the
   `.pl-card`'s flex column (see the shared enumeration near the shell rules). */
.pl-root .pl-scan-head {
  flex: none;
  margin: 0 0 14px;
}

.pl-root .pl-scan-num {
  font-family: var(--font-display);
  font-size: 128px;
  font-weight: 700;
  line-height: 0.82;
  color: var(--pl-ink);            /* 15.97:1 light · 16.54:1 night */
  font-variant-numeric: lining-nums tabular-nums;
}

.pl-root .pl-scan-found {
  font-family: var(--font-display);
  font-size: 45px;
  font-weight: 700;
  line-height: 0.9;
  color: var(--pl-ink);            /* 15.97:1 light · 16.54:1 night */
  margin-top: 8px;                 /* clears the numeral's descender */
  text-transform: uppercase;
}

.pl-root .pl-scan-sub {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 2px;
  line-height: 1.5;
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  text-transform: uppercase;
  margin-top: 10px;
  overflow-wrap: anywhere;         /* a long "·"-joined line wraps, never clips */
}

/* The DERIVED tag composed into the subline — identical washed-pill
   treatment to `.pl-count .pl-tag--derived` above, re-scoped to the new
   class so nothing shared drifts. */
.pl-root .pl-scan-sub .pl-tag--derived {
  background: var(--pl-wash-neutral);
  color: var(--pl-ink-chip);
  padding: 3px 6px;
  border-radius: 999px;
  margin-top: 0;
}

/* G4 item 6, GAP 1 of 2. Measured 0px at BOTH 390x745 and 320x568 while every
   other seam on the card ran 9-16px, so the posture chips and the deadline
   strip touched and read as one undivided mass. The defect was an ABSENT gap,
   not an oversized one. 14px is the card's own rhythm — the same value the
   hero->chips seam already uses — not a new number.
   Scoped to the one seam by an adjacent-sibling selector: `.pl-count` also
   renders on card 03 and inside two drills, and none of those wanted moving. */
.pl-root .pl-chips + .pl-count { margin-top: 14px; }

.pl-root .pl-link-out {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;                /* mono ships 400/500 only */
  letter-spacing: 1.5px;
  color: var(--pl-green-text);     /* 5.73:1 light · 9.74:1 night */
  background: none;
  border: none;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-transform: uppercase;
  -webkit-tap-highlight-color: transparent;
}

.pl-root .pl-link-out:focus-visible {
  outline: 2px solid var(--pl-green-text);
  outline-offset: 2px;
  border-radius: 6px;
}

/* ── CARD 02's ACTION QUEUE PREVIEW (Harold's authorized exception to "CARD
   02 CARRIES THE MOVE AND ITS REASONING, nothing else") ────────────────────
   A compact, read-only nav strip below the card's existing content — NOT a
   second checklist. Each row is `.pl-link-out` (the file's existing tab
   hand-off class, see above) plus `.pl-aq-prev-row`, which overrides the
   underlined-mono-link look with a plain tappable row so it reads as a list,
   not a stack of links. No checkbox, nothing writes `gaf-aq-done` here — the
   tap is the only interaction, and it leaves Pulse. */
.pl-root .pl-aq-preview {
  margin-top: 8px;    /* facelift Step 5: was 12px */
  padding-top: 10px;
  border-top: 1px solid var(--pl-hair);
}

.pl-root .pl-aq-prev-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 400;
  letter-spacing: 1.5px;
  line-height: 1.5;
  color: var(--pl-muted);          /* 5.01:1 light · 7.36:1 night */
  text-transform: uppercase;
  margin-bottom: 4px;
}

.pl-root .pl-aq-prev-label .pl-tag { margin-top: 0; }

.pl-root .pl-aq-preview .pl-aq-prev-row {
  width: 100%;
  min-height: 40px;
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-display);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--pl-ink);            /* 15.97:1 light · 16.54:1 night */
  text-decoration: none;
  text-transform: none;
  letter-spacing: normal;
  padding: 5px 0;
  border-bottom: 1px solid var(--pl-hair);
  text-align: left;
}

.pl-root .pl-aq-preview .pl-aq-prev-row:last-child { border-bottom: none; }

.pl-root .pl-aq-prev-urg {
  flex: none;
  font-family: var(--font-mono);
  font-size: 8.5px;
  font-weight: 500;                /* mono ships 400/500 only */
  letter-spacing: 0.5px;
  line-height: 1.5;
  padding: 3px 7px;
  border-radius: 6px;
  background: var(--pl-wash-neutral);
  color: var(--pl-ink-chip);       /* 6.83:1 light · 12.10:1 night */
  white-space: nowrap;
  text-transform: uppercase;
}

/* Urgency palette borrowed verbatim from `.pl-verdict--*` — "now" reads on
   the same JUMP-strength green-fill chip, "this_week" on the same amber wash
   MONITOR already uses, and "monitor" keeps the neutral fallback above. No
   new tokens minted for this. */
.pl-root .pl-aq-prev-urg--now       { background: var(--pl-verdict-bg); color: var(--pl-verdict-ink); }
.pl-root .pl-aq-prev-urg--this_week { background: var(--pl-wash-amber); color: var(--pl-ink-amber); }

.pl-root .pl-aq-prev-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* THE WHY/NEXT FOOTER (big-numbers-minimal-info pass) — replaces the two
   boxed reason tiles (`.pl-tiles`, above) and the Action Queue preview row
   (`.pl-aq-preview`, above) entirely. Two compact columns: WHY (the single
   highest-priority non-queue reason) and NEXT (the queue's own top undone
   item, read straight off its title). No line-clamp — `overflow-wrap` plus
   keeping the source strings short is cheaper than `-webkit-line-clamp`'s
   stacking-context edge cases, and this card doesn't need it. */
.pl-root .pl-move-footer {
  display: flex;
  gap: 12px;
  margin-top: 8px;
  padding-top: 10px;
  border-top: 1px solid var(--pl-hair);
}

.pl-root .pl-mf-col {
  flex: 1 1 0;
  min-width: 0;
}

.pl-root .pl-mf-label {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 400;                /* mono ships 400/500 only — §6 */
  letter-spacing: 1.5px;
  color: var(--pl-green-quiet);    /* facelift Phase 1 quiet-eyebrow green */
  text-transform: uppercase;
  margin-bottom: 4px;
}

.pl-root .pl-mf-val {
  font-family: var(--font-display);
  font-size: 14.5px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--pl-ink);            /* 15.97:1 light · 16.54:1 night */
  overflow-wrap: anywhere;
}

/* --- §7 pre-season Scanner explainer legend --- */

.pl-root .pl-legend {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 2px 0 4px;
}

.pl-root .pl-legend-row {
  display: flex;
  align-items: flex-start;         /* the gloss wraps to 2 lines at 320px */
  gap: 10px;
  font-size: 13.5px;
  line-height: 1.35;
  color: var(--pl-quiet);          /* 8.62:1 light · 11.55:1 night */
}

/* The gloss takes the rest of the row and is allowed to wrap. Specificity keeps
   this off the badge itself (.pl-badge scores higher and stays flex: none).

   Phase 3: italic, to read as the same editorial-explainer voice as the
   card's hook line — a gloss on what a badge WILL mean, not a live claim.
   Style only, no family/size change: `.pl-badge` sets its own font-family
   (mono) and does not inherit this, so the chip label stays upright: a
   synthetic-oblique skew shares the glyph's own advance width, so the
   wrap point at 320px (noted above) is unchanged — zero fit risk on the
   tightest card in the deck. */
.pl-root .pl-legend-row > span:last-child {
  flex: 1 1 auto;
  min-width: 0;
  font-style: italic;
}

/* Legend chips share one column width so the explainer prose sits on a single
   left margin instead of stepping in and out with each badge's label length.
   (Contract §7: badges inside .pl-legend-row are a LEGEND, never a claim about
   a named player — the zero-in-.pl-scan-row assertion is unaffected.) */
.pl-root .pl-legend-row .pl-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 96px;                 /* fits "VALUE · DEFCON" at 8.5px + tracking */
  margin-top: 1px;                 /* optical baseline against the gloss */
  padding-top: 4px;
  padding-bottom: 4px;
}

/* --- calm / all-clear card --- */

.pl-root .pl-calm {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 20px 6px;
}

.pl-root .pl-calm-mark {
  width: 104px;
  height: 104px;
  flex: none;
  margin-bottom: 18px;
  /* no CSS fill declaration — mark colour lives in its fill= attribute */
}

.pl-root .pl-calm-h {
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 600;
  line-height: 1.1;
  color: var(--pl-ink);
  margin-bottom: 12px;
}

.pl-root .pl-calm-p {
  font-size: 15px;
  line-height: 1.55;
  color: var(--pl-quiet);
  max-width: 270px;
}

.pl-root .pl-calm-nug {
  margin-top: 26px;
  padding-top: 14px;
  border-top: 1px solid var(--pl-hair);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.5px;
  line-height: 1.5;
  color: var(--pl-muted);
  max-width: 280px;
}

.pl-root .pl-calm-nug b {
  color: var(--pl-green-text);     /* 5.73:1 light · 9.74:1 night */
  font-weight: 500;                /* mono ships 400/500 only */
}


/* ---------------------------------------------------------------------------
   6 · NIGHT THEME — token redeclaration only
   Every component rule above resolves through a --pl-* token, so night needs
   no component overrides. All pairs recomputed and AA-clear (see report).
   The FDR ramp and the verdict pill are deliberately identical in both themes:
   they carry their own background, so the page ground is irrelevant.

   ⚠ LOOK G3 — THE PULSE OWNS ITS THEME. These four rules used to key off
   `body.night`, the DESKTOP app's saved theme (index.html's own toggle, stored
   as `gaf-theme`). That meant one tap of the desktop moon button — on any
   device, ever — put The Pulse in night permanently, with no Pulse-side way
   back. It was never the OS colour-scheme media feature: exactly ONE such
   query exists in the whole tree and it is scoped to the desktop Fixtures tab.
   (Named, not quoted — this file's prose is grepped, see the header.)

   The signal is now `data-pl-theme` on `.pl-root`, written by plThemeApply()
   from the Pulse's own `pl-theme` key. PAPER IS THE DEFAULT: an absent key
   renders paper, so a night-desktop user gets a paper Pulse until they say
   otherwise on this surface. The desktop's 24 other tabs are untouched — no
   `body.night` rule anywhere else in the app changes.
   ------------------------------------------------------------------------ */

.pl-root[data-pl-theme="night"] {
  --pl-paper:        #121214;
  --pl-card:         #1C1C20;
  --pl-hair:         #2C2C31;   /* NON-TEXT */
  --pl-faint:        #4A4A50;   /* NON-TEXT ONLY */

  --pl-ink:          #F2F1EC;   /* 16.54:1 on night paper */
  --pl-prose:        #E0DDD5;   /* 13.79:1 */
  --pl-quiet:        #CFCBC1;   /* 11.55:1 */
  --pl-muted:        #A6A29A;   /*  7.36:1 — small-text floor */

  --pl-green:        #2B9F4C;   /* FILL ONLY */
  --pl-green-text:   #4FD37A;   /*  9.74:1 — the light value is far below floor here */
  --pl-red-text:     #F2647C;   /*  6.14:1 */
  --pl-green-quiet:  #7AAE86;   /*  7.33:1 on night paper — quieter than --pl-green-text, still clears floor */

  --pl-wash-neutral: #232326;  --pl-ink-chip:   #E4E2DC;  /* 12.10 */
  --pl-wash-green:   #24392E;  /* ink = --pl-green-text      6.43 */
  --pl-wash-amber:   #3C312B;  --pl-ink-amber:  #E9B071;  /*  6.55 */
  --pl-wash-blue:    #2C3440;  --pl-ink-blue:   #8FC0F0;  /*  6.56 */
  --pl-wash-violet:  #2F2B3E;  --pl-ink-violet: #B3A8EE;  /*  6.35 */
  --pl-p90-bg:       #27272B;  --pl-p90-ink:    #E4E2DC;  /* 11.49 */
  --pl-ledger-bg:    #17211C;  --pl-ledger-bd:  #2C4433;

  /* Decoration flips sign: on paper the jersey ring is a dark hairline that
     keeps a WHITE kit off a white tile; at night it is a light hairline that
     keeps a DARK kit off a near-black one. Same job, inverted ground.
     NON-TEXT, so no contrast floor applies to either. */
  --pl-jersey-ring: rgba(242, 241, 236, 0.18);
  --pl-lift:        0 1px 2px rgba(0, 0, 0, 0.34);
}

.pl-root[data-pl-theme="night"] .pl-mast { background: var(--pl-paper); }
.pl-root[data-pl-theme="night"] .pl-drill { background: var(--pl-paper); }
.pl-root[data-pl-theme="night"] ::selection { background: var(--pl-wash-green); }

/* ⚠ THE SKELETON FLASH — a defect DECOUPLING CREATES, fixed in the same gate.
   loadPulse calls the DESKTOP's showSkeleton() (app.js:220), whose `.skeleton`
   is painted from style.css and flipped by `body.night` (style.css:2615).
   Before Look G3 the two always agreed, because the Pulse followed the desktop.
   They can now disagree: a night-DESKTOP user reading a PAPER Pulse would get
   dark shimmer bars on a paper ground for the whole load window — a repeatable
   wrong-theme flash that did not exist before this change.

   Re-point the shimmer at --pl-* tokens so it follows the PULSE's theme. Those
   tokens are redeclared in the night block above, so this one rule is correct
   in both themes and needs no night twin.

   ⚠ SPECIFICITY IS LOAD-BEARING, not incidental: `body.night .skeleton` is
   (0,2,1). The `[data-pl-theme]` attribute takes this to (0,3,0), which wins on
   class count — so it holds regardless of stylesheet order. Written as an
   attribute PRESENCE test, not a value test, so it covers paper and night with
   one rule and only ever applies where the Pulse actually owns the theme. */
.pl-root[data-pl-theme] .skeleton {
  background: linear-gradient(90deg,
    var(--pl-card) 25%,
    var(--pl-wash-neutral) 50%,
    var(--pl-card) 75%
  );
  background-size: 200% 100%;
}


/* ---------------------------------------------------------------------------
   7 · VIEWPORT GUARDS
   The Pulse is phone-only by boot gate. These keep it sane if a wide or short
   viewport reaches it anyway. No landscape re-layout — the column just centres.
   ------------------------------------------------------------------------ */

@media (min-height: 900px) {
  .pl-root .pl-card { padding-top: 22px; }
}

/* SHORT viewport (SE-class, or a phone in landscape that got past the gate).
   Only the display type moves; the rows, chips and tiles keep their rhythm, so
   the page loses height without losing the density it is built on. The hero
   mark shrinks through its TOKEN, which is what keeps the hero's second line
   indented to the mark instead of stepping out from under the delta. */
@media (max-height: 640px) {
  .pl-root {
    --pl-mast-h: 46px;
    --pl-hero-mark: 52px;
  }
  .pl-root .pl-hero-delta { font-size: 52px; }   /* big-numbers-minimal-info pass, was 38px */
  .pl-root .pl-hero-st { font-size: 9px; letter-spacing: 1.2px; margin-top: 3px; }
  .pl-root .pl-move-title { font-size: 31px; }
  .pl-root .pl-move-line  { font-size: 21px; }
  .pl-root .pl-bs-word { font-size: 52px; }
  .pl-root .pl-bs-name { font-size: 26px; }
  .pl-root .pl-wk-pts     { font-size: 23px; }
  .pl-root :where([data-pl-card="team"]) .pl-kpi-n { font-size: 26px; }
  .pl-root [data-pl-card="team"] .pl-wk-pts { font-size: 20px; }
  .pl-root .pl-calm-mark  { width: 78px; height: 78px; }
  .pl-root .pl-calm-h     { font-size: 25px; }
  .pl-root .pl-scan-num   { font-size: 106px; }
  .pl-root .pl-scan-found { font-size: 37px; margin-top: 5px; }
  .pl-root .pl-scan-sub   { letter-spacing: 1.4px; margin-top: 7px; }
  .pl-root .pl-scan-head  { margin-bottom: 10px; }

  /* ⚠ CARD 03 · SHORT-VIEWPORT DENSITY (G3).
     The rail below the deck is 60px of card padding bought for the swipe hint
     (bottom 40) and the dot rail (bottom 16). On a 568px viewport that is 11%
     of the card for two pieces of chrome, so the hint and the dots move down
     together with the padding — the clearances are preserved exactly, the
     furniture just stops sitting so high. Nothing here changes a COLOUR, so
     every contrast pair computed for the default tier still holds. */
  .pl-root .pl-card  { padding-bottom: calc(48px + var(--pl-safe-b)); }
  .pl-root .pl-hint  { bottom: calc(30px + var(--pl-safe-b)); }
  .pl-root .pl-dots  { bottom: calc(11px + var(--pl-safe-b)); }

  .pl-root .pl-ovh   { margin: 11px 0 7px; }
  .pl-root .pl-foot  { font-size: 14px; padding-top: 10px; }
  .pl-root .pl-folio { padding-bottom: 10px; margin-bottom: 11px; }

  /* the §7 explainer: the legend is the card, so it tightens rather than the
     prose it explains — the honest limitation keeps its full sentence at a
     readable 12.5px/1.5. Padding and gaps pay for the fit; the paragraph does
     not, because that paragraph IS the card's argument. */
  .pl-root .pl-legend      { gap: 5px; }
  .pl-root .pl-legend-row  { font-size: 12.5px; }
  .pl-root .pl-hook        { font-size: 15.5px; }
  .pl-root .pl-empty       { padding: 9px 12px; font-size: 12.5px; margin-top: 5px; }

  /* G4 item 4 — GENUINE HEADROOM AT 320x568. The §7 explainer fitted with
     0.72px to spare, which is not a fit, it is a coincidence: one more legend
     word, one more line in the honest limitation, or a slightly taller foot and
     the first-shipping edition (§0.6) starts scrolling inside a horizontal
     swipe deck. Bought back from PADDING AND GAPS ONLY — the legend keeps its
     three rows, the limitation keeps its full sentence at 12.5px/1.5, and no
     colour moves, so every contrast pair computed for the default tier holds. */
  .pl-root .pl-count       { letter-spacing: 1.4px; }
  .pl-root .pl-legend-row  { gap: 8px; }

  /* the live board */
  .pl-root .pl-ledger              { padding: 8px 11px; }
  .pl-root .pl-ledger-v            { font-size: 18px; }
  .pl-root .pl-scan-row            { padding: 7px 0; }
  .pl-root .pl-scan-row .pl-hook   { font-size: 13.5px; }
  .pl-root .pl-p90                 { margin-top: 5px; }

  /* the drill is an overlay and MAY scroll, so it loses padding, never type */
  .pl-root .pl-drill-body { padding-top: 11px; }
}

/* ---------------------------------------------------------------------------
   THE 4.7" HEIGHT TIER — iPhone SE 2/3, 7, 8 (375x667)  [G4, operator-ruled]

   A SIBLING of the max-height:640px block above, deliberately BOUNDED at both
   ends so it bites ONLY this class:

     >= 741px   the 390-class (745 binding, 844 nominal) — FULL DENSITY, untouched
     641-700    THIS TIER — the 4.7" devices
     <= 640     the existing short tier (and 320x568, below the supported floor)

   ⚠ WHY THIS TIER HAD TO EXIST AT ALL. 375x667 fell in the DEFAULT tier: the
   max-height:640px block that buys the short viewports their fit does not reach
   a 667px viewport. So this class was getting FULL default spacing in 78px less
   height than the binding case, and cards 01 and 02 overflowed by 43px and 93px.
   It is not a device we can measure directly, but 667 is its real physical-point
   height and that is what this tier is sized against — not an estimated
   toolbars-visible figure, which would be freezing an invented number as binding.

   ⚠ The 640-boundary is a real discontinuity, and it is why the earlier numbers
   looked non-monotonic: at 641 card 02 was +119 and at 640 it was +39. One pixel
   of viewport, 80px of overflow, because the short tier fires — content drops
   75px AND the card reclaims 12px of bottom padding. Both figures were correct;
   monotonicity only holds with the tier held constant.

   Paid for in PADDING, MARGINS, TRACKING and two type steps. No colour moves, so
   every contrast pair computed for the default tier still holds. No content is
   removed — this class shows exactly what the 390-class shows.

   The toolbars-visible sub-case (~603) is NOT chased here: it falls under the
   640 tier and leans on `.pl-card`'s `overflow-y: auto`, the same graceful
   degradation accepted below the floor. Verified reachable, nothing clipped. */
@media (min-height: 641px) and (max-height: 700px) {
  .pl-root { --pl-hero-mark: 54px; }

  /* the rail below the deck — same trade as the short tier: the hint and the
     dots move down WITH the padding, so their clearances are preserved. */
  .pl-root .pl-card  { padding-bottom: calc(48px + var(--pl-safe-b)); }
  .pl-root .pl-hint  { bottom: calc(30px + var(--pl-safe-b)); }
  .pl-root .pl-dots  { bottom: calc(11px + var(--pl-safe-b)); }

  /* structural whitespace */
  .pl-root .pl-folio { padding-bottom: 10px; margin-bottom: 11px; }
  .pl-root .pl-rule  { margin: 9px 0; }
  .pl-root .pl-ovh   { margin: 11px 0 7px; }
  .pl-root .pl-chips { margin-top: 10px; }
  .pl-root .pl-kpis  { margin-top: 10px; }
  .pl-root .pl-ticker { margin-top: 10px; }
  /* ⚠ TRACKING ONLY — NO blanket margin-top on `.pl-count`.
     An earlier draft of this tier set one, and it made the card TALLER: the
     trailing "ALL 9 SEASONS IN THE FULL RECORD" line carries `margin-top: 0`
     by default, so a blanket rule ADDED 10px to a block it was supposed to be
     shrinking. Only the chips seam below wants a top margin. */
  .pl-root .pl-count { letter-spacing: 1.4px; }
  /* ⚠ restated: `.pl-chips + .pl-count` ties this rule on specificity (0,2,0)
     and would otherwise lose on source order, collapsing G4's chips->count seam
     back to the 0px it was built to fix. */
  .pl-root .pl-chips + .pl-count { margin-top: 10px; }

  /* two type steps, both on display numerals that have room to give */
  .pl-root .pl-hero-delta { font-size: 58px; }   /* big-numbers-minimal-info pass, was 40px */
  /* v4 G1 — card 01's NEW in-season blocks, re-derived against the new
     structure rather than inherited. `.pl-hero-st` and the lede are additive
     height on a card that has 21.22px of headroom at this viewport. */
  .pl-root .pl-hero-st { font-size: 9px; letter-spacing: 1.3px; margin-top: 4px; }
  .pl-root .pl-hook    { font-size: 15.5px; }
  .pl-root .pl-move-title { font-size: 32px; }
  .pl-root .pl-move-line  { font-size: 22px; }
  .pl-root .pl-scan-num   { font-size: 111px; }
  .pl-root .pl-scan-found { font-size: 38px; margin-top: 6px; }
  .pl-root .pl-scan-sub   { letter-spacing: 1.4px; margin-top: 7px; }
  .pl-root .pl-scan-head  { margin-bottom: 10px; }
  .pl-root .pl-bs-word { font-size: 54px; }
  .pl-root .pl-bs-name { font-size: 27px; }
  .pl-root :where([data-pl-card="team"]) .pl-kpi-n { font-size: 28px; }
  .pl-root [data-pl-card="team"] .pl-wk-pts { font-size: 22px; }

  /* card 02's ledger is the mass: five reasons at 17.5px display. One step down
     keeps the argument intact and every line readable. */
  .pl-root .pl-reason   { padding: 7px 0; }
  .pl-root .pl-reason-v { font-size: 15.5px; }

  /* card 01's season rows — the same padding trade `.pl-scan-row` makes in the
     short tier. Two rows at 11px top+bottom is 44px of gutter on a card that had
     9px of headroom; at 8px it has ~21px, which is a margin rather than a
     coincidence. Card 01's height is PAYLOAD-dependent (a longer manager name, a
     fourth posture chip, a deadline line that wraps) so it needs real slack. */
  .pl-root .pl-wk-row { padding: 8px 0; }

  /* LOOK G3 — the pre-season blocks, re-derived for this tier rather than
     inherited. Both are padding-only trades that mirror ones this tier already
     makes elsewhere (`.pl-wk-row` above, `.pl-scan-row` in the short tier): no
     type step, no colour change, nothing removed.

     The ink row's vertical padding is the whole cost of un-boxing the tiles —
     it is what replaces the border and the wash — so it is the right thing to
     give here. The season rows go inline at this tier too, so their row padding
     tightens with them. */
  .pl-root [data-pl-phase="preseason"] [data-pl-card="team"] .pl-kpi {
    padding: 11px 5px 11px 0;
  }
  .pl-root .pl-wk[data-pl-src="history.past"] .pl-wk-row { padding: 7px 0; }

  /* card 02's tiles — re-derived for this tier, not inherited. The same trade
     the ledger rows make two rules above: padding and one type step on a
     display face that has room to give. */
  .pl-root .pl-tiles { gap: 8px; margin-top: 10px; }
  .pl-root .pl-tile  { padding: 9px 10px; }
  .pl-root .pl-tile-v { font-size: 14.5px; margin-top: 6px; }

  .pl-root .pl-foot { padding-top: 10px; font-size: 14.5px; }

  /* Facelift Step 5, second pass — THIS TIER ONLY. Card 02 still overflowed
     -28px here after the base Step 5 cuts (which apply to every viewport,
     including 390x745 and 360x800 where they'd already closed the gap).
     Same [data-pl-card="act"] selectors as the base cuts above, restated
     here so the higher specificity + later source order wins just inside
     this media condition — 390x745 and the default tier are untouched. */
  .pl-root [data-pl-card="act"] .pl-folio { margin-bottom: 2px; }
  .pl-root [data-pl-card="act"] .pl-ovh   { margin: 2px 0 2px; }
  .pl-root [data-pl-card="act"] .pl-gauge { margin-top: 0; }
  .pl-root [data-pl-card="act"] .pl-hook  { margin: 0 0 0; }
  /* .pl-tiles/.pl-aq-preview no longer render on this card's face at all
     (big-numbers-minimal-info pass — replaced by .pl-move-footer) — their
     margin restatements here are dead, replaced by the footer's own. */
  .pl-root [data-pl-card="act"] .pl-move-footer { margin-top: 4px; }
}

/* Facelift Step 5, second pass — 360px-and-narrower ONLY. This width sits in
   the DEFAULT height tier (800pt nominal), so none of the 641-700 cuts above
   reach it, yet Card 02 still overflowed -5px here from wrap alone. A small
   width-scoped nudge closes it without touching 375/390's full-size margins. */
@media (max-width: 360px) {
  .pl-root [data-pl-card="act"] .pl-folio { margin-bottom: 3px; }
  .pl-root [data-pl-card="act"] .pl-ovh   { margin: 3px 0 3px; }
}

/* NARROW viewport — 320px is the floor (SE 1st-gen, Fold cover screen).
   The KPI grid deliberately STAYS 2×2. Collapsing it to one column was the
   G1 fallback, and it is the wrong trade here: at 320px each tile is still
   140px wide, "£102.4m" at 22px Cormorant needs ~70px of that, and a single
   column doubles card 01's height to buy dead space on the right. Density is
   the requirement; the numerals step down instead. */
@media (max-width: 340px) {
  .pl-root {
    --pl-gutter: 16px;
    --pl-hero-mark: 58px;
  }
  .pl-root .pl-hero-delta { font-size: 40px; }
  .pl-root .pl-kpi        { padding: 9px 10px; }
  .pl-root .pl-kpi-n      { font-size: 22px; }
  .pl-root .pl-kpi-l      { letter-spacing: 1px; }
  .pl-root .pl-wk-name    { font-size: 15.5px; }
  .pl-root .pl-wk-pts     { font-size: 23px; min-width: 30px; }
  .pl-root .pl-move-title { font-size: 29px; }
  .pl-root .pl-move-line  { font-size: 20px; }
  .pl-root .pl-bs-word { font-size: 49px; }
  .pl-root .pl-bs-name { font-size: 25px; }
  .pl-root .pl-reason-k   { width: 58px; }
  .pl-root .pl-reason-v   { font-size: 16.5px; }

  /* ⚠ THE TILES GO TO ONE COLUMN HERE, unlike `.pl-kpis` which deliberately
     stays 2x2. The difference is the payload: a KPI tile holds a short numeral
     and a two-word label, while a reason tile holds a SENTENCE. Two columns in
     a 288px content width gives each tile a ~130px text column — about 19
     characters a line — which turns a two-line claim into four. One full-width
     column per tile keeps every reason readable at the cost of vertical space
     the card is already spending here (320x568 is below the supported floor and
     scrolls gracefully, contract §12.4). */
  .pl-root .pl-tiles      { grid-template-columns: 1fr; gap: 8px; }
  .pl-root .pl-fdr-pill   { min-width: 38px; font-size: 10px; }
  .pl-root .pl-legend-row .pl-badge { min-width: 88px; }

  /* TRACKING IS THE WIDTH LEVER, not size. `.pl-count` carries the §7 countdown
     ("THE SCANNER WAKES AT GW1 · IN 3D 14H") — 37 characters of 10px mono at
     2px of tracking is 296px and wraps to two lines inside a 288px column, so
     the tracking alone was costing a whole line. At 1.2px the same string is
     266px and sets on one, at the SAME size and the SAME 5.01:1 ink. This is
     the trick `.pl-kpi-l` above already uses; it is not a new idea, just the
     one element that had not had it applied. */
  .pl-root .pl-count      { letter-spacing: 1.2px; }
  .pl-root .pl-ovh        { letter-spacing: 1.8px; }
  .pl-root .pl-folio      { letter-spacing: 1.8px; }

  /* the watch control keeps its 44px hit rectangle — only the label tightens */
  .pl-root .pl-scan-add   { letter-spacing: 0.5px; }
  .pl-root .pl-scan-name  { font-size: 15.5px; }
  .pl-root .pl-ledger     { font-size: 12px; letter-spacing: 0.4px; }
}

/* ---------------------------------------------------------------------------
   DIRECTION — FROZEN VOCABULARY (contract v2 §1 amendment, operator-approved)

   `data-pl-dir="up|down|flat"` on any element that carries a directional claim:
   .pl-hero-delta · .pl-kpi-n · .pl-wk-pts · .pl-ledger-delta.

   ONE frozen attribute rather than eight near-duplicate `--up`/`--down` class
   modifiers, and it is what `plSettledDirection()` already returns.

   `flat` is a real, distinct state: equal ranks is a FACT but not a DIRECTION,
   so it renders muted rather than borrowing green. An element with no
   attribute makes no directional claim at all and keeps its default ink.
   --------------------------------------------------------------------------- */
.pl-root [data-pl-dir="up"]   { color: var(--pl-green-text); }
.pl-root [data-pl-dir="down"] { color: var(--pl-red-text); }
.pl-root [data-pl-dir="flat"] { color: var(--pl-muted); }
