/* programs.css V2.18 - 2026-10-08, CONV-B. The staffed door in the shared staff language (Karl's accepted design): the counts row (.pg-door-kp), the section labels with their icons (.pg-door-sec), and the At the door group (.pg-door-tools). */
/* programs.css V2.17 - 2026-10-05, Nolan. CLEANUP RELEASE: the fixed program / session card photos are retired; the card wears the Space's own header (set inline). */
/* programs.css V2.16 - 2026-10-01, Nolan. AY-01 v45: `.pg-pill--inline`, On-Ice / Dryland beside the arena. */
/* programs.css V2.15 - 2026-09-30, Nolan. The Goalies program type wears the goalie blue (Karl, walk v37). */
/* programs.css V2.14 - 2026-09-28, Nolan. FS-01: the standard Filters button replaces `.pg-filterbtn`; each filter label wears its dot. */
/* programs.css V2.13 - 2026-09-28, Nolan. `.pg-kdone-undo`: the kiosk card's 10-second Not me / undo. */
/* programs.css V2.12 - 2026-09-28, Nolan. B22 note: `.pg-searchrow` puts a compact Filters button at the right of the search bar; `.pg-filtersum` is the showing line under it. */
/* programs.css V2.11 - 2026-09-28, Nolan. B08: `.pg-browse-h` names the Programs / Sessions tab over its tip; `.pg-card-pills` stacks a card's pills in one right-hand column. */
/* programs.css V2.10 - 2026-09-25. Find Sessions tabs use the approved filled
   green/purple glow states over the corrected darker launch surfaces. */
/* programs.css V2.9 - 2026-09-25. Keep protected hero and purple states
   accessible across the retained post-launch palettes. */
/* programs.css V2.8 - 2026-09-25. Keep catalogue glow states on opaque
   surfaces so the deferred alternate palettes remain accessible. */
/* programs.css V2.7 - 2026-09-25. Find Sessions uses its hero as the page
   header, adds a protected close control, and restores the preview glow system. */
/* programs.css V2.6 - 2026-09-25. Apply the approved Impact display role to
   the catalogue hero and listing titles without changing interface/body type. */
/* programs.css V2.5 - 2026-09-25. Premium browse presentation: photo-led
   program/session cards, catalogue search and stronger hierarchy. */
/* programs.css V2.4 - 2026-09-23, Rita. `S2-109` - Karl, on tapping a search box: "we don't need the extra squared outline inside the already existing outline that's around the search. The rounded outline around the search should glow." `.pg-search` (the door and
   walk-up name searches) glows on its own rounded border instead of wearing
   base.css's detached ring. */
/* programs.css V2.3 - 2026-09-22, Marlow. `S2-14` - TWO RULES WERE BACK UNDER
   THE `SYS-07` 16px iOS ZOOM FLOOR, and both are on member screens Karl uses on
   a phone: `.pg-fld input, … select, … textarea, .pg-rolebox input` at 13px -
   the programme form fields and the "Search by name, email or ID" box - and
   `.pg-pay-amt input` at 15px, which this file's own comment calls "the one
   most often retyped". Padding and every other declaration are untouched; only
   the size moves.
   ⚖️ NEITHER WAS A MISTAKE WHEN IT WAS WRITTEN. `SYS-07` left `base.css`'s floor
   at specificity (0,0,1) ON PURPOSE so a component could still win, and banked
   NOTHING - so anybody sizing a component fourteen days later stepped over it
   without knowing there was a floor. NEW GUARD `tests/zoom-floor.test.mjs`
   banks it, and its scanner is driven against these two rules as they were.
   📌 SHARED FILE, AND I AM NAMING IT: V2.2 is Meryl's `S2-02` spacing pass. This
   touches `font-size` only and no rule she wrote for spacing. */
/* programs.css V2.2 - 2026-09-18, Meryl. `S2-02`, the third screen Karl named:
   "in the find session/programs the txt sits too tight to the left side of the
   session cards". It was `--sp-1`, FOUR PIXELS, plus the 1px border - measured
   headless at 390px as a 5.00px inset. Now `--sp-4` (13px), a 14.00px inset,
   which is what `.h-find` next door already uses (11px 12px).
   ⛔ HORIZONTAL ONLY. A first draft raised the vertical too and grew every card
   10px taller - something Karl did not ask for and which compounds down a list.
   Card height delta 0.00, row height delta 0.00, measured both ways.
   ⚖️ THREE ROW TYPES, ONE SCREEN. `.pg-card`, `.pg-sess` and `.pg-mine` all had
   the same 4px inset; `.pg-mine` is the one Karl's sentence does not name and it
   is included because leaving one row type at 4px puts two insets on one screen,
   which is the complaint reappearing inside the fix. Said out loud in the outbox
   so it can be struck in a line if that is the wrong call. */
/* programs.css V2.1 */
/* V2.1 - 2026-09-23, Vera. `SYS-57`, the CSS half. The Find filter sheet stops
   being four hidden-scrollbar scrollers and becomes Schedule's rhythm: one
   group per row at full width, a hairline between rows, options that WRAP.
   📏 MEASURED AT 390px BEFORE AND AFTER, on Karl's own four rows:
     TYPE  401/301 -> 332/332   `Conditioning (3)` 19% visible -> 100%
     FOR   318/301 -> 332/332   `Goalies only (0)` 89% visible -> 100%
   Zero clipped rows and 8/8 chips fully visible across all five theme x style
   combinations; the form field's rail (`segField`) is untouched and still
   scrolls, because every rule here is scoped to `.pg-filters`.
   ⚖️ THE CONTROL IS NOT COPIED, ONLY THE FRAME: Schedule's rows are boolean
   toggles and Find's are pick-one-of-N, so there is no switch and no
   `Select all`. The `(0)` options stop looking like broken buttons and become
   greyed text — they were already inert in `programs.js`; what was missing was
   a look that said "empty" rather than "failing".
   🪞 AND ONE SELF-INFLICTED DEFECT CAUGHT BY RENDERING: my first chip rule set
   `background` unqualified and outranked `.h-seg button.is-on`, leaving the
   SELECTED chip at 1.11:1. Base rule is geometry-only now; the three states
   measure 13.06:1 / 6.13:1 / 5.41:1.
   ⏭️ NEEDS ONE TOKEN FROM `programs.js`, WHICH IS EMILY\'S: the wrapper at
   `filterRowsHtml()` becomes `<div class="h-grp pg-filters">`. Without it the
   rows divide but sit in no card. */
/* V2.0 - 2026-09-20, Vera. `H365-02` REOPENED with the target reversed: the
   panel is full-screen again (`inset: 0`) and covering the app chrome is now
   what Karl wants. It stays `absolute` inside `.h-app` rather than reverting to
   `fixed`, because `.h-app` is the `100dvh` box — that is what makes it
   full-screen with NO SLIVER on a phone instead of running under the browser
   toolbar. */
/* V1.9 - 2026-09-20, Vera. `H365-02`: `.pg-panel` is `position: absolute`
   inside `.h-app` instead of `fixed` against the window. `.h-app` is `100dvh`
   and `fixed` resolves against the layout viewport, so on iOS Safari the panel
   was a browser-toolbar taller than the shell and covered the bottom nav. Every
   other overlay in the app already does it this way. */
/* V1.8 - 2026-09-19, Vera. `H365-03`: the filter rails move into a sheet and
   `.pg-filters` becomes ONE grid, so the label column is a shared max-content
   column instead of a 44px basis that GENDER overflowed. Fixes the rag by
   construction. No other rule changes, and no new selector: the bar itself is
   `.h-filterbar`, already shared from components.css. */
/* programs.css V1.7
   V1.7 - 2026-09-19, Zoey. `.pg-seg .h-seg button[disabled]` - the look of a
     filter chip with nothing behind it. An ATTRIBUTE selector rather than a new
     class, so `client-contracts` has nothing new to keep in step and the state
     lives where the DOM already carries it. Structure only: no colour of the
     shared `.h-seg` is redeclared, so `SYS-04`'s paint can restyle the rail
     without touching this rule.
   V1.6 - 2026-09-15, Zoey. `tokens` red on `:67`. `.pg-panel` now DECLARES
     `--pg-top: 0px` and `--pg-bottom: 0px` rather than relying on the `var()`
     fallback alone. The lint's rule is "no rule dresses a hard-coded value as a
     token with a fallback", and a token declared nowhere is exactly that shape
     to a static reader - it cannot see `el.style.setProperty` in
     `programs.js:331`. NO BEHAVIOUR CHANGE: the runtime write is an INLINE
     style and beats this rule; pinned in `programs-smoke` by asserting the
     inline value is the COMPUTED one and is not `0px`, and mutation-proved
     (remove the runtime write and three assertions go red).
   V1.5 - 2026-09-15, Zoey. `SYS-17` second pass and `H365-02`.
     · `.pg-fld--seg` and the `.pg-seg` rules move out of the filter block and
       now dress every segmented field in this file, not only the browse
       filters. `.pg-seg` releases `.h-seg button`'s `flex: 1` so an open-ended
       list scrolls instead of squashing, and redeclares no colour, state or
       transition of the shared control.
     · `.pg-pay-method` and `.pg-move-sel` are REMOVED rather than emptied -
       both dressed a `<select>` that no longer exists, and a selector kept with
       nothing in it is the `.pg-pill--static` shape this file paid for once.
       `.pg-move .pg-seg` keeps the width the move row needs.
     · `.pg-panel` is inset by `--pg-top` / `--pg-bottom`, written from the
       rendered height of `.h-appbar` and `.h-bnav` by `chromeInset()` in
       `programs.js`. The `0px` fallbacks are exactly today's full-screen
       behaviour on a page with no app chrome.
   V1.4 — 2026-09-07, Vera. `SYS-30`: the hero's text was 1.02:1 in light,
   on a ground the GROUP chooses. A scrim makes the ground predictable while
   leaving `colour2` visible in a band; the ink is `--on-colour` at full and
   the two `opacity:.85` lines are gone. No JS change, no markup change.
   V1.3 — 2026-09-07, Vera. `FND-02` step two: `.pg-pill--drop` and
   `.pg-toggle.is-on` fill with `--purple-solid`. Borders stay on `--purple`.
   V1.2 — REG-01 / REG-02 / SYS-17. Adds `.pg-pill--reg` and an UNSCOPED
     `.pg-pill--wait` (the door keeps its own, more specific rule) for the
     "Registered" / "Waitlist" state on browse cards; `.pg-person.is-taken` for
     a picker row that is already registered. Replaces the four native filter
     dropdowns with the shared `.h-seg` control: `.pg-fl` and its `select` rule
     are RETIRED with the selects they dressed, `.pg-filters` becomes a column,
     and `.pg-filtrow` / `.pg-filtl` / `.pg-seg` are new — `.pg-seg` releases
     `.h-seg button`'s `flex: 1` so an open-ended list of programme types
     scrolls instead of squashing, and redeclares NO colour, state or
     transition of the shared component.
   ============================================================================
   PROGRAMS — browse, register, create/publish.

   T-002 BY BYTES. Not one new colour, radius, shadow, font or spacing value is
   invented here: every declaration below resolves to a token defined in
   tokens.css. `tools/css-tokens.mjs` and `tests/tokens.test.mjs` both fail on a
   `var(--x)` that names nothing, and the header of the first one lists the six
   times this project shipped exactly that — a whole block written against
   `--text`, `--brand`, `--focus`, `--panel-2`, `--line-strong` and `--bg-sunk`,
   none of which exist. So the palette used here is deliberately small and
   deliberately borrowed:

     --panel / --panel2   surfaces        --line / --line2   edges
     --ink / --muted / --faint  text      --green / --purple accents
     --on-green / --on-purple   ink ON those accents (never assumed white)
     --warn / --danger    states          --r-sm / --r-2xl   radii
     --sp-1 / --sp-5      spacing         --sh-1 / --sh-2    shadows
     --t-fast / --t-med   transitions

   THE PROGRAM TYPE COLOURS ARE ONE RULE, NOT EIGHT. The live app keys a card
   colour off the value-list entry with `.h365-fs-card-type--<slug>` and falls
   back to a generic class for anything the stylesheet does not recognise —
   which is what lets Karl add a new format by editing a list instead of
   shipping CSS. Same here: `.pg-type-*` sets one custom property and everything
   downstream reads it, so an unrecognised type renders in the fallback rather
   than unstyled.
   ========================================================================== */

.pg-panel {
  /* `H365-02` — INSET BY THE APP'S OWN CHROME RATHER THAN OVER IT. Both
     properties are OVERWRITTEN AT RUNTIME by `chromeInset()`
     (`assets/js/programs.js:331`) from the RENDERED height of `.h-appbar` and
     `.h-bnav`, as an INLINE style — which beats this rule, so declaring them
     here changes no behaviour at all. On a page with no app chrome, or before
     the script has ever run, the panel fills the viewport exactly as it did
     before this item.

     🔎 DECLARED RATHER THAN LEFT TO THE FALLBACK, AND `tokens` WAS RIGHT TO
     RED ON IT. Its rule is *"no rule dresses a hard-coded value as a token with
     a fallback"* — and `var(--pg-top, 0px)` against a token declared NOWHERE is
     exactly that shape: it reads as a token and always resolves to the literal.
     The lint is a static reader and cannot see a property set by
     `el.style.setProperty`, so it could only see the lie. **The right answer is
     to make the claim true, not to except the file** — a declared default is
     also the honest place for a reader to learn what the two mean.

     📌 THE `0px` FALLBACKS STAY, AND THEY NOW GUARD A DIFFERENT FAILURE. They
     are no longer reachable through the script being absent — the declarations
     below cover that. They cover the DECLARATIONS being removed by a later
     tidy-up, where a bare `var(--pg-top)` would resolve to nothing and take the
     whole `inset` with it. */
  --pg-top: 0px;
  --pg-bottom: 0px;
  /* 🔴 `absolute`, NOT `fixed`, AND THIS IS `H365-02`'s SECOND HALF.
     `position: fixed` resolves against the LAYOUT viewport. `.h-app` is sized
     `height: 100dvh` — the DYNAMIC viewport, which on iOS Safari shrinks by the
     browser toolbar. Those two boxes are the same on a desktop and DIFFERENT on
     a phone, so the panel's bottom inset was measured from a baseline lower
     than the one the bottom nav sits in, and the panel landed over the nav.
     `chromeInset()` was measuring correctly the whole time; it was writing the
     right number into the wrong coordinate space.
     ⚖️ AND IT MAKES THIS PANEL BEHAVE LIKE EVERY OTHER OVERLAY IN THE APP.
     `.h-sheet` and `.h-panel` are `position: absolute` inside `.h-app`, which
     is `position: relative` and already their containing block. This panel was
     the only one mounting to `document.body` and positioning against the
     window — which is also why sheets have always been fine on his phone and
     this has not.
     📌 THE HARNESS CANNOT SEE THIS. In headless Chromium `dvh` and `vh` are
     equal, so `programs-smoke`'s H365-02 assertion was green while the phone
     was wrong. It stays green after this change; it is a no-regression check
     here, not a proof. The proof is Karl's phone. */
  position: absolute;
  /* 🔄 `inset: 0` — FULL SCREEN, REOPENED AND REVERSED. `H365-02` originally
     inset this panel past the app bar and bottom nav, because covering them is
     what made an overlay read as "navigates away". Karl has walked that and
     changed the target: full-screen, no sliver, covering the nav is fine.
     ⚖️ THE `absolute`-INSIDE-`.h-app` HALF STAYS, AND IT IS WHAT MAKES "NO
     SLIVER" TRUE. `.h-app` is `100dvh`, the box the member can actually see;
     `inset: 0` against it fills exactly that and stops. Going back to
     `position: fixed` would fill the LAYOUT viewport instead, which on iOS
     Safari is a browser-toolbar taller — the panel would run under the toolbar
     and its bottom content would be unreachable. Same reasoning as V1.9, now
     serving the opposite requirement.
     📌 `--pg-top` / `--pg-bottom` are still written by `chromeInset()` and are
     no longer read here. They stay because the measurement is also what decides
     `aria-modal`, and because this target has now reversed once. */
  inset: 0;
  /* 🔴 WAS 60, WHICH IS THE TOAST'S LAYER. `components.css` keeps the stacking
     order in one place and this panel was parked two bands above where it
     belongs:
         44  the detail panel   <- this is one of those
         50  the scrim
         51  a sheet — "ALWAYS above whatever opened it"
         60  a toast — above everything, it reports on all of them
     Nothing had noticed because `programs.js` had never opened a sheet. The
     moment one did (`H365-03`'s filter sheet) it mounted at 51, UNDER a panel
     at 60, and `programs-smoke` reported it honestly as a click the panel
     intercepted — 57 retries against a control the member can see.
     At 44 the scrim dims the panel behind the sheet, which is the behaviour the
     table describes. The panel does not need a high z-index to clear the app
     chrome: the appbar and bottom nav are plain flex children with no z-index
     at all, and this panel is INSET past them by `--pg-top` / `--pg-bottom`
     rather than layered over them. */
  z-index: 44;
  display: none; flex-direction: column;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
}
.pg-panel.is-open { display: flex; }

/* --- head ---------------------------------------------------------------- */
.pg-head {
  display: flex; align-items: center; gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-1);
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  flex: none;
}
.pg-title {
  flex: 1; min-width: 0;
  font-family: var(--font-head);
  font-size: 15px; font-weight: 800;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pg-back, .pg-close {
  background: transparent; border: 0; color: var(--muted);
  font-size: 17px; line-height: 1; cursor: pointer;
  padding: 8px 10px; border-radius: var(--r-sm);
  transition: color var(--t-fast), background var(--t-fast);
}
.pg-back:hover, .pg-close:hover { color: var(--ink); background: var(--panel2); }
.pg-back-gap { width: 34px; flex: none; }

/* --- body ---------------------------------------------------------------- */
.pg-body {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: var(--sp-1) var(--sp-1) var(--sp-5);
  -webkit-overflow-scrolling: touch;
}
.pg-loading, .pg-empty {
  padding: var(--sp-5) var(--sp-1);
  text-align: center; color: var(--faint); font-size: 13px;
}
.pg-body--browse {
  background: radial-gradient(circle at 100% 0, var(--green-dim), transparent 30%),
    radial-gradient(circle at 0 54%, var(--purple-dim), transparent 28%);
}
.pg-browse-lead {
  position: relative; isolation: isolate; overflow: hidden; min-height: 150px;
  display: flex; flex-direction: column; justify-content: flex-end;
  margin: 0 0 var(--sp-1); padding: var(--sp-4);
  border: 1px solid var(--line2); border-radius: var(--r-2xl);
  background: var(--panel) url('../img/training/program-ice.jpg') center 42% / cover no-repeat;
  box-shadow: var(--sh-2);
}
.pg-browse-lead::before {
  content: ''; position: absolute; z-index: -1; inset: 0;
  background: linear-gradient(0deg, var(--scrim-identity), var(--scrim-modal));
}
.pg-browse-lead small { color: var(--green); font-size: 10px; font-weight: 900; letter-spacing: .12em; }
.pg-browse-lead strong {
  margin-top: 5px; color: var(--on-colour); font-family: var(--font-display);
  font-size: 24px; line-height: 1.02; letter-spacing: .02em; text-transform: uppercase;
}
.pg-browse-lead span { margin-top: 6px; max-width: 390px; color: var(--on-colour); font-size: 12px; line-height: 1.4; }
.pg-browse-title {
  margin: 5px 42px 0 0; color: var(--on-colour); font-family: var(--font-display);
  font-size: 24px; line-height: 1.02; letter-spacing: .02em; text-transform: uppercase;
}
.pg-browse-close {
  position: absolute; z-index: 2; top: 12px; right: 12px;
  display: grid; place-items: center; width: 38px; height: 38px; padding: 0;
  border: 1px solid rgba(255,255,255,.32); border-radius: 50%;
  background: var(--bg); color: var(--ink); font-size: 17px;
  box-shadow: 0 6px 20px rgba(0,0,0,.32); cursor: pointer;
}
.pg-browse-close:hover, .pg-browse-close:focus-visible { border-color: var(--green); box-shadow: 0 0 18px rgba(var(--green-rgb), .18); }
.pg-browse-count {
  min-height: 16px; margin: -4px 2px 5px; color: var(--purple);
  font-size: 11px; font-weight: 800;
}
/* THE DESK TILL — P-005(c). It replaces the Take-payment button IN PLACE on
   the row rather than opening a sheet, for the reason the amount is on the row
   at all: the person is standing there, and a desk with a queue does not want a
   second screen between them and a twenty-dollar note.

   IT WRAPS RATHER THAN SCROLLS. Three controls plus a refusal do not fit across
   a phone at 390px, and a horizontally-scrolling money form is a way to press
   the wrong button — so the row grows instead. */
.pg-pay { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; width: 100%; }
/* THE AMOUNT IS THE WIDEST CONTROL AND THE ONE MOST OFTEN RETYPED — a partial
   payment is real, so it is a number field and not a confirm. */
.pg-pay-amt { flex: 1 1 90px; min-width: 90px; }
.pg-pay-amt input {
  width: 100%; box-sizing: border-box;
  padding: 8px 10px;
  font-size: 16px; /* SYS-07: was 15px; under 16 the field zooms iOS */
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-sm);
}
/* `SYS-17` — `.pg-pay-method` IS NOW THE HIDDEN INPUT'S HOOK, NOT A CONTROL.
   The dressing it used to carry belonged to a `<select>`; the visible control is
   the shared `.h-seg`, which owns its own look. The rule is REMOVED rather than
   emptied — a selector kept with nothing in it is the `.pg-pill--static` shape
   this file already paid for once. The attribute still exists on the input and
   is still what `takePaymentAtDoor` reads. */
/* AND THE CONTROL THAT OPENS IT. Deliberately not `.danger` — taking money that
   is owed is the ordinary thing this screen is for, not a destructive act; the
   two-tap arming belongs on Remove and on Generate teams, which cannot be
   undone. A payment that was wrong is corrected by a reversal, which is a real
   row in the ledger rather than an erasure. */
.pg-door-take { flex-shrink: 0; }

/* THE MOVE PANEL — "signed up for the wrong night". Same shape as the till:
   it wraps rather than scrolls, because a horizontally-scrolling correction on
   a phone is a way to press the wrong button, and the row grows instead. */
/* "+ ADD MEMBER" — the organiser's manual add (ETM Round 9 Q3). A COLUMN, not
   a row like `.pg-move`, and the difference is what the two panels are for: a
   move is one decision made in one glance, this is a search that grows a list
   of results underneath it and needs the vertical room to do it. */
.pg-addm {
  display: flex; flex-direction: column; gap: 8px;
  width: 100%; padding: 10px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-sm);
}

.pg-move { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; width: 100%; }
/* `SYS-17` — `.pg-move-sel` GOES THE SAME WAY. Its point survives in the chip
   text: a session name AND a date, because "Session 3" alone does not tell an
   organiser which Sunday they are about to move somebody to. */
.pg-move .pg-seg { flex: 1 1 160px; min-width: 140px; }
/* THE EMPTY STATE REUSES `.pg-empty` AND ONLY ADJUSTS ITS LAYOUT. Two classes,
   so it beats `.pg-empty`'s own single-class padding without inventing a second
   empty-state colour — the `.pg-pill--static` lesson applied on purpose this
   time. It sits in a row rather than alone on a screen, so the block padding
   and the centring come off; nothing about how it READS changes. */
.pg-move .pg-empty {
  flex: 1 1 auto; padding: 0; text-align: left; font-style: normal; font-size: 12px;
}

/* WHICH DAVE SMITH — the email and profile id under a search result. Quieter
   than the name and quieter than the action line, because it is what you check
   AFTER the name has caught your eye and BEFORE you press. */
.pg-pick-id { font-style: normal; font-size: 11.5px; color: var(--faint); }

/* THE WAY OUT OF KIOSK MODE, AND IT IS QUIET ON PURPOSE.
   Live's is a low-opacity "Admin" link and the reason is not aesthetics: a
   prominent "Exit kiosk mode" button on a screen facing a queue is an
   invitation, and everybody who presses it gets a PIN prompt they cannot answer
   and a moment of thinking they have broken something. It has to be findable by
   the organiser and unremarkable to everybody else.
   ⚠️ AND IT IS `opacity: .45`, NOT `color: var(--faint)` — because it has to
   stay a real, hit-testable, focusable control. A contrast-failing DECORATION
   would be a bug; this is a deliberately recessive control, so it also carries
   a large touch target and comes fully opaque on focus, which is what a
   keyboard or screen-reader user needs and a lobby does not. */
.pg-kiosk-foot { display: flex; justify-content: center; padding: var(--sp-5) 0 var(--sp-1); }
.pg-kiosk-admin {
  background: none; border: 0; cursor: pointer;
  color: var(--muted); opacity: .45;
  font-size: 12px; letter-spacing: .04em;
  padding: 12px 24px; min-height: 44px;
}
.pg-kiosk-admin:hover, .pg-kiosk-admin:focus-visible { opacity: 1; color: var(--ink); }

/* A REFUSAL THE PERSON CAN READ — a wrong PIN, or a reason the server gave.
   It carries `role="alert"` in the markup, so it is spoken as well as shown;
   this is only the seen half. */
/* ⚠️ `--danger`, NOT AN INVENTED `--red`. My first version of this rule used
   `var(--red)` and `var(--red-dim)`, neither of which exists in `tokens.css` —
   an undefined custom property falls back to the inherited value, so the box
   would have rendered as ordinary body text with a transparent border and
   looked *fine* rather than broken. `--danger` is the INK half of the pair and
   `--danger-solid` the FILL half; this is text on the panel, so it takes the
   ink token and a tinted border rather than a fill. */
.pg-err {
  color: var(--danger);
  border: 1px solid var(--danger); border-radius: var(--r-sm);
  background: rgba(var(--danger-rgb), .08);
  padding: 8px 10px; font-size: 12.5px; line-height: var(--lh);
  margin: var(--sp-1) 0;
}

/* THE KIOSK'S SEARCH-ONLY PROMPT — "Type your name to check in."
   IT IS NOT STYLED LIKE THE OTHER EMPTY STATES AND THAT IS THE POINT. Every
   other `.pg-empty` is an apology read by a staff member at arm's length:
   small, faint, get out of the way. This one is an INSTRUCTION, it is the only
   thing on the screen, and it is read by a stranger standing at a table in a
   rink lobby with a queue behind them. Faint 13px text is the wrong answer to
   "what do I do" — so it takes the ink colour and roughly double the size.
   It inherits the padding and centring above rather than restating them. */
.pg-kiosk-prompt {
  color: var(--ink); font-size: 22px; font-weight: 600;
  font-family: var(--font-head);
  padding-top: var(--sp-5); padding-bottom: var(--sp-5);
}

/* ── THE KIOSK'S TWO NON-CONTROLS ──────────────────────────────────────────
   Both exist because Karl's Q10/Q11 answers remove a BUTTON from a kiosk row
   and leave a person standing there needing to know what happened. A row that
   simply loses its control says nothing, and "nothing" on a self-serve screen
   reads as "it did not work".

   `pg-pill--static` — the arrived tick, with the pill's shape and none of its
   affordance: no pointer, no hover, and it is a `<span>` rather than a
   `<button>` in the markup so a screen reader does not announce a control that
   cannot be operated. It reuses `.pg-pill.is-in`'s colour deliberately — the
   state is the same state the desk sees, and only its interactivity differs. */

/* `pg-door-seeadmin` — the waitlisted hint. Quiet, because it is an
   instruction and not a refusal: the person has done nothing wrong and the
   answer is two steps away. */
.pg-door-seeadmin {
  color: var(--muted); font-size: 12px; font-style: italic;
  padding: 6px 2px; white-space: nowrap;
}

/* THE WAY TO THE NEXT SESSION'S DESK — Karl: *"events are usually 1 hour long
   with multiple events in a row so making it easy to switch over to the next
   event would be a good feature."*

   IT SITS IN THE STICKY ACTION BAR BESIDE "+ Add walk-in" AND IS THE QUIETER OF
   THE TWO. Both are things an organiser does at a desk, but one happens
   constantly through a session and the other once at the end of it — so the
   walk-in keeps the full-width green and this takes the ghost treatment. On the
   wide breakpoint the bar is already `flex-direction: row`, so they sit side by
   side without a rule of their own. */
.pg-door-switch { flex-shrink: 0; }


.pg-sec {
  font-family: var(--font-head);
  font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--faint);
  margin: var(--sp-5) 0 var(--sp-1);
}
.pg-intro { color: var(--muted); font-size: 12.5px; line-height: var(--lh); margin: 0 0 var(--sp-2); }
.pg-prose { color: var(--ink); font-size: 13px; line-height: var(--lh); margin: var(--sp-1) 0; white-space: pre-wrap; }
.pg-hint { color: var(--faint); font-size: 11.5px; margin-top: 6px; }

/* --- tabs ---------------------------------------------------------------- */
.pg-tabs { display: flex; gap: 6px; margin-bottom: var(--sp-2); }
.pg-tab {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--panel); border: 1px solid var(--line);
  color: var(--muted); font-size: 12.5px; font-weight: 700;
  padding: 10px 8px; border-radius: var(--r-sm); cursor: pointer;
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.pg-tab.is-active { color: var(--ink); background: var(--panel2); }
.pg-tab:first-child.is-active {
  color: var(--active-green-ink); background: var(--active-green-bg); border-color: var(--green);
  box-shadow: inset 0 0 0 1px rgba(var(--green-rgb), .22), 0 0 20px rgba(var(--green-rgb), .26);
}
.pg-tab:nth-child(2).is-active {
  color: var(--active-purple-ink); background: var(--active-purple-bg); border-color: var(--purple);
  box-shadow: inset 0 0 0 1px rgba(var(--purple-rgb), .22), 0 0 20px rgba(var(--purple-rgb), .26);
}
.pg-tab-n {
  font-size: 10.5px; font-weight: 800; color: var(--faint);
  background: var(--panel2); border-radius: var(--r-sm); padding: 1px 6px;
}
.pg-tab:first-child.is-active .pg-tab-n { color: var(--active-green-ink); background: var(--panel); }
.pg-tab:nth-child(2).is-active .pg-tab-n { color: var(--active-purple-ink); background: var(--panel); }

/* --- filters ------------------------------------------------------------- */
/* SYS-17 — THE BAR IS FOUR STACKED ROWS OF CHIPS, NOT FOUR DROPDOWNS SIDE BY
   SIDE. `.pg-fl` (a column with a label over a `<select>`) is gone with the
   selects it dressed; `.pg-fld`, which looks like it and is NOT it, is the FORM
   field and stays — the two names are one letter apart and the selector group
   below used to bind them together, which is exactly how a shared rule outlives
   the thing it was written for. */
/* `H365-03` — ONE GRID FOR THE WHOLE BLOCK, SO THE LABEL COLUMN CANNOT GO
   RAGGED. Measured off Karl's `BadFindSessions.png` at DPR 3: three rails
   started at 171px and GENDER alone at 185, and the gap between a label and its
   own rail swung from 32px to 92px. That swing is what reads as unset.

   THE CAUSE WAS `flex: 0 0 44px` WITH NO `min-width: 0`: a flex item's
   `min-width` defaults to `auto`, so an item that refuses to shrink still
   GROWS past its basis when its content is wider — and "GENDER" at 10.5px/800
   is wider than 44px. The basis was a number three of four labels happened to
   fit.

   A grid fixes it by construction rather than by a bigger number: one
   `max-content` column, shared by every row, so the column is exactly as wide
   as the longest label and every rail starts at the same x — including the
   label somebody adds next year. It also RECLAIMS width rather than spending
   it, which is the other half of the four-rails finding.

   `display: contents` on the row is what lets one grid own children that are
   still wrapped per row in the markup. The wrapper is a plain `<div>` carrying
   no role or semantics, which is the case where `display: contents` is safe —
   it is not on a list, a table or anything with an implicit role to lose. */
/* ===========================================================================
   `SYS-57` — THE FILTER SHEET STOPS BEING A ROW OF SCROLLERS.

   🗣️ KARL: *"the filtering options inside the drop down look terrible…
   Schedule filter is the prefered clean appearance."*
   ⛔ AND IT IS NOT TASTE. MEASURED AT 390px BEFORE ANY CHANGE, on the four rows
   from his own screenshot:
     TYPE    track 401 / 301   100px off the edge, `Conditioning (3)` 19% visible
     FOR     track 318 / 301    17px off the edge, `Goalies only (0)` 89% visible
     AGE     301 / 301 fits · GENDER 301 / 301 fits
   🔑 `.pg-seg` IS A SCROLLER WHOSE SCROLLBAR IS HIDDEN — `overflow-x: auto`
   plus `::-webkit-scrollbar { display: none }` — so those options are not just
   ugly, **there is no cue they exist.** A member cannot know to swipe.

   ⚖️ THE FRAME IS `schedule.js:825`'s AND THE CONTROL IS NOT, WHICH IS THE
   WHOLE RULING: Schedule's rows are independent BOOLEAN toggles (one switch per
   calendar); Find's are PICK-ONE-OF-N. A toggle per row cannot say *pick one of
   five*. So this takes the RHYTHM — one group per row, full width, a hairline
   between rows, the button pair beneath — and lets the options WRAP. Chips stay
   chips; they just stop living on a fixed track.
   ⛔ AND NO `Select all`. Schedule has one because its rows are independent.
   On a pick-one group it cannot mean anything, and adding it for symmetry is
   how a control starts lying.
   📌 THE ROW WRAPPER IS NOW THE SHARED `.h-grp` CARD (one token added in
   `programs.js`), NOT A SECOND BODY OF THE CARD LOOK RESTATED HERE. That is the
   `SYS-51` lesson applied while the ink is wet. */
.pg-filters { display: block; margin-bottom: var(--sp-1); }
/* ONE GROUP PER ROW, divided the way every other stacked row in the app is.
   `.h-grp > :first-child { border-top: 0 }` in components.css already clears the
   leading rule, so this does not restate it. */
.pg-filters .pg-filtrow {
  display: block; padding: 11px 12px;
  border-top: 1px solid var(--line);
}
/* THE LABEL SITS ABOVE ITS OPTIONS, because a label in a fixed left column is
   what forced the options into the narrow track that clips. */
.pg-filters .pg-filtl {
  display: flex; align-items: center; gap: 6px; margin: 0 0 7px;
  text-align: left; min-width: 0;
}
.pg-filtl .h-frow__dot { width: 9px; height: 9px; border-radius: 50%; }
/* ⛔ NO SCROLLER IN HERE. This is the line the defect lived on. */
.pg-filters .pg-seg { overflow: visible; flex: none; }
/* THE RAIL IS NOT A RAIL IN THIS CONTEXT. A pill track inside a card row is a
   box in a box — the same "extra outline" Karl reported on Recruitment — and a
   rail that wraps to three lines is not a rail anyway. The CHIPS keep their own
   pill shape from `.h-seg--pill button`; only the container stands down.
   ⚠️ SCOPED TO `.pg-filters` AND NOWHERE ELSE. `.pg-seg` is also the form
   field's rail (`segField`, programs.js:3047) and that one is still a rail. */
.pg-filters .pg-seg .h-seg {
  flex-wrap: wrap; gap: 6px;
  width: auto; min-width: 0;
  background: none; border: 0; border-radius: 0; padding: 0;
}
/* 🔴 AND THE CHIP HAS TO CARRY ITS OWN EDGE ONCE THE RAIL IS GONE, WHICH I
   ONLY SAW BY RENDERING IT. `.h-seg button` is `background: transparent;
   border: none` — correct ON a rail, because the rail supplies the surface that
   says "these are controls". Strip the rail and an UNSELECTED chip becomes
   plain text, which put it one grey away from the DISABLED one I had just
   turned into plain text on purpose. **Pressable and not-pressable must not
   look the same**, and my first cut made them nearly identical.
   ⚠️ THIS DOES REDECLARE THE SHARED CONTROL'S PAINT, which this file's own note
   forbids as a habit — so it is scoped to `.pg-filters` and it is here because
   the CONTEXT differs, not because the look was disliked: with no rail there is
   no other surface to carry the affordance. `.is-on` is untouched and still
   comes from `components.css`. */
/* 🪞 AND THE FIRST CUT OF THIS RULE BROKE THE SELECTED CHIP, ONE PASS AFTER I
   BUILT THE RATCHET FOR EXACTLY THIS SHAPE. I wrote `background: var(--panel2)`
   unqualified; at (0,4,0) it outranks `components.css`'s
   `.h-seg button.is-on { background: var(--green) }` at (0,2,0), so the chosen
   chip kept its dark-green INK on a dark panel — measured at **1.11:1**, which
   is invisible.
   ⛔ `css-dupes` CANNOT SEE THIS AND I CHECKED: it finds one selector declared
   twice, and this is two DIFFERENT selectors where the more specific one wins a
   property. Same family, different mechanism — worth knowing before anyone
   reads that ratchet as covering it.
   ✅ SO THE BASE RULE SETS GEOMETRY ONLY, and the surface is scoped to the
   state that actually needs one. A transparent border on every chip keeps all
   three states the same size, so nothing moves as the selection changes. */
.pg-filters .pg-seg .h-seg button { flex: 0 0 auto; border: 1px solid transparent; }
.pg-filters .pg-seg .h-seg button:not(.is-on):not([disabled]) {
  background: var(--panel2); border-color: var(--line2);
}
/* ⚖️ THE `(0)` RULING, WHICH THE ROW ASKED FOR IN THIS PASS.
   The option is ALREADY inert — `programs.js:711` disables it on
   `o.count === 0 && !on`, and that was ruled and shipped. What was left is what
   it should LOOK like, and `opacity: .38` on a pill was the wrong answer: a
   dimmed BUTTON reads as a control that is failing, not as a category that is
   empty. Karl's words for the old live version were "tappable-but-broken".
   ✅ SO IT STOPS BEING A BUTTON TO THE EYE: no fill, no border, no pill — the
   label sits in the row as greyed TEXT, which is what it is. The information
   survives ("Goalies only" exists, and it is at zero); the affordance does not.
   ⛔ NOT HIDDEN, and that is this file's own earlier ruling which still holds:
   a chip that vanishes under your thumb takes the way back with it, and the
   CURRENT selection stays pressable at `(0)` because `!on` guards it. */
.pg-filters .pg-seg .h-seg button[disabled] {
  background: none; border-color: transparent;
  padding-left: 4px; padding-right: 4px;
  color: var(--faint); font-weight: 600;
}
.pg-filtl {
  font-size: 10.5px; font-weight: 800; letter-spacing: .06em;
  text-transform: uppercase; color: var(--faint);
}
/* THE ONLY THING THE SHARED CONTROL DOES NOT DO: HOLD MORE CHIPS THAN FIT.
   `programType` is an open list, so Type can be any length. `.h-seg button` is
   `flex: 1`, which would squash eight types into unreadable slivers rather than
   scroll — so this releases the flex and lets the row scroll sideways. NO
   COLOUR, NO STATE, NO TRANSITION IS REDECLARED: `components.css` carries a
   long note about a per-page copy of this control's rule being "a trap for the
   third page", and it was written after that happened twice. */
/* `SYS-17`, SECOND PASS — THE SAME WRAPPER NOW DRESSES EVERY SEGMENTED FIELD
   IN THIS FILE, not only the browse filters, so its rules move out of the
   filter block and its comment moves with them. */
.pg-fld--seg { gap: 4px; }
.pg-fld--seg > span { margin-bottom: 0; }
.pg-seg { flex: 1 1 auto; min-width: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.pg-seg::-webkit-scrollbar { display: none; }
.pg-seg .h-seg { margin: 0; width: max-content; min-width: 100%; }
.pg-seg .h-seg button { flex: 0 0 auto; white-space: nowrap; }
/* A FILTER THAT WOULD RETURN NOTHING IS NOT OFFERED. Karl's `BadFindSessions`
   screenshot has "Players only (0)" and "Goalies only (0)" sitting there as
   live chips — the count was put on the chip to be read and was never wired to
   whether the chip does anything.

   ⛔ AN ATTRIBUTE SELECTOR, NOT A NEW CLASS, DELIBERATELY. `client-contracts`
   requires a rule for every class this file emits, and a `.pg-seg__empty` would
   be a name to keep in step for a state the DOM already carries. `disabled` is
   the state; this is what it looks like.
   📌 STRUCTURE ONLY — no colour of the shared control is redeclared here, so
   `SYS-04`'s paint can restyle the rail without touching this rule. */
.pg-seg .h-seg button[disabled] { opacity: .38; cursor: not-allowed; }
.pg-fld input, .pg-fld select, .pg-fld textarea, .pg-rolebox input {
  background: var(--panel2); color: var(--ink);
  border: 1px solid var(--line2); border-radius: var(--r-sm);
  padding: 8px 9px; font-family: var(--font-body);
  font-size: 16px; /* SYS-07: was 13px; under 16 the field zooms iOS */
  width: 100%; box-sizing: border-box;
}
.pg-clear {
  background: transparent; border: 1px solid var(--line2); color: var(--muted);
  border-radius: var(--r-sm); padding: 8px 11px; font-size: 12px; cursor: pointer;
}

/* --- cards --------------------------------------------------------------- */
.pg-card {
  position: relative; isolation: isolate; overflow: hidden;
  display: block; width: 100%; min-height: 148px; text-align: left;
  background: var(--panel); border: 1px solid var(--line);
  /* `S2-02` — horizontal only; the vertical stays `--sp-1`. */
  border-radius: var(--r-2xl); padding: var(--sp-1) var(--sp-4);
  margin-bottom: 9px; cursor: pointer; color: inherit;
  box-shadow: var(--sh-1);
  transition: border-color var(--t-fast), transform var(--t-fast);
}
.pg-card-media, .pg-card-shade { position: absolute; z-index: -2; inset: 0; }
/* The photo is the Space's own header, set inline by `programs.js` cardMedia()
   (cleanup release, 2026-10-05: the fixed program / session photos are retired). */
.pg-card-media {
  background: var(--panel2) center / cover no-repeat;
  transform: scale(1.01); transition: transform var(--t-med);
}
.pg-card-shade {
  z-index: -1;
  background: linear-gradient(90deg, var(--scrim-identity), var(--scrim-modal));
}
.pg-card-content { display: block; position: relative; z-index: 1; }
.pg-card:hover .pg-card-media { transform: scale(1.045); }
.pg-card .pg-card-h b, .pg-card .pg-seat b { color: var(--on-colour); }
.pg-card .pg-card-h i, .pg-card .pg-note, .pg-card .pg-seat { color: var(--on-colour); }
.pg-card .pg-seat {
  background: var(--scrim-chip-hover); border-color: var(--hairline-on-scrim);
}
.pg-card:hover { border-color: var(--line2); transform: translateY(-1px); }
.pg-card-top { display: flex; align-items: center; gap: 9px; }
.pg-card-h { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.pg-card-h b { font-family: var(--font-display); font-size: 14px; font-weight: 800; color: var(--ink); }
.pg-card-h i { font-style: normal; font-size: 11.5px; color: var(--muted); }
/* THE PROGRAM EMBLEM. Sized and clipped here; DRAWN by groupavatar.js, which
   picks its own ink from the organiser's colours (`textOn`). Nothing in this
   rule sets a colour, and that is the point — see the note in programs.js. The
   container clips, so the same markup is a disc here and a rounded rect in a
   hero, exactly as the group emblem already works. */
.pg-disc {
  width: 34px; height: 34px; flex: none;
  border-radius: var(--r-sm); overflow: hidden;
  display: block;
}

/* The type colour, as ONE rule. `--pg-accent` falls back to the brand purple,
   so a type nobody has styled is still a program-coloured card rather than a
   colourless one. */
.pg-card--program { border-left: 3px solid var(--pg-accent, var(--purple)); }

/* ⚠️ REKEYED 2026-08-22 TO THE LIVE `Program Types` VALUE LIST. The five rules
   that used to be here were keyed off a vocabulary this seat invented —
   `power-skating`, `learn-to-play`, `clinic`, `conditioning`, `league-prep`
   have never appeared on a Hockey365 programme, and `goalie` (singular) missed
   the two real `Goalies` programmes by one letter, so they drew the fallback
   while a rule sat here that looked like it covered them.

   SIX VALUES, FOUR ACCENT TOKENS, so two pairs share — grouped by AXIS rather
   than by whatever was left over. `Players`/`Defence`/`Forwards` are one
   family (the list is mostly a role axis), and `Goalies` keeps its own because
   position is the thing a member filters for. */
.pg-type-skills { --pg-accent: var(--green); }
.pg-type-goalies { --pg-accent: var(--goalie); }   /* goalies are blue app-wide (Karl, walk v37) */
.pg-type-players, .pg-type-defence, .pg-type-forwards { --pg-accent: var(--info); }
.pg-type-camp { --pg-accent: var(--purple); }

/* WHO IS WORKING TONIGHT. Sits under the rooms panel and is deliberately
   quieter than the roster: the desk's main job is the players, and the crew is
   a thing they check once and glance at afterwards. */
.pg-door-crew {
  margin: 8px 0 0; padding: 10px 12px;
  background: var(--panel2); border: 1px solid var(--line2); border-radius: var(--r-md);
}
.pg-crew-h {
  font-size: 11px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 6px;
}
.pg-crew-row {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 4px 0; border-top: 1px solid var(--line2);
}
.pg-crew-row:first-of-type { border-top: 0; }
.pg-crew-row i { color: var(--muted); font-style: normal; font-size: 12px; }
/* THE STATE SITS AT THE END OF THE ROW AND CARRIES ITS OWN COLOUR, because a
   desk scans this column and nothing else. Arrived is muted, NOT arrived is
   the one worth an accent — the opposite of the instinct, and right: the list
   exists to find who is missing. */
.pg-crew-state { margin-left: auto; font-size: 12px; font-weight: 700; color: var(--warn); }
.pg-crew-row--in .pg-crew-state { color: var(--muted); font-weight: 600; }
.pg-crew-none { font-size: 13px; color: var(--muted); }
/* A REFUSAL IS NOT AN ABSENCE and must not read like one. */
.pg-crew-shut { font-style: italic; }
.pg-crew-open { margin-top: 6px; font-size: 12px; font-weight: 700; color: var(--warn); }

/* ⚠️ REKEYED AGAIN 2026-08-22 (second pass), and the list is now KINDS ONLY —
   the position values moved to a derived axis (`focusOf`). The `goalies` and
   `players` rules below are LEGACY: three real rows still carry them until the
   retype lands, and deleting the rules first would grey out the exact
   programmes this round is about. They retire with the rows. */
.pg-type-academy { --pg-accent: var(--info); }
.pg-type-development { --pg-accent: var(--purple); }

/* WHO IT IS FOR, when that is not everybody. Deliberately louder than the type
   pill: "Goalies only" changes whether the reader is eligible, and the type
   does not. */
.pg-pill--focus { color: var(--ink); border-color: var(--warn); }

/* NO TYPE IS NOT A TYPE, AND THIS IS THE MAJORITY CASE: `typeSlug(null)` is
   `other`, and 10 of Karl's 19 programmes land here. On the brand fallback
   they rendered identically to a Camp — a colour key saying something untrue
   about half the catalogue. A muted line still reads as a programme card and
   claims no category. */
.pg-type-other { --pg-accent: var(--line2); }

.pg-pill {
  font-size: 10px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase;
  color: var(--muted); background: var(--panel2);
  border: 1px solid var(--line2); border-radius: var(--r-sm);
  padding: 3px 7px; flex: none;
}
.pg-pill--live { color: var(--on-green); background: var(--green); border-color: var(--green); }
.pg-pill--drop { color: var(--on-purple); background: var(--purple-solid); border-color: var(--purple); }

/* REG-02 — ARE *YOU* IN. Two states and no third: Karl named both and both
   exist in live ("registered" / "wait list").

   ⚖️ IT IS THE SOLID GREEN, THE SAME WEIGHT AS `--live`, BECAUSE IT IS THE ONE
   PILL ON THE CARD THAT IS ABOUT THE READER RATHER THAN THE PROGRAMME. A muted
   badge sitting among Type and Focus is a badge that gets scanned past, which
   is the whole complaint — Karl was scrolling a list and could not tell.

   ⛔ AND THE WAITLIST VARIANT IS UNSCOPED HERE ON PURPOSE, BESIDE THE DOOR'S.
   `.pg-door-row .pg-pill--wait` below already dresses this name for the check-in
   list; that rule is more specific and keeps the door exactly as it is, in any
   source order. What was missing was a rule for the name OUTSIDE the door — the
   `.pg-pill--static` cascade lesson recorded further down this file is the same
   shape, and the fix there was to be explicit about which selector owns which
   context rather than to reuse one and hope. */
.pg-pill--reg { color: var(--on-green); background: var(--green); border-color: var(--green); }
.pg-pill--wait {
  background: linear-gradient(rgba(var(--warn-rgb), .2), rgba(var(--warn-rgb), .2)), var(--panel);
  color: var(--warn-ink-on-wash); border-color: var(--warn);
}
[data-theme="light"] .pg-pill--wait {
  background: linear-gradient(rgba(var(--warn-rgb), .28), rgba(var(--warn-rgb), .28)), var(--panel);
}

/* --- per-role seats ------------------------------------------------------ */
.pg-seats { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px; }
.pg-seat {
  display: inline-flex; align-items: baseline; gap: 5px;
  font-size: 11.5px; color: var(--muted);
  background: var(--panel2); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 4px 8px;
}
.pg-seat b { color: var(--ink); font-weight: 800; font-size: 11px; }
.pg-seat i { font-style: normal; color: var(--green); font-weight: 700; }
.pg-seat.is-full { color: var(--faint); }
.pg-seat.is-full i { color: var(--faint); }
.pg-seats--none { color: var(--faint); font-size: 11.5px; margin-top: 9px; }

/* --- hero / detail ------------------------------------------------------- */
/* ===========================================================================
   `SYS-30` — THE HERO'S TEXT SAT ON GROUP DATA AND WAS UNREADABLE.

   `programs.js:1280` paints this box with an INLINE
   `linear-gradient(135deg, <the group's colour2>, #0d0d0d)`. An inline style
   beats any stylesheet, so the ground under this text cannot be set from here
   and cannot be predicted at all: `functions/lib/groupidentity.js:79` accepts
   ANY valid hex, so `colour2` can be pure white.

   The text was `var(--on-purple)`, which is #0A0A0A in LIGHT. Near-black ink
   on the near-black end of the gradient measured **1.02:1** — the programme's
   name, kind and dates were effectively invisible. Karl reported it; it is
   `SYS-30`.

   ⛔ AN INK TOKEN ALONE CANNOT FIX THIS, and that is the whole point. Flip the
      ink to white and it fails the other way the moment a group picks a pale
      colour — which is one tap in the identity picker. There is no single ink
      that is safe over a ground the member chooses.

   ✅ SO THE GROUND IS MADE PREDICTABLE INSTEAD, and the group keeps its colour:
      · a scrim sits between the gradient and the text, transparent across the
        top so `colour2` still reads at FULL strength in a band, then opaque
        from 58px down, which is where the text begins.
      · `padding-top` is 64px so the first line always starts inside the
        opaque part. A wrapping name only grows downward, further into it.
      · every line is `--on-colour` at FULL strength.

   🔻 AND THE `opacity: .85` ON TWO OF THE LINES IS GONE, not moved.
      `hub.css` states the rule 440 lines from where it was broken: *"NOT
      opacity, which multiplies through to the text and drags it under AA."*
      Here it cost about a third of the contrast and it bought nothing — the
      hierarchy is already 10px/800 uppercase against 19px/800 against 12px.
      Measured over the worst ground a group can pick:
        white ink, no opacity        6.20   ok
        white ink at .85             5.03   ok but for no reason
        any dimmed grey (#E4E4E4)    3.36   FAILS at every workable scrim
      There is no dimmed ink that is safe here. Size and weight do the work.
   ========================================================================= */
.pg-hero {
  position: relative; overflow: hidden; isolation: isolate;
  border-radius: var(--r-2xl); padding: 64px var(--sp-1) var(--sp-5);
  margin-bottom: var(--sp-1); box-shadow: var(--sh-2);
}
.pg-hero::before {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: linear-gradient(to bottom,
    transparent 0, transparent 38px,
    var(--scrim-identity) 58px, var(--scrim-identity) 100%);
}
.pg-hero-k, .pg-hero-n, .pg-hero-s { position: relative; z-index: 1; }
.pg-hero-k {
  font-size: 10px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  color: var(--on-colour);
}
.pg-hero-n { font-family: var(--font-head); font-size: 19px; font-weight: 800; color: var(--on-colour); margin-top: 4px; }
.pg-hero-s { font-size: 12px; color: var(--on-colour); margin-top: 3px; }

.pg-already {
  border: 1px solid var(--green); background: var(--green-dim);
  border-radius: var(--r-2xl); padding: var(--sp-1); margin-bottom: var(--sp-1);
}
.pg-already-row { font-size: 12.5px; color: var(--ink); }
.pg-already-row + .pg-already-row { margin-top: 5px; }
.pg-link { background: none; border: 0; color: var(--green); font-size: 12px; font-weight: 700; cursor: pointer; padding: 0; }

.pg-includes {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-2xl); padding: var(--sp-1); margin-top: var(--sp-1);
  font-size: 12.5px; color: var(--muted); line-height: var(--lh);
}
.pg-includes b { display: block; color: var(--ink); font-size: 11px; font-weight: 800;
  letter-spacing: .06em; text-transform: uppercase; margin-bottom: 4px; }

.pg-row {
  display: flex; align-items: center; justify-content: space-between; gap: 9px;
  width: 100%; text-align: left; margin-top: var(--sp-1);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-2xl); padding: 12px var(--sp-1);
  color: inherit; cursor: pointer; font-size: 13px;
}
.pg-row b { font-weight: 800; color: var(--ink); }
.pg-row i { font-style: normal; color: var(--muted); font-size: 12px; }

.pg-sess {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  background: var(--panel); border: 1px solid var(--line);
  /* `S2-02` — same 4px inset as `.pg-card`, same fix. */
  border-radius: var(--r-sm); padding: 10px var(--sp-4); margin-bottom: 6px;
}
.pg-sess b { font-size: 13px; font-weight: 700; color: var(--ink); flex: 1 1 auto; min-width: 0; }
.pg-sess i { font-style: normal; font-size: 11.5px; color: var(--muted); }
/* A PAST SESSION IS DIMMED, NOT HIDDEN. Karl's call, recorded in the live
   ProgramsJSON v2.4: a member three weeks into a clinic wants to see the three
   they have done as much as the five they have not. */
.pg-sess.is-past { opacity: .55; }
/* ⛔ A CALLED-OFF SESSION IS NOT MERELY DIMMED. `is-past` is "this already
   happened", which is ordinary; this is "this is NOT happening", which the
   organiser has to be able to tell apart at a glance on a list of eight rows —
   and opacity alone reads as the same fact twice. It takes the warn edge this
   file uses for everything a human still has to decide about, and the name
   itself is struck through, which is the one piece of typography that says
   "cancelled" without a word.

   ⚠️ NOT `opacity` ON TOP. This row carries the only control that can undo the
   cancellation, and a control at 55% on a warn-tinted panel is the contrast
   failure this file has already been caught by once. */
.pg-sess.is-off {
  border-color: var(--warn);
  background: var(--panel2);
}
.pg-sess.is-off b { text-decoration: line-through; color: var(--muted); }
.pg-sess.is-off i { color: var(--warn-ink-on-wash); }

.pg-mine {
  display: flex; flex-direction: column; gap: 2px; width: 100%; text-align: left;
  background: var(--panel2); border: 1px solid var(--line);
  /* `S2-02` — the third row type on this screen, see the header. */
  border-radius: var(--r-sm); padding: 10px var(--sp-4); margin-bottom: 6px;
  color: inherit; cursor: pointer;
}
.pg-mine b { font-size: 13px; font-weight: 700; color: var(--ink); }
.pg-mine i { font-style: normal; font-size: 11.5px; color: var(--muted); }

/* --- the form ------------------------------------------------------------ */
.pg-people, .pg-roles { display: flex; flex-wrap: wrap; gap: 6px; }
.pg-person, .pg-role {
  flex: 1 1 30%; min-width: 100px;
  display: flex; flex-direction: column; gap: 2px; text-align: left;
  background: var(--panel); border: 1px solid var(--line2);
  border-radius: var(--r-sm); padding: 10px var(--sp-1);
  color: inherit; cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.pg-person b, .pg-role b { font-size: 13px; font-weight: 800; color: var(--ink); }
.pg-person i, .pg-role i { font-style: normal; font-size: 11px; color: var(--muted); }
.pg-person.is-on, .pg-role.is-on { border-color: var(--green); background: var(--green-dim); }
/* REG-01 — ALREADY REGISTERED. Karl asked for "gray out my name", so it greys
   rather than disappearing, and it keeps its full-size name: this row is still
   information (that person IS in), not a control that failed. `not-allowed`
   rather than `pointer` because the cursor is the only affordance a mouse user
   gets before they press, and `opacity` alone reads as "loading". */
.pg-person.is-taken {
  opacity: .55; cursor: not-allowed;
  background: var(--panel2); border-style: dashed;
}
.pg-person.is-taken i { color: var(--warn-ink-on-wash); font-weight: 700; }
/* FULL IS SHOWN, NOT HIDDEN — it is still a thing they can do (join the
   waitlist), and a role that vanishes reads as an app that lost it. */
.pg-role.is-full { border-style: dashed; }

.pg-total {
  display: flex; align-items: baseline; gap: 8px;
  margin-top: var(--sp-5);
  padding-top: var(--sp-1); border-top: 1px solid var(--line);
}
.pg-total b { font-family: var(--font-head); font-size: 20px; font-weight: 800; color: var(--ink); }
.pg-total i { font-style: normal; font-size: 12px; color: var(--muted); }

/* --- notes and refusals -------------------------------------------------- */
.pg-note {
  margin-top: var(--sp-1); padding: 10px var(--sp-1);
  background: var(--panel2); border: 1px solid var(--line);
  border-left: 3px solid var(--line2);
  border-radius: var(--r-sm);
  font-size: 12px; color: var(--muted); line-height: var(--lh);
}
.pg-note b { color: var(--ink); }
.pg-note--stop { border-left-color: var(--warn); color: var(--warn-ink-on-wash); }

/* THE MOCK BANNER. Deliberately loud — a mock that is silently live in front
   of members is the failure it exists to prevent, and it removes itself the
   moment the real engine answers. */
.pg-seamnote {
  margin-top: var(--sp-5); padding: 10px var(--sp-1);
  border: 1px dashed var(--purple); border-radius: var(--r-sm);
  background: var(--purple-dim);
  font-size: 11.5px; color: var(--ink); line-height: var(--lh);
}

/* --- confirm ------------------------------------------------------------- */
.pg-done { text-align: center; padding: var(--sp-5) var(--sp-1); }
.pg-done-i { font-size: 34px; line-height: 1; color: var(--green); }
.pg-done-h { font-family: var(--font-head); font-size: 18px; font-weight: 800; color: var(--ink); margin-top: 9px; }
.pg-done-p { font-size: 13px; color: var(--muted); margin-top: 6px; line-height: var(--lh); }

/* --- build --------------------------------------------------------------- */
.pg-fld { display: flex; flex-direction: column; gap: 4px; margin-bottom: 9px; }
.pg-fld span { font-size: 10.5px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--faint); }
.pg-grid3 { display: flex; gap: 6px; flex-wrap: wrap; }
.pg-rolebox {
  flex: 1 1 30%; min-width: 110px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: var(--sp-1);
}
.pg-rolebox b { display: block; font-size: 12px; font-weight: 800; color: var(--ink); margin-bottom: 6px; }
.pg-rolebox label { display: flex; flex-direction: column; gap: 3px; margin-bottom: 6px; }
.pg-rolebox label span { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--faint); }

.pg-toggle {
  background: var(--panel2); border: 1px solid var(--line2); color: var(--muted);
  border-radius: var(--r-sm); padding: 6px 10px; font-size: 11.5px; font-weight: 700; cursor: pointer;
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
}
.pg-toggle.is-on { color: var(--on-purple); background: var(--purple-solid); border-color: var(--purple); }

/* --- sticky actions ------------------------------------------------------ */
.pg-actions {
  flex: none; padding: var(--sp-1);
  border-top: 1px solid var(--line); background: var(--panel);
  display: flex; flex-direction: column; gap: 8px;
}
.pg-cta {
  width: 100%; border: 0; border-radius: var(--r-sm);
  background: var(--green); color: var(--on-green);
  font-family: var(--font-head); font-size: 14px; font-weight: 800;
  padding: 13px; cursor: pointer;
  transition: opacity var(--t-fast);
}
.pg-cta:disabled { opacity: .45; cursor: default; }
.pg-cta--ghost { background: transparent; color: var(--ink); border: 1px solid var(--line2); }
.pg-cta--inline { margin-bottom: var(--sp-1); }

/* --- desktop ------------------------------------------------------------- */
/* DESKTOP MODE IS PRESERVED — Karl's Q5 answer says so in capitals ("Desktop
   mode must be preserved (it exists in the FileMaker version) — don't lose
   it"). The panel becomes a centred sheet rather than a full-bleed overlay,
   which is what every other detail surface in this build does at this width. */
@media (min-width: 900px) {
  .pg-panel {
    inset: 4vh 50% 4vh 50%;
    width: min(860px, 92vw);
    /* ⚠️ MINUS FIFTY. NOT FIFTY. READ BEFORE "TIDYING" THE SIGN.

       `inset` sets `left: 50%`, so the panel's LEFT EDGE starts at the middle
       of the viewport. `translateX(-50%)` pulls it back by half its own width,
       which is what centres it. The positive form pushed it the other way and
       shipped the panel almost entirely off the right edge.

       MEASURED BEFORE AND AFTER, not reasoned about. As shipped:
         1024 →  942..1802  (778px off)      1080 →  970..1830  (750px off)
         1194 → 1027..1887  (693px off)      1280 → 1070..1930  (650px off)
         1440 → 1150..2010  (570px off)
       With the sign corrected: 1024 → 82..942, 1280 → 210..1070 — centred to
       the pixel at both, and on screen at every width from 900 up.

       🩺 THIS WAS NEVER A TABLET BUG, WHICH IS WHY IT SURVIVED SO LONG. Karl
       found it on an iPad, but the rule starts at 900px: a tablet in PORTRAIT
       (768/810/834) is below that and gets the correct full-bleed panel, while
       LANDSCAPE and every laptop are above it and got this. The overlay has
       been opening off screen on desktop since the rule was written; the iPad
       was simply the first device he used that crossed 900 while he held it.

       🔍 AND IT IS A LONE TYPO, NOT A HABIT — checked rather than assumed:
       this was the only `translateX(50%)` in either deploy tree, against one
       correct `translateX(-50%)` in `hub.css`. */
    transform: translateX(-50%);
    border: 1px solid var(--line2);
    border-radius: var(--r-2xl);
    box-shadow: var(--sh-3);
    overflow: hidden;
  }
  .pg-grid3 { flex-wrap: nowrap; }
  .pg-actions { flex-direction: row; }
  .pg-cta { width: auto; flex: 1; }
}

/* --- "I can't make this one" -------------------------------------------- */
/* The per-person seat control on a session row, and the organiser's
   free-versus-released readout. Both live on `.pg-sess`, which is why they
   wrap rather than sit in a fixed column: a session with three household
   members on it is a real case and a row that scrolls sideways is not. */
.pg-seatctl {
  flex-basis: 100%;
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line);
}
.pg-seatctl-w { font-size: 11.5px; color: var(--muted); }
.pg-mini {
  background: var(--green); color: var(--on-green); border: 0;
  border-radius: var(--r-sm); padding: 5px 10px;
  font-size: 11px; font-weight: 800; cursor: pointer;
  transition: opacity var(--t-fast);
}
.pg-mini.ghost { background: transparent; color: var(--muted); border: 1px solid var(--line2); }
/* THE ONLY DESTRUCTIVE CONTROL IN THIS FILE, so it is the only red one. Red is
   reserved here for refusals and for this — a control that takes a seat away
   and moves an invoice should not look like the control beside it that records
   an arrival. It only ever appears as the SECOND tap, so its job is to be
   unmistakable rather than inviting. */
.pg-mini.danger {
  /* --danger-solid, NOT --danger (Meryl, 2026-08-22 — Theo, revert freely).
     `--danger` is the INK token and `--danger-solid` is the FILL half of the
     pair, exactly as `--warn`/`--warn-solid` and `--green`/`--on-green` work.
     Painting `--on-danger` (#fff) on `--danger` (#ff6b6b) measures **2.78:1**
     and `tests/contrast.test.mjs` named this line — the same 2.78 Enzio filed
     against `.h-btn--danger` on 08-12, which is why the solid pair exists at
     all. #fff on #c92a2a is 5.46:1 in BOTH themes and clears 3:1 against every
     surface it sits on. One word, and the fallback `, #fff` goes with it:
     `--on-danger` is defined in tokens.css and a fallback for a token that
     always exists only hides the day it stops. */
  background: var(--danger-solid); border-color: var(--danger-solid); color: var(--on-danger);
  font-weight: 800;
}
/* The two halves of the confirm sit together and push the destructive one
   away from where the first tap landed, so the second tap is a deliberate
   move rather than a double-press on the same spot. */
.pg-confirm { display: inline-flex; gap: 6px; align-items: center; }
/* Quiet until asked for. Remove is the rarest thing anybody does at a door. */
.pg-drop { opacity: .75; }
.pg-mini:hover { opacity: .85; }

/* --- bringing somebody without an account -------------------------------- */
/* The guest block sits inside the SAME form as the family picker, because that
   is what it is: Karl's "in ADDITION to other people from their family that
   they added". A separate flow would produce a second registration and a
   second invoice, which is the thing the one-submission shape exists to
   prevent. */
.pg-guest {
  border: 1px dashed var(--line2); border-radius: var(--r-sm);
  padding: var(--sp-1); margin-bottom: 8px; background: var(--panel);
}
.pg-guest-h {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 8px;
}
.pg-guest-h b { font-size: 12px; font-weight: 800; color: var(--ink); }
.pg-guest-f { display: flex; flex-wrap: wrap; gap: 6px; }
.pg-guest-f .pg-fld { flex: 1 1 45%; min-width: 130px; margin-bottom: 0; }

/* --- P-005: THE DOOR, AND THE WALK-IN SHEET ------------------------------ */
/* THESE SEVEN CLASS NAMES SHIPPED WITH NO RULES AT ALL. The door screen was
   written, tested, routed and marked done while `.pg-door-row`,
   `.pg-door-who`, `.pg-door-head`, `.pg-door-warn`, `.pg-door-w`, `.pg-search`
   and `.pg-req` matched nothing in any stylesheet — so the one screen in this
   build whose entire justification is TOUCH-FIRST SIZING at a rink desk
   rendered as unstyled stacked text with a 10px badge where the check-in
   button should be.

   IT IS THE HOUSE DEFECT WEARING A NEW COAT: a surface reading something
   nothing writes. Every previous instance was a column or an API field; a
   class name is the same contract with a stylesheet on the other end, and it
   fails more quietly because the screen still renders — just uselessly.
   `tests/client-contracts.test.mjs` now pins it: every class the client emits
   must have a rule or be named in an explicit list of deliberate hooks. */

/* THE COUNTS STRIP — live's own wording, "12 / 18 checked in · 4 waitlisted".
   Big, because it is read across a table by somebody who is not holding it. */
.pg-door-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px;
  padding: var(--sp-1); margin-bottom: var(--sp-1);
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm);
}
.pg-door-head b { font-family: var(--font-head); font-size: 17px; font-weight: 800; color: var(--ink); }
.pg-door-head span { font-size: 12px; color: var(--muted); }
.pg-door-w { color: var(--warn); font-weight: 700; }

/* THE SEARCH BOX — shared by the door list and the walk-in member picker.
   16px is not a taste decision: iOS zooms the whole page in on focus for any
   font-size below it, and a door tool that zooms when you tap search is a door
   tool somebody has to pinch back out of while a queue watches. */
.pg-search {
  width: 100%; box-sizing: border-box;
  background: var(--panel2); border: 1px solid var(--line2); border-radius: var(--r-sm);
  color: var(--ink); font-family: var(--font-body); font-size: 16px;
  padding: 12px var(--sp-1); margin-bottom: var(--sp-1);
}
.pg-search::placeholder { color: var(--faint); }
.pg-search:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px rgba(var(--green-rgb), .25); } /* `S2-109` */

/* ONE BIG ROW PER PERSON. `min-height: 60px` is the whole point of the screen:
   the target has to be findable without looking, by somebody talking to the
   person it belongs to. */
.pg-door-row {
  display: flex; align-items: center; gap: var(--sp-1);
  width: 100%; box-sizing: border-box; min-height: 60px;
  padding: 10px var(--sp-1); margin-bottom: 6px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm);
  text-align: left;
}
.pg-door-row.is-in { border-color: var(--green); background: var(--green-dim); }
.pg-door-who { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.pg-door-who b { font-family: var(--font-head); font-size: 15px; font-weight: 800; color: var(--ink); }
.pg-door-who i { font-style: normal; font-size: 11.5px; color: var(--muted); }
.pg-door-warn { color: var(--warn) !important; font-weight: 700; }

/* THE CHECK-IN CONTROL IS A BUTTON, NOT A BADGE. `.pg-pill` elsewhere in this
   file is a 10px uppercase label; the door reuses the class as its primary
   action, so it is re-sized IN THE DOOR'S CONTEXT rather than by giving the
   badge a second personality everywhere it appears. 44px is the smallest
   target Apple and Google both call reliable, and this one is pressed with a
   glove on. */
.pg-door-row .pg-pill {
  min-width: 104px; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; letter-spacing: .02em; text-transform: none;
  color: var(--on-green); background: var(--green); border-color: var(--green);
  cursor: pointer;
}
.pg-door-row .pg-pill.is-in {
  color: var(--muted); background: var(--panel2); border-color: var(--line2);
}
/* ⛔ AND THE KIOSK'S STATIC FORM OF THAT TICK — NOT A BUTTON, AND IT MUST NOT
   LOOK LIKE ONE.

   With undo correctly hidden on the kiosk (Karl, Q11) an already-arrived row
   carries a `<span>` rather than a `<button>`. My first attempt styled it as a
   bare `.pg-pill--static` — ONE class against `.pg-door-row .pg-pill.is-in`'s
   three, so it lost the cascade and changed nothing at all. **The rule read
   correctly and did nothing**, and I only noticed by screenshotting the
   finished screen: the ticks were still rendering as bordered, filled controls.

   WHY IT MATTERS ON THIS SCREEN SPECIFICALLY: the kiosk's entire instruction is
   "tap your name". Something that looks tappable and is not is worse than a
   plain label — the person taps, nothing happens, and they conclude the kiosk
   is broken rather than that they are already checked in. */
.pg-door-row .pg-pill.pg-pill--static {
  cursor: default;
  background: transparent; border-color: transparent;
  min-width: 0; padding-right: 0;
  color: var(--green); font-weight: 700;
}
/* LETTING SOMEBODY IN OFF THE WAITLIST IS A DIFFERENT DECISION FROM CHECKING
   SOMEBODY IN, so it does not wear the same green. Green on this screen means
   "the expected thing happened"; this one can go over the role cap and commit
   the person to an invoice, and a control that looks identical to the routine
   one will be pressed as if it were routine.

   IT IS STILL A FULL 44px TARGET — the amber is about telling two decisions
   apart, not about making one of them harder to hit with a glove on. The
   `Arrived` control beside it is `.pg-mini`, which is the correct hierarchy:
   seating them is the primary act and recording an arrival without a seat is
   the exception Karl's "prompt, do not adjudicate" rule keeps available.

   THE WASH IS MIXED THE HOUSE WAY, and my first draft named a token that does
   not exist (`--warn-wash`). Nothing would have gone red: an unknown custom
   property falls back to `transparent`, so the button would have rendered with
   no background at all and every assertion about it still passed. That is this
   project's own rule — a class name is a contract with a stylesheet — with the
   contract one level further down, against `tokens.css`. `components.css`
   already establishes the pattern (`.h-urg-pill`) and the reason light needs a
   heavier mix, so this copies it rather than inventing a second way. */
.pg-door-row .pg-pill--wait {
  background: linear-gradient(rgba(var(--warn-rgb), .2), rgba(var(--warn-rgb), .2)), var(--panel);
  color: var(--warn-ink-on-wash); border-color: var(--warn);
}
[data-theme="light"] .pg-door-row .pg-pill--wait {
  background: linear-gradient(rgba(var(--warn-rgb), .28), rgba(var(--warn-rgb), .28)), var(--panel);
}

/* DRESSING ROOMS ON THE DOOR. A panel rather than a line, because it is read
   by somebody being asked a question across a desk and answered out loud —
   it wants to be findable at a glance, not scanned for. */
.pg-door-rooms {
  position: relative;
  padding: 9px 11px; margin-bottom: var(--sp-1);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
.pg-room-line { font-size: 14px; font-weight: 700; color: var(--ink); line-height: 1.5; }
.pg-room-line b {
  font-family: var(--font-head); font-size: 11px; font-weight: 800;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
  margin-right: 4px;
}
/* Coaches and refs sit below the skaters' rooms and quieter — they are read by
   two people, not by everybody in the corridor. */
.pg-room-staff { font-size: 12.5px; color: var(--muted); }
.pg-room-none { font-weight: 400; color: var(--faint); font-style: italic; }
/* THE STANDING NOTE. Always present, every mode — it is a fact about the
   building, not about the session, so it never brightens or hides. */
.pg-room-note { margin-top: 6px; font-size: 11.5px; color: var(--faint); line-height: var(--lh); }
.pg-room-edit { position: absolute; top: 7px; right: 8px; }

/* THE ROOMS SHEET. Three mode cards that carry their own explanation, because
   the person choosing has never read the documentation and is at a rink. */
.pg-modes { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--sp-1); }
.pg-mode {
  display: flex; flex-direction: column; gap: 2px; text-align: left;
  padding: 10px 12px; cursor: pointer;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--ink);
}
.pg-mode.is-on { border-color: var(--green); background: var(--green-dim); }
.pg-mode b { font-family: var(--font-head); font-size: 13px; font-weight: 800; }
.pg-mode i { font-style: normal; font-size: 11.5px; color: var(--muted); line-height: var(--lh); }
/* A LABEL AND A ROOM ON ONE LINE, with the remove control small and last —
   the row is filled left to right and destroyed rarely. */
.pg-room-row { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
/* NAMED RATHER THAN SELECTED BY ELEMENT, and the class audit is why: I styled
   these as `.pg-room-row input`, which works and still leaves `.pg-room-lab`
   and `.pg-room-rm` as class names with no rule anywhere — the exact contract
   this project guards, passing only because a neighbouring selector happened
   to cover them. A name in the markup that no stylesheet mentions is a name
   the next person will change safely and break. */
.pg-room-lab, .pg-room-rm {
  flex: 1 1 0; min-width: 0;
  background: var(--panel2); color: var(--ink);
  border: 1px solid var(--line2); border-radius: var(--r-sm);
  padding: 9px; font-size: 13px; font-family: var(--font-body);
}
/* The room is the answer somebody reads out, so it is the heavier of the two. */
.pg-room-rm { font-weight: 700; }

/* A DESK ON THE LAUNCHER. It is a `.pg-card` first — same target, same hover,
   same shadow as every other card in this file — so the only thing this rule
   adds is the count pill's alignment and the waiting line beneath. */
.pg-doorcard .pg-pill { align-self: center; }
.pg-doorcard-w {
  margin-top: 6px; font-size: 11.5px; font-weight: 700;
  color: var(--warn-ink-on-wash);
}

/* WHEN AND WHERE. Quiet — it is orientation, not a number anybody acts on —
   but present, because the alternative is an organiser working the wrong
   session's door and not finding out until somebody is missing. */
.pg-door-when {
  font-size: 12px; color: var(--muted); margin-bottom: 6px;
}

/* ⏭ THE NEXT DESK. It sits with the when-and-where line because it is the same
   kind of fact, but it is a BUTTON and has to read as one at a rink desk — so
   it does not inherit the muted 12px of the line above it.

   ITS OWN WRAPPER, not a flex row with the line above. The label carries a
   session name of unknown length; sharing a row would let a long name push the
   venue off the end of the strip, and the venue is the thing that stops an
   organiser working the wrong door.

   `max-width` + `text-overflow` RATHER THAN WRAPPING. A two-line button on a
   sticky header pushes the roster down on exactly the screen size where the
   roster matters most, and a truncated session name still tells you it is not
   the one you are on — the TIME, which never truncates, is the half that
   confirms it. */
.pg-door-nextwrap { margin: 0 0 8px; }
.pg-door-next {
  max-width: 100%;
  display: inline-block; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
  font-size: 12px; padding: 6px 11px;
}

/* HOW THE SIDES STAND — read at a glance, across a desk, at arm's length, so
   it is bigger than the counts line above it rather than smaller. The two
   halves are equal width so a difference in the NUMBERS is the only thing that
   moves; making them hug their content would let a long team name read as the
   bigger side. */
.pg-door-teams {
  display: flex; gap: 6px; margin-bottom: var(--sp-1);
}
.pg-door-side {
  flex: 1 1 0; min-width: 0;
  display: flex; align-items: baseline; gap: 5px;
  padding: 7px 10px; border-radius: var(--r-sm);
  background: var(--panel2); border: 1px solid var(--line2);
  font-size: 15px; font-weight: 800; color: var(--ink);
}
.pg-door-side b {
  font-family: var(--font-head); font-size: 11px; font-weight: 800;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
}
/* THE GOALIE NOTE IS THE QUIET HALF UNTIL IT IS THE LOUD HALF. `no goalie` is
   the state somebody is scanning for, so it takes the warn ink while a side
   that HAS one stays muted — the same amber this file uses for everything the
   organiser has to decide about. */
.pg-door-side i {
  font-style: normal; font-size: 11.5px; font-weight: 700; color: var(--muted);
}
.pg-door-side i.is-none { color: var(--warn-ink-on-wash); }
.pg-door-side--1 { border-left: 3px solid var(--ink); }
.pg-door-side--2 { border-left: 3px solid var(--line2); }

/* WHO OWES, ON THE ROW. Amber rather than red: an unpaid seat at a door is a
   thing to DO, not a thing that has gone wrong, and this build reserves red
   for refusals. It is the same wash the waitlist control uses because it is
   the same category — something the organiser has to decide about — mixed the
   house way from `--warn-rgb` rather than by naming a token that does not
   exist, which is exactly the mistake the pill above records. */
.pg-door-owes {
  font-style: normal; font-size: 12px; font-weight: 700;
  color: var(--warn-ink-on-wash);
}
/* AND "PAID" IS DELIBERATELY QUIET. It is reassurance, not an instruction —
   if it competed with the row above it, the two rows that need money would
   stop standing out and the whole line would be worth less than nothing. */
.pg-door-paid { font-style: normal; font-size: 12px; color: var(--muted); }
/* THE NIGHT'S TOTAL, in the counts strip. Same amber, same reason. */
.pg-door-owe { color: var(--warn-ink-on-wash); font-weight: 700; }

/* --- the walk-in sheet --------------------------------------------------- */
.pg-pickrow { cursor: pointer; }
.pg-pickrow.is-on { border-color: var(--green); background: var(--green-dim); }
/* ALREADY HERE — dimmed AND disabled, because the engine would refuse the
   duplicate anyway and the desk should see that before pressing, not after. */
.pg-pickrow.is-dim { opacity: .5; cursor: default; }
/* GENERATE TEAMS sits under the balance strip it acts on, and WRAPS rather than
   squeezing: the armed state is a whole sentence, and a sentence clipped to one
   line is a confirm nobody reads. */
/* THE CONTACT NUMBER, quieter than the emergency line above it: this is "how
   to reach them tonight", that one is "who to call if this goes wrong", and the
   row must not make them look like the same fact. */
.pg-door-tel { display: block; font-style: normal; font-size: 12px; color: var(--muted); }
.pg-door-tel a { color: inherit; }

/* THE TEAMS SWITCH sits above the balance strip and wraps, because on a phone
   two name fields and a checkbox will not sit on one line and a clipped input
   is a field nobody fills in. */
.pg-door-teamset {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-1);
  margin: 0 0 var(--sp-2);
}
.pg-door-teamset .pg-fld { flex: 1 1 8rem; }
.pg-fld--row { flex-direction: row; align-items: center; gap: var(--sp-1); }

.pg-door-gen {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1);
  margin: 0 0 var(--sp-2);
}
.pg-door-gen .pg-confirm { flex: 1 1 12rem; }

.pg-done-tick {
  font-size: 40px; line-height: 1; color: var(--green); margin-bottom: 6px;
}
/* A WAITLIST RESULT IS NOT A SUCCESS AND NOT A FAILURE, so it is neither green
   nor red. `--warn` is the token for "this worked and you need to know
   something about it", which is exactly what a waitlisted walk-in is. */
.pg-done--wait .pg-done-tick { color: var(--warn); }
.pg-done b { display: block; font-family: var(--font-head); font-size: 18px; font-weight: 800; color: var(--ink); }
.pg-done i { font-style: normal; font-size: 13px; color: var(--muted); }
/* A PARTIAL SUCCESS HAS TO LOOK DIFFERENT FROM A SUCCESS. `walkIn` does not
   undo the registration when check-in fails, so this note is the only thing
   standing between the organiser and a screen that claims both halves worked. */
.pg-note--warn {
  border-left-color: var(--warn); color: var(--warn-ink-on-wash);
}

/* --- the leftovers this audit turned up ---------------------------------- */
/* A CHECKBOX INSIDE `.pg-fld`, which is `flex-direction: column` — so every
   checkbox in this file rendered as a box on one line and its label on the
   next, under the uppercase micro-type meant for field captions. */
.pg-fld--check { flex-direction: row; align-items: center; gap: 9px; flex-wrap: wrap; }
.pg-fld--check input[type="checkbox"] { width: 22px; height: 22px; flex: none; margin: 0; }
.pg-fld--check span {
  flex: 1; font-size: 12.5px; font-weight: 600; letter-spacing: 0;
  text-transform: none; color: var(--ink);
}
.pg-fld--check .pg-hint { flex-basis: 100%; margin-top: 0; }
/* THE REQUIRED MARK. Emitted on every required field in the guest form and the
   walk-in sheet, and until now it inherited nothing — an asterisk the same
   colour and weight as the caption, which is no mark at all. */
.pg-req { color: var(--warn); font-weight: 800; }
.pg-sec--sub { margin-top: var(--sp-1); color: var(--muted); }
/* THE CLUB-JOIN OFFER (Karl: "1 button during and after registration"). */
.pg-note--offer { border-left-color: var(--purple); background: var(--purple-dim); }
/* THE ORGANISER'S WAY IN, sitting among member controls — it needs to read as
   a different kind of thing rather than as one more green button. */
.pg-manage { margin-left: auto; }
/* THE OTHER TWO CARD KINDS. `.pg-card--program` has carried a left accent
   since it was written and these two carried nothing — so a programme, a
   drop-in session and an organiser's own draft were three identical
   rectangles in three different lists. Same 3px rail, three readings:
   a session inherits the programme accent when it has one and falls back to
   the app's own green; an organiser's card is DASHED, which is this file's
   existing vocabulary for "not finished / yours to edit" (see `.pg-guest`
   and `.pg-role.is-full`). */
.pg-card--session { border-left: 3px solid var(--pg-accent, var(--green)); }
.pg-card--manage { border-left: 3px dashed var(--purple); }

/* --- the waiver, signed at the door -------------------------------------- */
/* THE CONSENT TEXT ITSELF. Scrollable rather than truncated, and set in body
   type at a readable size: this is the only thing on the screen the person is
   actually agreeing to, and a summary in small grey type is how somebody signs
   something nobody read to them. `pre-wrap` because the stored text carries
   its own paragraph breaks and re-flowing them into one block would change
   what the words look like, if not what they say. */
.pg-consent {
  max-height: 46vh; overflow-y: auto;
  padding: var(--sp-1); margin-bottom: var(--sp-1);
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--ink); font-size: 13px; line-height: var(--lh); white-space: pre-wrap;
}
/* THE ROW'S OWN WAY OUT OF AN UNSIGNED WAIVER. Sits under the name rather than
   beside the check-in pill: it is an exception path, and crowding the primary
   target is how a door tool gets slower to use for everybody. */
.pg-door-sign { align-self: flex-start; margin-top: 6px; min-height: 36px; }
/* WHO TO CALL. Deliberately NOT styled as a warning — it is not a problem, it
   is a resource, and colouring it like the unsigned-waiver line would make a
   door list look alarming when it is merely complete. The link is given a
   proper touch target because it is pressed one-handed, in a hurry, by
   somebody who is also talking to a person. */
.pg-door-ice { font-style: normal; font-size: 11.5px; color: var(--muted); }
.pg-door-ice a {
  display: inline-block; min-height: 24px; padding: 2px 4px;
  color: var(--ink); font-weight: 700; text-decoration: underline;
}
/* AND THE ABSENCE, WHICH IS THE OPPOSITE DECISION TO THE ONE ABOVE.
   The muted treatment exists so a complete door does not look alarming. A row
   with NO contact is not complete — since Karl's 2026-08-30 ruling it is an
   outstanding obligation, the same kind of thing as an unsigned waiver — so it
   is the one state on this line that is allowed to be noticed.

   `--warn-ink-on-wash`, NOT `--warn`, AND NOT A NEW TOKEN. It is the exact
   treatment `.pg-door-side i.is-none` already uses for the other "nothing here
   yet" state on this same row, so the two absences on one line read as one
   idea. `--warn` is the fill colour and has failed AA as text on this surface
   before; `--warn-ink-on-wash` is the token this project settled on precisely
   for warning TEXT, per theme. */
.pg-door-ice--none { color: var(--warn-ink-on-wash); font-weight: 700; }

/* --- WHY THE SIGN BUTTON IS SHUT (guest waiver, 2026-08-30) --------------
   `.pg-actions` is already a column with a gap, so the sentence sits above the
   button with no layout of its own. Muted, not warned: it is an instruction
   for the next thing to do, not a refusal — the row is a step in a form the
   person is choosing to complete, and colouring it as an error would make a
   waiver they are halfway through look like something that went wrong.

   EMPTY WHEN THE GATE IS OPEN, and the element stays in the DOM because it is
   `role="status"`: a live region that is removed and re-added is announced
   inconsistently, and this is the one sentence a screen-reader user needs to
   know why the button will not take. */
.pg-gate-msg {
  font-style: normal; font-size: 12px; color: var(--muted); text-align: center;
}
.pg-gate-msg:empty { display: none; }

/* --- WHAT REGISTERING LEFT YOU OWING (route one, 2026-08-29) -------------
   Inside `.pg-note`, which already carries the guest-waiver offer directly
   above it — same container, because they are the same kind of thing said to
   two kinds of person, and giving the member version its own panel would put
   two differently-shaped boxes on one confirmation screen saying "you still
   owe something".

   THE ROWS ARE STACKED, NOT INLINE. A name, then what is outstanding under it:
   at 320px an emergency-contact form beside a label wraps into an unreadable
   ladder, and this screen is read almost entirely on a phone. */
.pg-ob { margin-top: 10px; }
.pg-ob b { font-family: var(--font-head); font-size: 14px; }
.pg-ob-r { margin: 4px 0 8px; display: flex; flex-direction: column; gap: 6px; }
/* The footer is the sentence that keeps the promise non-blocking, so it is
   muted rather than warned: it is reassurance, not another obligation. */
.pg-ob-f { margin-top: 10px; font-size: 11.5px; color: var(--muted); }
.pg-f { display: flex; flex-direction: column; gap: 2px; font-size: 11.5px; color: var(--muted); }
/* 16px, and it is not a taste decision — the same reason `.pg-search` carries
   it: iOS zooms the whole page in on focus for any font-size below 16px, and a
   member who has just registered would watch the confirmation jump. */
.pg-in {
  width: 100%; box-sizing: border-box;
  background: var(--panel2); border: 1px solid var(--line2); border-radius: var(--r-sm);
  color: var(--ink); font-family: var(--font-body); font-size: 16px;
  padding: 10px var(--sp-1); min-height: 40px;
}
.pg-in::placeholder { color: var(--faint); }

/* --- which side, at the door -------------------------------------------- */
/* THE SIDE IS A BUTTON because tapping it moves them, and it sits BEFORE the
   check-in pill so the primary action stays where the thumb already is. Narrow
   on purpose: it is a label that happens to be tappable, not a second call to
   action competing with Check in. */
.pg-team {
  min-width: 62px; min-height: 44px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-sm); border: 1px solid var(--line2);
  font-size: 11.5px; font-weight: 800; letter-spacing: .02em;
  cursor: pointer;
}
/* DARK AND LIGHT AS THE FALLBACK NAMES ARE A LABEL, NOT A COLOUR CONTRACT —
   the real jersey colours live on the event and an organiser can name the sides
   anything. These two are told apart by weight and fill rather than by hue
   alone, because a door is read at arm's length by somebody who may not
   separate two colours quickly, and `measureThemes` cannot check that for us. */
.pg-team--1 { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.pg-team--2 { background: var(--panel2); color: var(--ink); border-color: var(--line2); }
/* PINNED BY A HUMAN. A dotted edge is this file's existing vocabulary for
   "somebody decided this" versus "we worked it out" — and it is the visible
   half of the rule that an undo will not clear it. */
.pg-team.is-locked { border-style: dotted; border-width: 2px; }
/* THE KIOSK'S JERSEY IS A LABEL, NOT A CONTROL — see `doorRow`. Same colours,
   because which side you are on is exactly what the attendee came to read; no
   pointer, because there is nothing to press.

   ⚠️ TWO CLASSES ON PURPOSE. `.pg-team` sets `cursor: pointer` with one class,
   so a single-class `.pg-team--static` would tie on specificity and depend on
   source order — which is precisely how `.pg-pill--static` lost its cascade
   earlier today and read correctly while doing nothing. */
.pg-team.pg-team--static { cursor: default; }

/* ✅ THE KIOSK CHECK-IN CONFIRMATION — Karl's design, see `paintDoor`.

   IT COVERS THE SCREEN, AND THAT IS THE REQUIREMENT RATHER THAN A STYLE. The
   person it is for tapped a name and is already turning away; a panel in the
   flow of a scrolling roster is one they will not see. It is also what makes
   the fifteen-second dismissal safe — nothing underneath is readable while it
   is up, so the previous person's row is not on display either.

   NO NEW COLOURS. The card is `--panel` on a dimmed `--bg`, and the jersey
   block reuses `.pg-team--1` / `--2`, which is what makes the colour on this
   panel the SAME colour as the pill on the row rather than a second opinion. */
.pg-kdone {
  /* FIXED, NOT ABSOLUTE. This markup sits inside `.pg-body`, which has no
     `position` of its own — so `absolute` would resolve against `.pg-panel` and
     work by accident today and stop the day anything between them gains a
     position. Fixed says what it means: it covers the viewport. */
  position: fixed; inset: 0; z-index: 5;
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp-1);
  /* ⚠️ SOLID `--bg`, NOT AN `rgba(var(--bg-rgb), …)` WASH — MY FIRST VERSION
     WROTE THE SECOND AND THERE IS NO `--bg-rgb` TOKEN IN THIS APP. An undefined
     custom property makes the whole declaration invalid, so it would have been
     DROPPED: the panel would have floated over a fully legible roster with no
     background at all, looking like a rendering bug rather than a missing
     token. `--green-rgb`, `--danger-rgb` and `--warn-rgb` exist; the surfaces
     do not have an rgb half. This file's own header lists the six times this
     project shipped a block written against tokens that do not exist.

     AND OPAQUE IS THE RIGHT ANSWER ANYWAY: while this panel is up the roster
     underneath must not be readable, which is the same privacy rule that clears
     the search box after a kiosk check-in. */
  background: var(--bg);
}
.pg-kdone-card {
  width: 100%; max-width: 420px;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: var(--sp-5) var(--sp-1);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-2xl); box-shadow: var(--sh-2);
  text-align: center;
}
.pg-kdone-tick { font-size: 34px; line-height: 1; color: var(--green); }
/* THE NAME IS THE BIGGEST THING ON THE PANEL because the first question a
   person asks a confirmation is "is this me" — a kiosk is a shared screen and
   the commonest mistake it can make is checking in the wrong person. */
.pg-kdone-name {
  font-family: var(--font-head); font-size: 26px; font-weight: 800;
  color: var(--ink); line-height: 1.15;
}
.pg-kdone-sub { font-size: 14px; color: var(--muted); }
/* THE JERSEY BLOCK. `--1` and `--2` bring the side's own background and ink,
   so the SVG — which is `fill: currentColor` — is painted in the colour that
   reads against it rather than in a colour of this rule's choosing. */
.pg-kdone-team {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 10px 16px; border-radius: var(--r-sm);
  border: 1px solid var(--line2);
  font-size: 16px; font-weight: 800;
}
.pg-kdone-jersey { display: inline-flex; width: 30px; height: 30px; }
.pg-kdone-jersey svg { width: 100%; height: 100%; }
.pg-kdone-room { font-size: 15px; font-weight: 700; color: var(--ink); }
.pg-kdone-ok { margin-top: 6px; min-width: 140px; min-height: 48px; }

/* --- the panel's heading ------------------------------------------------- */
/* `.pg-title` became an `<h2>` so the dialog has a name and a heading; these
   rules keep it looking exactly as the div did. `tabindex="-1"` makes it
   focusable BY SCRIPT only — `paint()` puts focus here when a repaint would
   otherwise drop it on the floor — and the outline is suppressed because this
   is a landing place, not a control somebody tabbed to. Keyboard focus on real
   controls is untouched and still uses base.css's `:focus-visible`. */
/* ONLY THE BROWSER'S `<h2>` DEFAULTS ARE RESET — the margin. The rule at the
   top of this file already sets `font-size: 15px` and `font-weight: 800`, and
   my first version of this block added `font-size: inherit; font-weight:
   inherit`, which sits LATER at equal specificity and therefore SILENTLY
   OVERRODE it. The comment above said it "keeps it looking exactly as the div
   did" while the code changed how it looked — the third comment-versus-code
   mismatch I have written myself this week, caught by reading the file rather
   than by any test, because nothing here measures type. */
.pg-title { margin: 0; }
.pg-title:focus { outline: none; }

/* V2.11 - B08 (Karl, 2026-09-28). The tab's name as a header, the tip under it. */
.pg-browse-h { font-family: var(--font-head); font-size: 17px; font-weight: 800; color: var(--ink); margin: var(--sp-2) 0 2px; }
/* A card's pills stack in one column at the right: type over Registered. */
.pg-card-pills { margin-left: auto; flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.pg-card-pills:empty { display: none; }
/* AY-01 v45: On-Ice / Dryland rides the day · arena line under the name. */
.pg-pill--inline { display: inline-block; vertical-align: 1px; margin-left: 4px; padding: 1px 5px; font-size: 9px; }

/* V2.12 - B22 note (Karl, 2026-09-28): search and a small Filters button on one row. */
.pg-filtersum { margin: 0 0 var(--sp-3); padding-left: 4px; font-size: 11.5px; color: var(--muted); }

/* V2.13 - the kiosk card's own undo, for 10 seconds (Karl, 2026-09-28). A quiet
   text button under OK: it must be findable by the person who mis-tapped and
   never mistaken for the main action. */
.pg-kdone-undo { display: block; margin: 10px auto 0; background: none; border: 0; padding: 6px 10px;
  font: inherit; font-size: 14px; font-weight: 700; color: var(--muted); text-decoration: underline; cursor: pointer; }
.pg-kdone-undo:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; border-radius: 6px; }

/* ---- THE STAFFED DOOR IN THE SHARED STAFF LANGUAGE (CONV-B, 2026-10-08) ----
   Registered / Here / Owing, three equal tiles that fit a 320px phone; Owing
   takes the warning colour only when somebody seated owes. Never drawn on a
   kiosk (programs.js). */
.pg-door-kp { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 10px 0 6px; }
.pg-door-kp > div { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md);
  padding: 8px 10px; min-width: 0; }
.pg-door-kp small { display: block; font-size: 11px; font-weight: 700; color: var(--muted); }
.pg-door-kp b { display: block; font-family: var(--font-head); font-size: 20px; font-weight: 800; color: var(--ink); }
.pg-door-kp > .is-owing b { color: var(--warn); }
.pg-door-sec { display: flex; align-items: center; gap: 7px; }
.pg-door-sec .h-ic { width: 15px; height: 15px; color: var(--green); }
.pg-door-tools { margin-top: 16px; }
