/* components.css V3.84 - 2026-10-05, Nolan. Partners: .h-panel--full (NR-02, the shared full-width right panel) and a 36px .h-sheet__x (NR-14). */
/* components.css V3.83 - 2026-10-05, Nolan. CLEANUP RELEASE: the legacy group picker (.h-gid*) and the shared colour picker (.h-pick*) are retired (archived). */
/* components.css V3.82 - 2026-10-05, Nolan. B65 G4: .h-sphoto / .gs-hero lay a designed Space's colour wash (--hero-wash) over the header. */
/* components.css V3.81 - 2026-10-05, Nolan. Character + Space C3: .gs-pav/.gs-av.has-face (a face inside the roster ring). */
/* components.css V3.80 - 2026-10-04, Nolan. Stats & Three Stars: the old `.gs-stars` card retired (`statsws.css`).
/* components.css V3.79 - 2026-10-03, Nolan. MST: `.h-toast__act` (the toast's optional Undo). */
/* components.css V3.78 - 2026-10-03, Nolan. AR-01: `.h-req` rows on the parent approval sheet. */
/* components.css V3.77 - 2026-10-03, Nolan. `.ice-attrib` moved here from ice.css (the member app never loads ice.css, so Discover showed the credit unstyled at 15px): a small tip, one line, small green links. */
/* components.css V3.76 - 2026-10-03, Nolan. FC-01: the wizard Back is one button (chevron + the word Step); only the chevron is drawn as a button. */
/* components.css V3.75 - 2026-10-03, Nolan. Karl: a wizard's back chevron beside Step N of M (`.h-wizsteps__back`, `.h-wizsteps__step`). */
/* components.css V3.74 - 2026-10-02, Nolan. A showing toast takes a tap (it moves the queue on). */
/* components.css V3.73 - 2026-10-01, Nolan. BC-01: `.h-fade-x`, shaded edges on any sideways date strip. */
/* components.css V3.72 - 2026-10-01, Nolan. `.h-post` matches the hub's V11 card (one edge, 2xl corners). */
/* components.css V3.71 - 2026-10-01, Nolan. Centre Ice V11 CI-2: the post header row (40px avatar, time on the right, body with its own top gap). */
/* components.css V3.70 - 2026-10-01, Nolan. `.gs-hero__swt` — the game title as the switcher. */
/* components.css V3.69 - 2026-10-01, Nolan. Walk v45: `.h-panel__av` (a Space's avatar in its Manage drawer); `.gs-hero--dright` / `__mid` / `__txt` — the game's date card on the right under the status chip. */
/* components.css V3.68 - 2026-10-01, Nolan. The Youth age chip fills with `--purple-solid` (contrast). */
/* components.css V3.67 - 2026-10-01, Nolan. AY-01 v45: `.h-agetip` is a small filled chip sized to its word (Youth on `--purple-solid`). */
/* components.css V3.66 - 2026-09-30, Nolan. RE-01 v45: `.h-rfound__alt` / `.h-rfound__retry` (Don't know this person? Try another email). */
/* components.css V3.65 - 2026-09-30, Nolan. Pinned buttons' glass 50% heavier (`--glass-btn-*`). */
/* components.css V3.64 - 2026-09-30, Nolan. `--pin-h` declared on `.h-sheet--foot` (ui.js measures it). */
/* components.css V3.63 - 2026-09-30, Nolan. Walk v45: the WIZARD PROTOCOL (`.h-wizsteps`, `.h-wizfoot`: wizard buttons in the flow, never pinned); pinned buttons (`.gs-pk__bar`, `.h-sheet--pin`) sit on the bottom edge with no bar and turn to glass themselves. */
/* components.css V3.62 - 2026-09-30, Nolan. Walk v42: `.h-agetip`, the Adult / Youth tip on list rows. */
/* components.css V3.61 - 2026-09-30, Nolan. Walk v42: `.h-sheet--foot` — a sheet with a button bar scrolls in an inner box and the bars stick at `bottom: 0` (no negative offsets; iOS left them floating). */
/* components.css V3.60 - 2026-09-30, Nolan. PY-10 v42: pinned bars (sheet footer, roster picker, roster Add) wear the bottom nav's glass while more is below; solid, no shadow, at the floor. */
/* components.css V3.59 - 2026-09-30, Nolan. Walk v41: the roster picker bar and the roster Add wizard's nav are pinned with the sheet glass, solid at the floor; `.h-search__me`; the shared `.is-attn` ring (moved from hub.css); RE-01's `.h-rfound`. */
/* components.css V3.58 - 2026-09-30, Nolan. `.h-skill__range` spacing. */
/* components.css V3.57 - 2026-09-30, Nolan. LR-03: `.h-skill*`, the shared skill control. */
/* components.css V3.56 - 2026-09-30, Nolan. `.h-range` declares its own `--fill` default (tokens test). */
/* components.css V3.55 - 2026-09-30, Nolan. DC-01: the still ice card is `[data-still]`. */
/* components.css V3.54 - 2026-09-30, Nolan. WL-01 purple accent line on waitlist sheets; DC-01 still ice cards with an arena link. */
/* components.css V3.53 - 2026-09-30, Nolan. An unset `.h-range` slider is dimmed green with a ringed thumb, never a grey dot. */
/* components.css V3.52 - 2026-09-30, Nolan. `.h-sheet--pin`: the long edit form's pinned footer (flush, heavy glass, solid at the floor). */
/* components.css V3.51 - 2026-09-30, Nolan. `.h-range`: the app's own slider (green fill to the value, green thumb, grey while unset); the round panel back. */
/* components.css V3.50 - 2026-09-29, Nolan. Comment correction (seen on a MacBook, not iPhone). */
/* components.css V3.49 - 2026-09-29, Nolan. Sheet actions back at the end of the sheet (the sticky bar floated and covered short wizards). */
/* components.css V3.48 - 2026-09-29, Nolan. The styled drop list (`.h-drop*`, `H365UI.dropField`); sheet action bars stick to the bottom (Done always in sight) unless `.h-sheet--end`. */
/* components.css V3.47 - 2026-09-29, Nolan. Chip squish on Discover's City card: the text column takes the room and the where-it-goes pill sits under it (`.h-listcard__ext`). */
/* components.css V3.46 - 2026-09-29, Nolan. Rules for `.h-rstep`, `.h-fbtn__knobs`, `.tb-free.is-used`, `.tb-chip.is-goalie`. */
/* components.css V3.45 - 2026-09-29, Nolan. The team sheet paper is real size (scaled to fit a phone preview, never in print); `.tb-notyet`. */
/* components.css V3.44 - 2026-09-29, Nolan. SS-04 guest lines; RR-01/02/04 roster sort, purple waitlist +, sticky picker bar; RO-09 step bar; GM-07 `.h-app` overflow clip; the team builder as a full page with the print sheet (TM-01/02/04/07/08, B26). */
/* components.css V3.43 - 2026-09-29, Nolan. Glass nav: `.h-under-nav` reserve on whichever box scrolls under it (Centre Ice, Discover, Locker Room, Schedule); glass 10% darker (Karl). */
/* components.css V3.42 - 2026-09-29, Nolan. Glass nav fallbacks: the Find ring goes solid too (matched the glass rule specificity). */
/* components.css V3.41 - 2026-09-29, Nolan. THE GLASS BOTTOM NAV (Sloane's handoff): floating, glass, solid at the floor, over every Space, hidden on a Space's Chat tab; content reserves `--bnav-h`. */
/* components.css V3.40 - 2026-09-29, Nolan. GB-02: `.gs-jersey`, `.tb-jersey` and `.tb-swatch__art` show the painted jersey files (one per colour). */
/* components.css V3.39 - 2026-09-28, Nolan. The game roster rebuild (`.gs-rhead`, rings, `.gs-rrow`, the ••• card, the picker) and the team builder (`.tb-*`). */
/* components.css V3.37 - 2026-09-28, Nolan. `--gs-tabs` is declared on `.gs-tabs` (tokens lint); GB-02 drawn jersey. */
/* components.css V3.36 - 2026-09-28, Nolan. SS-03/04 guest DOB + Use me; FS-01/RO-01: the standard search row (`.h-searchrow`, `.h-search`), Filters (`.h-fbtn`) and Sort (`.h-sortbtn`). */
/* components.css V3.35 - 2026-09-28, Nolan. Can't make it underlined with an X; `.gs-exempt` under the answer (TB-03). */
/* components.css V3.34 - 2026-09-28, Nolan. `.gs-tlink--leave` / `--exempt` named for the act, not a failure state. */
/* components.css V3.33 - 2026-09-28, Nolan. TB-03: `.gs-attwrap` / check-in pill, `.gs-strip__links` + `.gs-tlink` small text buttons. */
/* components.css V3.32 - 2026-09-28, Nolan. `.gs-apply--wait` / `.h-reg__wait`: joining a waitlist is purple, registering stays green (Karl). */
/* components.css V3.31 - 2026-09-28, Nolan. `.h-ic__spark`: the Quick Discover compass's sparkle twinkles softly (SF-05); still under reduced motion. */
/* components.css V3.30 - 2026-09-28, Nolan. `.h-datef`: the readable face every date box wears (ui.js V1.20). */
/* components.css V3.29 - 2026-09-28, Nolan. `.gs-manage` retired: MM-01 moved Manage to the top bar (`.h-mgbtn`, hub.css). */
/* components.css V3.28 - 2026-09-26, Rowan. Rules for the visitor/RSVP callouts and the waitlist card. */
/* components.css V3.27 - 2026-09-26, Rowan. `.gs-*`: the Game Space body from the approved preview. */
/* components.css V3.26 - 2026-09-26, Rowan. `--hero` has a declared default. */
/* components.css V3.25 - 2026-09-26, Rowan. Rules for `.h-reg`, `.h-reg__pr`, `.h-ehero__parent`, `.h-ehero__pav`, `.h-mdrawer`. */
/* components.css V3.24 - 2026-09-26, Rowan. `.h-reg__*`: the contextual registration sheet. */
/* components.css V3.23 - 2026-09-26, Rowan. `.h-sphoto`: the preset hero photo frame. */
/* components.css V3.22 - 2026-09-26, Rowan. Stage 1 shared Space head: the
   circular arrow-only Back, the kind kicker over the title, the wide-screen
   column for `.h-detail`, and the manager drawer's "for" line. */
/* components.css V3.21 - 2026-09-25. Active filter controls now use the shared
   filled green glow instead of a text-only colour change and outline. */
/* components.css V3.20 - 2026-09-25. Purple filter states keep their colour
   in the edge/glow while inheriting accessible interface ink. */
/* components.css V3.19 - 2026-09-25. Filter glows retain opaque card surfaces
   so their text remains accessible in the dormant post-launch light palette. */
/* components.css V3.18 - 2026-09-25. Shared filter-tone modifiers use the
   approved dark tinted glow: first layer green, second layer purple. */
/* components.css V3.17 - 2026-09-25. Panels can slide from the right for the
   global Menu while retaining the shared panel focus and dismissal behavior. */
/* components.css V3.16 - 2026-09-25. Shared full-screen Back control: labelled on
   roomy viewports, icon-only at 430px and below so page titles keep their own
   protected centre column. The accessible label remains on the button. */
/* components.css V3.15 - 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." A tapped text field matches `:focus-visible`
   in every engine (measured), so the pointer exemption SYS-08 wrote could never
   fire on a text field: every tap drew base.css's detached two-tone ring. Text
   fields now wear ONE indicator, their own rounded border in green with a soft
   green glow that follows its corners. Reasoning at `.h-input:focus`. */
/* components.css V3.14 - 2026-09-22, Marlow. `S2-14` - `.h-chatin input` was
   13px, under the `SYS-07` 16px iOS zoom floor. Rendered only in `kit.html`
   today, which is the reason to fix it now rather than the reason not to: it is
   a rule waiting for the screen that uses it, and the screen will not bring a
   keyboard test with it. Size only; nothing else in the rule moves.
   See `tests/zoom-floor.test.mjs`, which banks the floor `SYS-07` measured and
   never recorded. */
/* components.css V3.13 */
/* V3.13 - 2026-09-19, Jackie. `SYS-29` - 20 raw corners -> `var(--r-*)`.
   Of them: 4 pixel-identical, 16 moved 1px.
   The map in `tools/css-radius.mjs` is now EMPTY, so a new raw corner here is
   no longer a number to raise - it is an unrecorded file. Mapped by that
   tool's own SCALE/NEAREST tables, never by eye; measured before and after on
   every visible element of all 26 pages. */
/* V3.12 - 2026-09-19, Jackie. `S2-03`. The filter bar's icon chip was
   `--green-dim` / `--green` unconditionally, so the control looked lit whether
   or not anything was filtered - and a control that is always lit cannot signal
   that it is doing something. The resting state steps back to the neutral
   surface and the live colours move to `.is-on`. Karl: *"it should appear
   always but look more active when a filter is activated."*
   ⛔ The two resting declarations are EDITED IN PLACE rather than overridden
   underneath: my first cut added a second `.h-filterbar__ic svg` rule and
   `css-dupes` refused it in one line. */
/* components.css V3.11 */
/* V3.11 - 2026-09-18, Jackie. `S2-08`. ONE NEW RULE, `.h-listcard__src`: the
   line that says a Discover result belongs to the City of Ottawa and not to us.
   It sits in the listcard's MIDDLE column, not in `.h-listcard__right`, and the
   rule above it is the reason -- `.h-listcard__right` is `flex: none`, so
   anything added there sets a floor under the card's width at every viewport.
   No existing declaration changed. */
/* V3.10 - 2026-09-22, Vera. `SYS-51`, the one real defect among fourteen
   duplicate declarations. `.h-rail__item:hover` had a second body in `hub.css`
   — an orphan from the 08-17z migration, sitting under hub.css's own note
   announcing that move — and the two carried DIFFERENT values with the SAME
   4.08:1 measurement comment copied into both files.
   🔴 AND THE ROW'S PREMISE WAS BACKWARDS. Measured before touching anything:
   the body that never won is the one that FAILS — 2.62:1 in light, because
   `--chrome-rail-hover` is declared in the dark block only and paints near-black
   on a light rail. The duplicate was the only thing keeping the row readable.
   ✅ One body here, carrying the pair that measures 14.95:1 dark / 17.37:1
   light. The stale comment is deleted rather than moved. */
/* V3.9 - 2026-09-21, Vera. `SET-01` reopened, defect 3 of 3. ONE LINE: the
   openice card accent goes from `2px solid var(--action)` to the same
   `1px solid var(--line)` its other three sides carry. It was drawing on all
   eleven Settings cards at once AND landing 0px under the heading's own rule,
   so the screen's only boundary marker read as stripes. Reasoning at the rule.
   ⛔ NOTHING ELSE IN THIS FILE IS TOUCHED — Meryl owns it and this is the line
   her own comment assigns to me. */
/* V3.8 - 2026-09-19, Meryl. `SYS-39`: the sheet's close control is a GLYPH IN
   THE CORNER instead of a BOX IN THE ROW. Karl: "a bit more simplistic X that
   sits tighter in the top right corner, rather than floating in the same row as
   the table." It was a filled, bordered, rounded 30x30 box sitting as a flex
   item in a `space-between` head, so it shared a baseline with the title and
   read as a control belonging to the card below. Now absolutely positioned
   against `.h-sheet` (which was already `position: absolute` -- no new
   positioning context), with no fill, no border and no radius. `.h-sheet__head`
   gains `padding-right` because the glyph now overlaps its right edge and a
   long title would otherwise run underneath it. THE COLOUR IS DELIBERATELY
   UNCHANGED: `contrast` is a browser ratchet this seat cannot run, and dimming
   a control I cannot measure is how a polish row becomes an accessibility row. */
/* V3.7 - 2026-09-17, Meryl. `SPC-08`: `.h-grp--prose` RENAMED `.h-grp--pad`.
   It was coined a day ago for a list and two sentences in Settings; Edit Player
   needs the same box round a run of FORM FIELDS, and a class called `--prose`
   wrapping a form is a name that lies to the next reader. Named for the
   mechanism now: the box pads its own content because no row inside it does. */
/* V3.6 - 2026-09-17, Meryl. `SET-01`: `.h-grp--pad`, the inset for a section whose content is not rows.
   `.h-grp` carries no padding -- every row type buys its own through a named
   rule (Vera's finding 3) -- and a prose box has no row to carry it. A
   modifier rather than `.h-grp .h-lr__hint`, which would pad any hint that
   ever lands in any group including the row cards prose is kept out of. */
/* V3.5 - 2026-09-17, Meryl. `Q-02` corrected on Karl's walk. `.h-sheet__back`
   and `.h-panel__back` are GONE -- there is no automatic slot in either shared
   head. `.h-back` is now GUIDES' body verbatim (green, --fs-xs, no chevron),
   promoted rather than invented, because the control Karl wants already
   existed. The `.h-detail__back` and `.h-detail__act` merges STAY: those were
   real duplicate bodies and merging them was right either way. */
/* V3.4 - 2026-09-17, Meryl. `Q-02` sweep: `.h-back` and `.h-back--icon`, ONE body for what was six.
   `.h-mback`, `.h-sa__back` and `.h-ch__back` are retired into it and
   `.h-back` moved out of content.css. The shape kept is `.h-sa__back`'s,
   because it was already the right one. Also `.h-detail__act` merged on the
   RENDERED reading -- hub.css's container body kept, this file's old
   button-shaped body dropped; the header at the rule lists both. */
/* V3.4 - 2026-09-15, Vera. `SYS-04` card paint READ. `.h-grp` now reads
   `--card-edge` and `--card-tex`; the two tokens have existed in `tokens.css`
   since V1.8 and nothing consumed them, so street's green edge and purple dot
   texture and pond's rule edge were defined and invisible. Three declarations,
   all paint: an INSET shadow (not a border — a border grows the box), a
   background-image placed after the `background:` shorthand that would reset
   it, and the 9px tile the dot pattern needs. openice and gm set neither token,
   so they render unchanged. */
/* V3.3 - 2026-09-16, Meryl. `Q-02` build: `.h-sheet__back` and `.h-panel__back`
   place the SHARED back control in the two shared overlay heads. They say only
   WHERE it sits -- the look is `.h-detail__back`, merged in V3.2, which is why
   that merge had to land first. The two titles take the free space so a head
   WITHOUT the slot lays out exactly as it does today; measured in a browser
   both ways, nothing moved on the 77 overlays that already exist. */
/* V3.2 - 2026-09-16, Meryl. `Q-02` prerequisite: `.h-detail__back` is now
   defined ONLY here. It was in this file and in hub.css; the boxes agreed but
   the 180-degree chevron rotation lived only in hub.css, so a Back button on
   the seven pages that load this file without hub.css pointed RIGHT. Latent
   until Q-02 puts the class in the shared overlay head. Same move the 08-17z
   sweep made for `.h-detail__head`, `__title` and `__body`, which walked past
   this block. NO SCREEN CHANGES: both files load together in the app. */
/* V3.1 - 2026-09-16, Meryl. `SYS-17` shared segmented FIELD. `.h-segf`,
   `.h-segf__l`, `.h-segf__s` and `.h-segf--sel` are the portable form of
   `programs.css`'s `.pg-fld--seg` / `.pg-seg`, promoted because seventeen more
   files need the control and a per-file copy of it is the trap `.h-seg`'s own
   warning already records twice. Layout only: no colour, no state, no
   transition, no token, no new radius. */
/* V3.0 - 2026-09-16, Meryl. `SYS-02` width half, third report, off Karl's phone
   screenshot. The value pseudo-element takes the width; `-webkit-appearance:
   none` lands SCOPED to iOS Safari via `@supports (-webkit-touch-callout)`, so
   desktop Chrome keeps its picker indicator; and `type="number"` joins the rule
   for the Skaters counter, width only so its centring survives. */
/* V2.9 - 2026-09-15, Cass. `LKR-10`: ONE colour control for three builders.
   `.h-pick` / `.h-pick__row` / `.h-pick__sw` / `.h-pick__wheel` / `.h-pick__in`
   are the group builder's own dot-row and hue wheel, promoted to a shared name
   because the row says fix once, both places. `.h-gid__sw`, `.h-gid__dot` and
   `.h-gid__wheel` are REMOVED, not left beside them, so there is one gradient
   and not two - raw-colour count unchanged at 7. Structure and plumbing only:
   the wheel's gradient is copied verbatim from Karl's rule, no token, no
   spacing scale, no colour decision, so it stays this side of `SYS-14`'s hold.
   Vera's V2.8 `FND-02` line is untouched. */
/* V2.8 - 2026-09-07, Vera. `FND-02` step two: `.h-btn--purple` fills with
   `--purple-solid` instead of `--purple`. One word, one line. The ink token
   `--on-purple` is unchanged and still correct - it pairs with the fill. */
/* V2.7 - 2026-09-06, Meryl. The rest of Vera's ratified pattern, landed with
   the Recruitment rebuild that is its first caller: `.h-grp--pad`,
   `.h-grp__foot`, `.h-input--flush`, `.h-grp__note`, and the inset ON bar.
   Plus `.h-grp .h-mrow` corrected from 14px to the mock's 11px - my number,
   my correction. */
/* V2.6 - 2026-09-06, Meryl. Vera's findings 1, 2 and 3 in the `.h-grp` I landed
   at V2.4, plus the WebKit half of `SYS-02` and a closing marker on the V2.5
   entry below, which was swallowing V2.4 (`SYS-28`). Reasoning at each site. */
/* V2.5 - 2026-09-06, Cass. `SYS-08` — the unwanted grey ring on inputs.
   `.h-input:focus` removed the outline but could not remove `:focus-visible`'s
   box-shadow, so every input wore HALF the two-tone ring, on pointer focus as
   well as keyboard. Pointer focus now shows the green border only; KEYBOARD
   focus keeps the full ring and the 2026-09-04 accessibility measurement. */
/* V2.4 - 2026-09-06, Meryl. `SYS-26`(b) - one container per list. `.h-grp`,
   lifted from Vera's ratified mock 1 exactly as she wrote it. The wrapper is
   inserted by `H365UI.group()` over the assembled string - one transform, not
   forty hand-placed tags - and only the STRUCTURE is here: her ON-row accent,
   label type step and switch geometry are scoped to [data-style="openice"] and
   are values, which are hers. Reasoning at the rule and in ui.js. */
/* V2.3 - 2026-09-06, Meryl. Karl re-walked the seven screens; two items came
   back and both land here.
   ① `SYS-02` REOPENED - date and time fields run past the sheet edge, his
      second report. `min-width: 0; max-width: 100%; text-align: left` on
      date/time/datetime-local/month/week. Reasoning AND its verification limit
      are at the rule: this is a WebKit intrinsic-width behaviour and my harness
      is Chromium, so it is targeted rather than reproduced. Needs his walk.
   ② `SYS-26`(b) - the "odd grey line above each button" on Manager Tools:
      `border-radius: 12px` beside a lone `border-top` made the divider CURL
      at both ends. Not two borders and not a box per row. One declaration.
   ⛔ The grouping half of `SYS-26` is NOT here - section gaps, one container
   per list, and Recruitment all wait on Vera's Settings mock to define the
   pattern, per the Orchestrator's sequencing. */
/* V2.2 - 2026-09-06, Meryl. STAMPING THE SEAM, NOT THE AUTHORSHIP: two
   changes landed here after V2.1 and only the second is mine.
   ① CASS, `MORE-05` — `.h-panel`, the Partners full-screen presentation:
      absolute, full width, sliding in from the LEFT where `.h-sheet` rises,
      with its own head/body/foot. Karl: "I can pick and choose which screens
      convert over to full screen and which stay a bottom sheet."
   ② MERYL — the raw `rgba(0, 0, 0, 0.28)` drop shadow on `.h-panel` removed.
      `css-raw-colour --check` found it; the reason to act is that a hard black
      shadow does not flip and was invisible in dark. Reasoning at the rule.
   ⚠️ THIS STAMP EXISTS BECAUSE TWO RATCHETS RAN FOR THE FIRST TIME IN WEEKS.
   `css-stamp --check` and `css-raw-colour --check` are chained into
   `npm run smoke` but `tools/smoke-all.mjs` never executed them — its parser
   dropped every entry carrying an argument. Five stamps went stale at once
   rather than one at a time because nothing was reporting. Fixed in `1dd637d`. */
/* V2.1 - 2026-09-05, Meryl. Bug Sweep 1 package A, `SYS-16`.
   Three row components stopped centring their own labels, one cause each:
   `.h-frow` never declared `text-align` and a <button> defaults to centre;
   `.h-mtool` was one class serving a TILE and a ROW and the 08-17z merge kept
   the tile, so the app has been wearing kit.html's layout - the row is now the
   default and the tile is a `.h-mtools >` override with byte-identical values;
   `.h-scard` had both faults plus no width, so the game card shrink-wrapped.
   And `.h-filterbar__t` is dropped from the `.h-btn, .h-chip` inline-flex
   group - that is `SCH-01`, the third component this rule has mis-centred.
   Guarded by tools/rowalign.mjs; all four reverted and seen to go red. */
/* V2.0 — 2026-09-04, Vera. V1.9 -> V2.0 because .9 is the end of the minor in
   this tree (see stats.css, widgets.css), not because anything here is a
   rewrite. Four groups of change, and all of it is additive or token-swapped:

   ① 43 raw colours -> tokens, 0 left (7 sanctioned: the hue wheel on the
      identity picker, which is every hue in every theme).
   ② THREE THEME-SCOPED SELECTORS DELETED — `.h-na__row`, `.h-cal__cell`,
      `.h-week__day`. Such a selector matches on any light ANCESTOR while the
      tokens beside it resolve from the NEAREST scope; nesting the two makes
      them disagree. Replaced by the --hairline and --cell-bg pairs.
   ③ THE STAFF TIER GLOW + label, inert until a page carries `data-tier`.
   ④ THE `data-style` COMPONENT HALVES for openice, gm, street and pond, plus
      the shared sheet line and the dock line — every rule gated behind the
      attribute, so a page with no style renders exactly as it did before.

   `.h-btn` now reads --action/--on-action, which default to --green/--on-green:
   "the primary action" and "the semantic accent" were one token and had to be
   able to disagree. */

/* ============================================================================
   HOCKEY365 COMPONENT KIT
   Ported 1:1 from docs/Hub_Prototype.html (v3.2). Every component reads theme
   tokens only — no literal colours below except where the brand mark demands it.
   Naming: .h-<component> / .h-<component>__<part> / .is-<state>
   ========================================================================== */

/* ---------------------------------------------------------------------------
   1. APP SHELL
   ------------------------------------------------------------------------ */
/* THE APP BAR AND THE BOTTOM NAV ARE FIXED CHROME. They do not scroll. Two
   lines make that true, and BOTH are required:

   1. `height: 100dvh`, not `min-height`. min-height sets a floor, not a
      ceiling — the shell was free to grow to the height of its content, so the
      DOCUMENT scrolled and the nav sat at the bottom of the content, hundreds
      of pixels below the fold. Measured before the fix on a 844px-tall phone:
      the shell was 2830px and the nav's top was at y=2794. That is exactly the
      "bottom nav only appears when I scroll to the bottom" report.

   2. `min-height: 0` on the scrolling child. A flex item defaults to
      `min-height: auto`, which means it REFUSES to shrink below its content.
      So `flex: 1` + `overflow-y: auto` does nothing on its own: the item grows
      to fit its content instead of scrolling inside a fixed box, and takes the
      shell with it. This is the single most common flexbox scroll bug and it
      is invisible until the page is taller than the screen.

   `overflow: hidden` on the shell does not save you — the shell itself is what
   grew. Do not "fix" a scroll problem here by adding position:fixed to the
   nav; that breaks the safe-area padding and the sheet/scrim stacking. */
.h-app {
  display: flex; flex-direction: column;
  height: 100dvh; max-height: 100dvh;
  background: var(--bg); color: var(--ink);
  position: relative; overflow: hidden;
  /* GM-07 (Karl): "it brought my screen half out of focus ... I could not even
     side scroll". A slide-in panel waits off to the right, and focusing a field
     in it let the phone SCROLL this hidden box sideways (a hidden box is still
     a scroll container) with no way back. `clip` cuts the overflow without
     being scrollable at all; `hidden` stays as the fallback. */
  overflow: clip;
}

/* ---------------------------------------------------------------------------
   OPEN ICE RUSH — the component half of the style. Karl's launch default.

   Everything here is scoped to `[data-style="openice"]` and nothing outside it
   changes. The token half lives in `tokens.css`.
   ------------------------------------------------------------------------ */

/* 🔻 THE ACTION'S EDGE IN LIGHT, AND THIS IS THE DEBT THE TOKEN FILE RECORDED.
   The action stays brand green in both modes on purpose — Open Ice Rush is
   about finding a game and the button that does it should not change colour
   when the lights go on. Its INK is fine either way (13.50:1). Its BOUNDARY is
   not: lime on a pale page measures 1.20:1 against --bg and 1.47 against
   --panel, so in light the button has almost no edge of its own.

   A border is the honest fix. The alternative was to dull the green until it
   separated, which is how a style loses the thing it was chosen for. */
[data-style="openice"][data-theme="light"] .h-btn:not(.h-btn--ghost) {
  border: 1.5px solid var(--ink);
}

/* 🔑 THE ACCENT LINE MARKS AN OVERLAY, NOT A SCREEN. Karl, 2026-09-04:
   *"i dont want the green line ... displayed on every screen, that should only
   appear on overlay / pop up windows"*, and then the rule that makes it more
   than decoration: **the line sits on the side OPPOSITE the edge the sheet came
   in from.** Slides up from the bottom, line on top.

   So it answers "where did this come from" as well as "this is an overlay" —
   it marks the leading edge, the boundary between the sheet and the page it is
   covering.

   TODAY THE APP HAS EXACTLY ONE SHEET and it slides up: `.h-sheet` is
   `bottom: 0` with `translateY(112%)`. The three other directions are written
   anyway, keyed off `data-from`, so the first side sheet somebody builds
   inherits the rule instead of rediscovering it. `data-from` absent means
   bottom, which is what every sheet in the tree is today.

   🔑 SCOPED TO `[data-style]` AND NOT TO ONE STYLE. The behaviour belongs to
   the app; the COLOUR belongs to the style, and `--action` is what carries it.
   So all four launch styles get the line in their own accent, and a page with
   no style attribute keeps the 1px `--line2` border it has today — which is
   why this ships with zero visual delta. */
[data-style] .h-sheet,
[data-style] .h-sheet[data-from="bottom"] {
  border-top: 4px solid var(--action);
}
[data-style] .h-sheet[data-from="top"] {
  border-top: 1px solid var(--line2); border-bottom: 4px solid var(--action);
}
[data-style] .h-sheet[data-from="left"] {
  border-top: 1px solid var(--line2); border-right: 4px solid var(--action);
}
[data-style] .h-sheet[data-from="right"] {
  border-top: 1px solid var(--line2); border-left: 4px solid var(--action);
}

/* 🔑 THE MESSENGER DOCK GETS THE LINE TOO, PAINTED FROM ITS OWN TOKEN.
   Orchestrator ruling, 2026-09-04 18:14, and it resolves two rulings that
   looked like they collided. Karl: every surface that slides in gets the line.
   The messenger: a deliberately separate identity so it reads as not-the-page.

   Both survive, because they are about different things. **The line marks a
   surface that SLIDES — that is behaviour. The dock's identity is COLOUR.** So
   the dock takes the same rule and paints it `--dock-accent` instead of
   `--action`: a dock with no line would read as an oversight, and a dock
   wearing the app's action colour would undo the separateness Karl asked for
   by name.

   It slides in from the RIGHT (`transform: translateX(100%)`), so by the same
   opposite-edge rule the line lands on its LEFT — where `.h-dock__panel`
   already carries a 1px border for exactly that seam. */
[data-style] .h-dock__panel {
  border-left: 4px solid var(--dock-accent);
}

/* The geometry: squarer than the app's default, and the heads run condensed
   uppercase. Radii come from the token block; this is the type. */
[data-style="openice"] .h-sheet__title,
[data-style="openice"] .h-ash__h1 {
  text-transform: uppercase; font-stretch: 82%; letter-spacing: .01em;
}


/* ---------------------------------------------------------------------------
   GENERAL MANAGER — front-office authority. The style is TYPE AND RULES, not
   colour: square corners and flat surfaces come from its token block, and what
   is left is the editorial half.

   NO FILLS AND NO SHADOWS IS THE WHOLE IDEA. A card stops being a floating
   object and becomes a ruled ENTRY in a list, the way a game programme or a
   scoresheet sets a roster. That is also why it costs nothing in contrast:
   everything sits on the page's own ground, so it inherits pairs that are
   already measured rather than introducing new ones.
   ------------------------------------------------------------------------ */
[data-style="gm"] .h-card {
  background: transparent; box-shadow: none;
  border-width: 0 0 1px; border-style: solid; border-color: var(--line);
  border-radius: 0; padding-left: 0; padding-right: 0;
}
[data-style="gm"] .h-ash__head {
  padding-bottom: 14px; margin-bottom: 16px; border-bottom: 2px solid var(--ink);
}
[data-style="gm"] .h-ash__h1,
[data-style="gm"] .h-sheet__title {
  text-transform: uppercase; font-stretch: 78%; font-weight: 800;
  letter-spacing: -.01em; line-height: 1.02;
}
/* The kicker is the dateline. Wide tracking is what makes a small line read as
   a masthead rather than as small print. */
[data-style="gm"] .h-ash__sub,
[data-style="gm"] .h-sheet__sub {
  text-transform: uppercase; letter-spacing: .2em; font-size: 10px; font-weight: 700;
}
[data-style="gm"] .h-btn { box-shadow: none; }

/* ---------------------------------------------------------------------------
   STREET HOCKEY — LOCKED DARK. Chunky outlines, hard offset shadows, no blur.
   The look is a printed comic panel: everything is a shape with an edge.
   Its palette is pinned in `tokens.css`, so nothing here needs a colour.
   ------------------------------------------------------------------------ */
[data-style="street"] .h-card {
  border: 3px solid var(--ink); box-shadow: 6px 6px 0 var(--green);
  background: var(--panel);
}
[data-style="street"] .h-btn {
  border: 2px solid var(--ink); box-shadow: 4px 4px 0 var(--purple);
}
[data-style="street"] .h-btn:active { transform: translate(2px, 2px); box-shadow: 2px 2px 0 var(--purple); }
[data-style="street"] .h-sheet {
  border: 3px solid var(--ink); border-top: 4px solid var(--action);
  box-shadow: 0 -8px 0 var(--purple);
}
[data-style="street"] .h-ash__h1,
[data-style="street"] .h-sheet__title {
  text-transform: uppercase; font-stretch: 88%; letter-spacing: .01em;
}

/* ---------------------------------------------------------------------------
   POND HOCKEY — LOCKED LIGHT. Hand-cut corners, dashed rules, painted-board
   cards with an offset edge. Every corner radius is deliberately UNEVEN: the
   point is that nothing looks machined.

   The alternating card geometry is the detail that sells it — two cards in a
   row with the same corners read as a template, and the whole style is trying
   not to.
   ------------------------------------------------------------------------ */
[data-style="pond"] .h-card {
  border: 2px solid var(--line); border-radius: 18px 6px 16px 8px;
  box-shadow: 3px 4px 0 var(--line); background: var(--panel);
}
[data-style="pond"] .h-card:nth-of-type(even) { border-radius: 6px 18px 8px 16px; }
[data-style="pond"] .h-btn {
  border-radius: var(--r-pill); border: 2px solid var(--ink);
  box-shadow: 3px 3px 0 var(--line);
}
[data-style="pond"] .h-sheet {
  border: 2px solid var(--line); border-top: 4px solid var(--action);
  border-radius: 26px 26px 0 0;
}
/* Dashed, because a dashed rule reads as drawn rather than printed. */
[data-style="pond"] .h-ash__head { border-bottom: 2px dashed var(--line); padding-bottom: 16px; }

/* ---------------------------------------------------------------------------
   THE STAFF TIER GLOW. Karl, 2026-09-04: an identifier that is "super obvious
   even if you were scrolled".

   🔑 IT NEEDS NO `position: fixed`, AND THE APP'S EXISTING SHAPE IS WHY.
   `.h-app` is already `position:relative; height:100dvh; overflow:hidden`, and
   the INNER `.h-app__screen` is what actually scrolls. So an absolutely-
   positioned pseudo-element on the shell is pinned to the viewport by
   construction: it cannot scroll away, it carries no transformed-ancestor risk
   the way `fixed` would, and it cannot spill because the shell already clips.

   ⛔ INERT UNTIL SOMETHING STAMPS THE TIER. Nothing in this file can know who
   is looking; `data-tier="staff|manager|founder"` on the shell root is Tony's
   (Orchestrator ruling, 2026-09-04) and `hub.js` already reads the tier off the
   boot payload. Until it lands, no element matches and nothing renders.

   ⚠️ MEMBER SCREENS MUST NEVER MATCH. The selector is attribute-only on
   purpose: no `data-tier`, no glow.
   ------------------------------------------------------------------------ */
.h-app[data-tier]::after {
  content: ""; position: absolute; inset: 0;
  pointer-events: none;          /* it must never eat a click */
  /* 🔑 ABOVE THE SHEET LAYER, BELOW THE TOAST, AND BOTH HALVES ARE DELIBERATE.
     The stack is scrim 50, sheet 51, toast 60. A pop-up is hosted INSIDE the
     shell (`tests/tier-glow.test.mjs`: "A POP-UP INHERITS ITS SCREEN'S LEVEL"),
     so at 52 the frame draws OVER an open sheet — which is the point: the level
     must not disappear at the moment somebody is confirming something on a
     founder screen. But it stays UNDER the toast, because a transient message
     is the one thing that should never have a stripe drawn across it. */
  z-index: 52;
  border-radius: inherit;
}
.h-app[data-tier="staff"]::after {
  box-shadow: inset 0 0 0 1px rgba(var(--tier-staff-rgb), .30),
              inset 0 0 26px 4px rgba(var(--tier-staff-rgb), .13);
}
.h-app[data-tier="manager"]::after {
  box-shadow: inset 0 0 0 2px rgba(var(--tier-manager-rgb), .45),
              inset 0 0 46px 8px rgba(var(--tier-manager-rgb), .20);
}
/* THE HAZARD EDGE IS THE FOUNDER TIER'S SECOND CHANNEL — a cue that survives
   colour blindness, and the reason founder is not a third hue. */
.h-app[data-tier="founder"]::after {
  box-shadow: inset 0 0 0 3px rgba(var(--tier-founder-rgb), .60),
              inset 0 0 62px 12px rgba(var(--tier-founder-rgb), .26);
  background:
    repeating-linear-gradient(135deg, rgba(var(--tier-founder-rgb), .55) 0 9px, transparent 9px 18px)
      top / 100% 5px no-repeat,
    repeating-linear-gradient(135deg, rgba(var(--tier-founder-rgb), .55) 0 9px, transparent 9px 18px)
      bottom / 100% 5px no-repeat;
}
/* THE LABEL. Not decoration: cyan and orchid separate under every common form
   of colour blindness, but at 13% behind a page of text that is a thinner cue
   than it looks. Pinned to the SHELL, not the scroller, so it rides with the
   glow. Tony emits it; these rules are waiting for it. */
.h-tierlab {
  position: absolute; z-index: 53; right: 9px; bottom: 9px; pointer-events: none;
  font-family: var(--font-head); font-size: 9.5px; font-weight: 800;
  letter-spacing: .09em; text-transform: uppercase;
  padding: 3px 9px; border-radius: var(--r-pill);
}
/* A SOLID FILL WITH A MEASURED INK, and the first draft was not: it painted
   the tier colour as TEXT on a 16% wash of itself, which `contrast.test.mjs`
   caught at 3.34:1 for staff against a 4.5 bar. 9.5px bold is small text.
   `--on-tier` is one ink per theme -- near-black in dark where both fills are
   bright, white in light where both are deep. */
[data-tier="staff"] .h-tierlab {
  background: rgb(var(--tier-staff-rgb)); color: var(--on-tier);
}
[data-tier="manager"] .h-tierlab {
  background: rgb(var(--tier-manager-rgb)); color: var(--on-tier);
}
[data-tier="founder"] .h-tierlab {
  background: rgb(var(--tier-founder-rgb)); color: var(--on-tier);
}

.h-app__screen {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  /* 🧊 GLASS NAV: the page scrolls UNDER the nav, so it reserves the nav's
     height (measured into `--bnav-h` by the shell, safe area included) plus
     16px, and the last item rests just above the nav at the floor. */
  padding: var(--sp-4) var(--sp-4) calc(var(--bnav-h) + 16px);
  position: relative; background: var(--bg);
}
/* Chrome must never be squeezed by a tall screen either. */
.h-appbar, .h-bnav { flex: 0 0 auto; }



/* ---------------------------------------------------------------------------
   2. APP BAR  (chrome — always dark)
   ------------------------------------------------------------------------ */
/* THE INK COMES FROM THE CHROME LOCK, and it never did until 08-18b.

   This bar's background is pinned dark in both themes — that is the whole point
   of `.h-chrome` — but the rule set no `color`, so any text in it that does not
   name its own colour INHERITED THE PAGE's. In light theme that is #0A0A0A:
   near-black text on near-black chrome, measured at 1.04:1 by the composited
   pass. Every child in the app happens to set its own colour today, which is
   the only reason nobody has seen it; the next one that does not would be
   invisible in light and fine in dark.

   `var(--ink)` inside `.h-chrome` resolves to `--chrome-ink` — which is the
   token the lock exists to supply. */
.h-appbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 11px 15px 10px;
  background: linear-gradient(180deg, var(--chrome-appbar-top), var(--panel));
  color: var(--ink);
  flex: none;
}
.h-appbar__brand { display: flex; align-items: center; justify-content: center; }
.h-wordmark { height: 15px; width: auto; color: var(--srcGreen); display: block; }
/* Member SVG foundation (2026-10-06, Klee F1): the wordmark's own <svg> carries only a viewBox, so in an auto-width
   box it collapsed to 0 px and the logo was blank on the pages that load it (Public Ice, the staff and admin pages).
   Height follows the box; the width comes from the drawing's own proportions. */
.h-wordmark svg { display: block; height: 100%; width: auto; }

.h-topicons { display: flex; gap: 12px; color: var(--muted); align-items: center; }
.h-topicon { position: relative; cursor: pointer; display: grid; place-items: center;
  background: none; border: 0; padding: 0; color: inherit; }
.h-topicon svg { width: 21px; height: 21px; }
.h-topicon__badge {
  position: absolute; top: -4px; right: -5px;
  background: var(--green); color: var(--on-green);
  font-size: 9px; font-weight: 800; border-radius: var(--r-sm);
  min-width: 15px; height: 15px; display: grid; place-items: center; padding: 0 3px;
}
/* `display: grid` above beats the browser's [hidden] rule, so a badge hidden
   by the attribute stayed on screen as an empty coloured dot. Every badge that
   is toggled at runtime rather than rendered conditionally needs this. */
.h-topicon__badge[hidden] { display: none; }
.h-avatar-btn {
  width: 34px; height: 34px; border-radius: 50%; overflow: hidden;
  cursor: pointer; flex: none; border: 1.5px solid var(--green); padding: 0; background: none;
}

/* ---------------------------------------------------------------------------
   3. BOTTOM NAV + RAISED FIND  (chrome — always dark)
   ------------------------------------------------------------------------ */
/* THE SAME OMISSION AS .h-appbar, IN ITS SIBLING — found the same way, one
   round later, by the composited pass reading a leftover bottom nav on the
   reviews harness: `rgb(10,10,10)` on `rgb(13,13,13)`, **1.02:1** in light.

   Both wear `.h-chrome`, which pins the surface dark in either theme; neither
   named an ink, so anything inside that does not set its own colour inherits
   the PAGE's. `.h-bnav__item` sets `--faint` and that is why nobody has seen
   it, exactly as `.h-appbar`'s children saved that one. Two instances is a
   class: a `.h-chrome` carrier has to name its ink. */
.h-app { --bnav-h: 64px; }   /* the shell measures the real height (safe area included) */
/* 🧊 THE GLASS BOTTOM NAV (Sloane's handoff, approved by Karl 2026-09-28).
   It floats over the bottom of `.h-app` — ABSOLUTE, inside the app, never
   `position: fixed` (the note at the top of this file: fixed broke safe-area
   padding and sheet stacking). Translucent and blurred so the page shows
   through; SOLID at the bottom of the page (`.is-solid`, set by the shell),
   which tells a member when there is nothing more below.
   STACKING: 42 — above the Hub's strip (20) and its expanded post (40), which
   keeps the nav reachable as before, and BELOW the detail panel (44), so every
   non-Space screen still covers it exactly as today. Over a Space it rises to
   45 (`.has-space`, below). Scrims (50) and sheets (51) stay above it. */
.h-bnav {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 42;
  display: flex; overflow: visible; flex: none; isolation: isolate;
  padding: 7px 4px calc(9px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--glass-nav-edge);
  background: linear-gradient(180deg, var(--glass-nav-sheen-top), var(--glass-nav-sheen-low) 60%), var(--glass-nav-tint);
  box-shadow: inset 0 1px 0 var(--glass-nav-hi);
  -webkit-backdrop-filter: blur(8px) saturate(1.8);
  backdrop-filter: blur(8px) saturate(1.8);
  color: var(--ink);
  transition: border-color .35s ease, box-shadow .35s ease;
}
/* The solid fill fades in over the glass; a layer, because a gradient
   background does not transition. */
.h-bnav::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: var(--bg); opacity: 0; transition: opacity .35s ease;
}
.h-bnav.is-solid { border-top-color: var(--line); box-shadow: none; }
.h-bnav.is-solid::before { opacity: 1; }
.h-bnav__item {
  flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
  align-items: center; gap: 3px; padding: 4px 2px; cursor: pointer;
  color: var(--glass-nav-ink); font-size: 9.5px; font-weight: 600; position: relative;
  text-shadow: 0 1px 2px var(--glass-nav-ink-shadow);
  background: none; border: 0;
  transition: transform var(--t-fast), color var(--t-fast);
}
.h-bnav__item svg { width: 21px; height: 21px; filter: drop-shadow(0 1px 2px var(--glass-nav-icon-shadow)); }
.h-bnav.is-solid .h-bnav__item { color: var(--faint); text-shadow: none; }
.h-bnav.is-solid .h-bnav__item svg { filter: none; }
.h-bnav__item.is-on { color: var(--green); }
.h-bnav__item.is-on svg { filter: drop-shadow(0 0 6px rgba(var(--green-rgb), .55)); }
.h-bnav__item:not(.h-bnav__item--center):active { transform: scale(.92); }

.h-bnav__item--center { justify-content: flex-end; }
.h-bnav__fab {
  position: relative; width: 50px; height: 50px; border-radius: 50%;
  background: var(--green); color: var(--on-green);
  display: grid; place-items: center;
  margin-top: -24px; margin-bottom: 2px;
  box-shadow: 0 6px 16px rgba(var(--green-rgb), .42);
  border: 4px solid var(--bg);
  transition: transform var(--t-fast), box-shadow var(--t-fast);
}
.h-bnav__fab svg { width: 24px; height: 24px; filter: none; }
.h-bnav__item--center:active .h-bnav__fab { transform: translateY(2px); }
.h-bnav__item--center.is-on .h-bnav__fab { box-shadow: 0 6px 20px rgba(var(--green-rgb), .6); }
/* FIND IS NEVER GLASS: always solid green. While the bar is glass its ring is
   see-through, so the page shows around the button instead of a dark disc. */
.h-bnav:not(.is-solid) .h-bnav__fab { border-color: transparent; background-clip: padding-box; }

/* 🧊 OVER A SPACE (Karl, 2026-09-28: Pickup Group, Program, Game and Session
   Spaces). The shell's `.has-space` lifts the nav over the Space panel; no tab
   is lit inside a Space; the Space's body reserves the nav's height; and a
   Space's Chat tab (`.space-chat`) hides the nav so the message box has the
   full width. */
/* Whatever box is scrolling under the nav (found by the shell's `glassNav`)
   reserves the nav's height, so its last item rests just above the nav. */
.h-under-nav { padding-bottom: calc(var(--bnav-h) + 16px) !important; }
.h-app.has-space > .h-bnav { z-index: 45; }
.h-app.has-space .h-bnav__item.is-on { color: var(--glass-nav-ink); }
.h-app.has-space .h-bnav.is-solid .h-bnav__item.is-on { color: var(--faint); }
.h-app.has-space .h-bnav__item.is-on svg { filter: drop-shadow(0 1px 2px var(--glass-nav-icon-shadow)); }
.h-app.has-space .h-bnav.is-solid .h-bnav__item.is-on svg { filter: none; }
.h-app.has-space .h-bnav__item--center.is-on .h-bnav__fab { box-shadow: 0 6px 16px rgba(var(--green-rgb), .42); }
.h-app.has-space .h-detail.is-open .h-detail__body { padding-bottom: calc(var(--bnav-h) + 16px); }
.h-app.has-space.space-chat > .h-bnav { display: none; }
.h-app.has-space.space-chat .h-detail.is-open .h-detail__body { padding-bottom: 30px; }

/* SAFETY NETS: Reduce Transparency, or no blur support, keeps the nav solid;
   reduced motion swaps instantly. */
@media (prefers-reduced-transparency: reduce) {
  .h-bnav { -webkit-backdrop-filter: none; backdrop-filter: none; border-top-color: var(--line); box-shadow: none; }
  .h-bnav::before { opacity: 1; }
  .h-bnav .h-bnav__item { color: var(--faint); text-shadow: none; }
  .h-bnav .h-bnav__item svg { filter: none; }
  .h-bnav .h-bnav__fab, .h-bnav:not(.is-solid) .h-bnav__fab { border-color: var(--bg); background-clip: border-box; }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .h-bnav { border-top-color: var(--line); box-shadow: none; }
  .h-bnav::before { opacity: 1; }
  .h-bnav .h-bnav__item { color: var(--faint); text-shadow: none; }
  .h-bnav .h-bnav__item svg { filter: none; }
  .h-bnav .h-bnav__fab, .h-bnav:not(.is-solid) .h-bnav__fab { border-color: var(--bg); background-clip: border-box; }
}
@media (prefers-reduced-motion: reduce) {
  .h-bnav, .h-bnav::before { transition: none; }
}
html[data-motion="off"] .h-bnav, html[data-motion="off"] .h-bnav::before { transition: none; }

/* ---------------------------------------------------------------------------
   4. DESKTOP SIDEBAR RAIL  (chrome — always dark)
   ------------------------------------------------------------------------ */
/* Same rule as the phone shell: the rail is fixed chrome, the columns scroll.
   height (not min-height) + min-height:0 on the scrolling descendant. */
.h-desktop { display: grid; grid-template-columns: 230px 1fr; height: 100dvh; overflow: hidden; background: var(--bg); }
.h-rail {
  overflow-y: auto; min-height: 0;
  background: var(--bg); border-right: 1px solid var(--chrome-rail-line);
  display: flex; flex-direction: column; padding: 16px 12px; color: var(--ink);
}
.h-rail__logo { margin: 6px 8px 22px; }
.h-rail__logo .h-wordmark { height: 18px; }
.h-rail__nav { display: flex; flex-direction: column; gap: 3px; flex: 1; }
.h-rail__item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 11px;
  border-radius: var(--r-md);
  color: var(--muted);
  font-weight: 600;
  cursor: pointer;
  font-size: 13.5px;
  background: none;
  border: 0;
  text-align: left;
  width: 100%;
  margin-bottom: 2px;
  font: inherit;
}
.h-rail__item svg { width: 19px; height: 19px; }
/* `SYS-51` — THE ONE DEFECT AMONG THE DUPLICATES, AND THE ROW'S PREMISE WAS
   BACKWARDS. IT IS WORTH THE PARAGRAPH.

   This rule read `background: var(--chrome-rail-hover)` and was overridden by a
   SECOND body of the same selector in `hub.css` — an orphan left behind when
   `.h-rail__item` moved here in 08-17z, sitting directly under hub.css's own
   comment announcing that move. So one of the two never drew a pixel, and the
   comment below (a 4.08:1 measurement) had been COPIED INTO BOTH FILES, which
   is how it read as justification for a value it was not measuring.

   🔴 THE ROW SAYS THE MEASURED FIX IS THE ONE BEING OVERRIDDEN. IT IS THE OTHER
   WAY ROUND, AND I MEASURED IT BEFORE TOUCHING ANYTHING — ink on the hover
   wash, both themes, hovering a real element:
     as shipped (the hub.css body wins)   dark 14.95:1   light 17.37:1
     orphan removed (this body wins)      dark  6.51:1   light  2.62:1  <- under AA
   ⚖️ SO THE DUPLICATE WAS THE ONLY THING KEEPING THIS ROW READABLE, AND
   "letting the measured fix win" WOULD HAVE SHIPPED A WCAG FAILURE IN LIGHT.
   🔑 WHY: `--chrome-rail-hover: #161616` is declared once, in the dark block,
   and is NEVER redeclared for light — so it paints a near-black wash on a light
   grey rail. The hub.css body also carried `color: var(--ink)`, which this one
   did not, so removing it dropped the ink to `--muted` on top of that.
   ✅ ONE BODY NOW, HERE, IN THE FILE THE MIGRATION NOTE SAYS OWNS THIS
   COMPONENT, carrying the pair that measures 14.95:1 / 17.37:1. The dead token
   is gone with it; it had exactly one consumer and this was it.
   ⛔ AND THE 4.08:1 COMMENT IS DELETED RATHER THAN MOVED. It measured a
   translucent wash that neither body has carried for weeks, and a stale
   measurement attached to a live number is worse than no comment at all — it is
   what made this look settled to four readers. */
.h-rail__item:hover { background: var(--panel2); color: var(--ink); }
.h-rail__item.is-on {
  background: linear-gradient(rgba(var(--green-rgb), .12), rgba(var(--green-rgb), .12)), var(--panel);
  color: var(--green);
}
.h-rail__item--find {
  background: var(--green); color: var(--on-green); font-weight: 800;
  box-shadow: 0 4px 14px rgba(var(--green-rgb), .28);
}
/* THE COUNT BESIDE A RAIL SECTION. `--muted`, not `--brand-black`, and finding
   that out is what merging the two definitions of this class was for.

   components.css painted a YELLOW pill — `background: var(--q)` — with
   `--brand-black` ink, which is right. hub.css then changed the background to
   `--panel2` and did NOT change the ink, so the app has been rendering
   near-black text on a dark grey pill: **1.16:1**, which is invisible. Nothing
   caught it because the effective pair was split across two files: each rule
   on its own looked fine, and the contrast check only ever saw one at a time.
   It became visible the moment the two rules became one. (08-17z) */
.h-rail__count {
  margin-left: auto;
  background: var(--panel2);
  color: var(--muted);
  font-size: 10px;
  font-weight: 800;
  border-radius: var(--r-pill);
  padding: 0 6px;
}
.h-rail__me {
  display: flex; align-items: center; gap: 10px;
  padding: 11px 8px 2px; border-top: 1px solid var(--chrome-rail-line); margin-top: 8px;
}
.h-desktop__main { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--bg); }
.h-desktop__top {
  display: flex; align-items: center; gap: 13px;
  padding: 15px 22px; border-bottom: 1px solid var(--line);
}
.h-desktop__search {
  margin-left: auto; display: flex; align-items: center; gap: 9px;
  background: var(--panel2); border: 1px solid var(--line2); border-radius: var(--r-pill);
  padding: 9px 15px; color: var(--muted); font-size: 12.5px; min-width: 220px;
}
.h-desktop__search svg { width: 15px; height: 15px; color: var(--green); }
.h-desktop__cols {
  display: grid; grid-template-columns: 1.7fr 1fr; gap: 18px;
  padding: 20px 22px; overflow-y: auto; flex: 1 1 auto; min-height: 0;
  align-content: start;
}
@media (max-width: 900px) { .h-desktop__cols { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------------------------
   5. BUTTONS
   ------------------------------------------------------------------------ */
.h-btn {
  background: var(--action); color: var(--on-action); border: none;
  border-radius: var(--r-md); padding: 12px 20px;
  font-weight: 800; font-size: 14px; cursor: pointer;
  box-shadow: var(--sh-accent);
  transition: transform var(--t-fast), filter var(--t-fast), box-shadow var(--t-fast);
}
.h-btn:hover  { filter: brightness(1.05); }
.h-btn:active { transform: translateY(1px); }
/* AN ICON AND ITS LABEL, LINED UP — and it has to live HERE, with the button.
   This rule was in hub.css until 08-17w, which only index.html and staff.html
   load, so every other page in the app — login, claim, sign-in link, email
   verify, the signup wizard, and kit.html itself — rendered a `.h-btn` with no
   centring and no gap between an icon and its text. `.h-btn--block` then had to
   be defined in both files, `display: block` here and `display: flex` there, so
   one class laid out two ways depending on the page.

   `.h-more__t` IS DELIBERATELY NOT IN THIS LIST, and that was the whole bug in
   the More sheet: this rule centres its contents, and a More row's title is a
   stacked heading with a left-aligned sub-line under it and no icon at all.
   Given `justify-content: center` every title floated in the middle above a
   left-aligned description. */
/* `.h-room__switch` IS NOT IN THIS LIST, and finding out why is what moving the
   rule was for. It declares its own `display: inline-flex; align-items: center;
   gap: 4px` in hub.css — a deliberate 4px — and because this rule used to sit
   BELOW it in the same file, the shared 7px silently won. Moving the rule up
   here would have flipped that back to the 4px its author wrote, which is a
   change nobody asked for in a round about consistency. It is dropped from the
   selector instead: the switch already lays itself out and never needed it. */
/* `.h-filterbar__t` IS DROPPED FROM THIS LIST TOO, 2026-09-05, AND IT IS THE
   THIRD REMOVAL FOR THE SAME REASON THE OTHER TWO ARE RECORDED ABOVE. It is
   `SCH-01` in Bug Sweep 1 — Karl: *"Filters is sitting centered in the button
   instead of left aligned"* — and he filed it as a one-off on one screen.
   It is this rule. `__t` is the label inside `.h-filterbar__c`, a flex COLUMN,
   so it stretches to the column's full width; `justify-content: center` then
   centres "Filters" inside that width. kit.html shows the same rule from the
   other side, with `.h-grow` doing the stretching instead.
   It is a text label. It has no icon to sit beside and never needed to be a
   flex container at all — which is exactly what was said about `.h-room__switch`
   and `.h-more__t`. A rule that has now mis-centred three different components
   is a rule about buttons and chips, not about every label they contain. */
.h-btn, .h-chip {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
}
.h-btn--block { width: 100%; display: flex; }
.h-btn--sm    { padding: 7px 15px; font-size: 12px; border-radius: var(--r-sm); }
.h-btn--lg    { padding: 13px; font-size: 14px; }
.h-btn--ghost {
  background: transparent; border: 1px solid var(--line2);
  color: var(--ink); box-shadow: none; font-weight: 700;
}
.h-btn--quiet { background: var(--panel2); border: 1px solid var(--line2); color: var(--ink); box-shadow: none; }
.h-btn--purple { background: var(--purple-solid); color: var(--on-purple); box-shadow: 0 3px 12px rgba(var(--purple-rgb), .28); }
/* THE FILL PAIR, NOT THE INK TOKEN. `background: var(--danger)` with white on
   top measured 2.78:1 in dark — every destructive button in the app. --danger
   is tuned to sit ON the page as ink; painting it as a FILL flips which side
   needs the contrast, which is the lesson --warn-solid already learned.
   Now 5.46:1, and identical in both themes. See tests/contrast.test.mjs. */
.h-btn--danger { background: var(--danger-solid); color: var(--on-danger); box-shadow: none; }
.h-btn:disabled { opacity: .45; cursor: not-allowed; transform: none; }

.h-pill {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--panel2); border: 1px solid var(--line); color: var(--muted);
  border-radius: var(--r-pill); padding: 5px 11px; font-size: 11.5px;
  cursor: pointer; font-weight: 600;
}
.h-pill svg { width: 14px; height: 14px; }

/* ---------------------------------------------------------------------------
   6. CARDS + PANELS
   ------------------------------------------------------------------------ */
.h-card {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: var(--sp-4); margin-bottom: 10px;
}
.h-card--tap { cursor: pointer; transition: transform var(--t-fast), border-color var(--t-fast); }
.h-card--tap:hover  { border-color: var(--green); }
.h-card--tap:active { transform: scale(.977); }
.h-card__title { font-weight: 600; font-size: 13px; }
.h-card__sub   { color: var(--faint); font-size: 11.5px; margin-top: 1px; }

/* Media row card (avatar/icon + text + right rail) */
.h-listcard {
  display: flex; align-items: center; gap: 11px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: 10px 11px; margin-bottom: var(--sp-3);
  cursor: pointer; transition: transform var(--t-fast), border-color var(--t-fast);
}
.h-listcard:active { transform: scale(.977); }
.h-listcard__ic { width: 40px; height: 40px; border-radius: var(--r-md); overflow: hidden; flex: none; display: grid; place-items: center; }
.h-listcard__right { margin-left: auto; text-align: right; flex: none; }
.h-listcard__price { font-weight: 800; color: var(--green); font-size: 13px; font-family: var(--font-head); }

/* 🏛 `S2-08`, KARL 2026-09-18: *"It is very important people understand this
   is a city of ottawa listing and not a hockey365 event."* WHOSE listing it is,
   on screen, under the title — Karl's own second suggestion: *"or put City of
   Ottawa below."* It had been in an `aria-label` only, so a screen-reader member
   was told and a sighted one was not.
   ⛔ DELIBERATELY NOT A `.h-tag` IN THE RIGHT RAIL. `.h-listcard__right` above
   is `flex: none`, so a second tag there sets a FLOOR under the card's width and
   the title column pays for it at 320px. This line lives in the shrinkable
   column instead.
   `--muted`, not `--faint`: this line is read, the same call `.h-qact__sub`
   makes twenty lines down and for the same contrast reason. */
/* ▾ The styled drop-down field (`H365UI.dropField`). Looks like a field;
   its list floats below it and pushes nothing. */
.h-drop { position: relative; }
.h-drop__btn {
  width: 100%; min-height: 48px; display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 11px 14px; border: 1px solid var(--line2); border-radius: var(--r-md); background: var(--panel2);
  color: var(--ink); font: inherit; font-size: 16px; text-align: left; cursor: pointer;
}
.h-drop__btn[aria-expanded="true"] { border-color: var(--green); }
.h-drop__v { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.h-drop__chev { flex: none; width: 9px; height: 9px; border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted); transform: rotate(45deg) translate(-2px, -2px); }
.h-drop__btn[aria-expanded="true"] .h-drop__chev { transform: rotate(-135deg) translate(-2px, -2px); border-color: var(--green); }
.h-drop__menu {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 20; max-height: 264px; overflow-y: auto;
  padding: 4px; border: 1px solid var(--line2); border-radius: var(--r-md); background: var(--elev);
}
.h-drop__opt {
  display: block; width: 100%; min-height: 44px; padding: 10px 12px; border: 0; border-radius: var(--r-sm);
  background: none; color: var(--ink); font: inherit; font-size: 15px; text-align: left; cursor: pointer;
}
.h-drop__opt:hover { background: var(--panel2); }
.h-drop__opt.is-on { color: var(--green); font-weight: 800; }
.h-listcard > div:not(.h-listcard__ic) { flex: 1 1 auto; min-width: 0; }   /* the text column takes the room (chip squish) */
.h-listcard__ext { margin-top: 8px; }   /* the external card's where-it-goes pill, under the text (chip squish fix) */
/* DC-01: a drop-in public-ice card is information, not a door. */
.h-listcard[data-still] { cursor: default; }
.h-listcard[data-still]:active { transform: none; }
.h-listcard__arena {   /* inside the reason line: its size, green and underlined */
  color: var(--action); text-decoration: underline; text-underline-offset: 2px;
}
.h-listcard__src { font-size: 11.5px; font-weight: 700; color: var(--muted); margin-top: 2px; }

/* Quick-action tiles */
.h-qacts { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.h-qact {
  background: var(--panel2); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 12px; cursor: pointer; display: flex; flex-direction: column; gap: 7px;
  text-align: left; transition: transform var(--t-fast), border-color var(--t-fast);
}
.h-qact:hover  { border-color: var(--green); }
.h-qact:active { transform: scale(.977); }
.h-qact__ic {
  width: 34px; height: 34px; border-radius: var(--r-sm);
  display: grid; place-items: center; background: var(--green-dim); color: var(--green);
}
.h-qact__ic svg { width: 19px; height: 19px; }
.h-qact--primary .h-qact__ic { background: var(--green); color: var(--on-green); }
.h-qact__name { font-weight: 700; font-size: 13.5px; }
.h-qact__sub  { color: var(--muted); font-size: 11.5px; }   /* was --faint: 4.4:1 in light. This line is read. */

/* Menu tiles */
.h-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.h-tile {
  background: var(--panel2); border: 1px solid var(--line); border-radius: var(--r-md);
  padding: 13px; cursor: pointer; position: relative; text-align: left;
  transition: transform var(--t-fast), border-color var(--t-fast);
}
.h-tile:active { transform: scale(.977); }
.h-tile__ic {
  width: 32px; height: 32px; border-radius: var(--r-sm); display: grid; place-items: center;
  background: var(--green-dim); color: var(--green); margin-bottom: 8px;
}
.h-tile__ic svg { width: 18px; height: 18px; }
.h-tile__name { font-weight: 700; font-size: 13px; }
.h-tile__sub  { color: var(--faint); font-size: 11px; }

/* ---------------------------------------------------------------------------
   7. NEEDS ATTENTION
   ------------------------------------------------------------------------ */
.h-na {
  background: linear-gradient(135deg, rgba(var(--purple-rgb), .16), rgba(var(--green-rgb), .05));
  border: 1px solid var(--line2); border-radius: var(--r-xl); padding: var(--sp-4); position: relative;
}
[data-theme="light"] .h-na { background: linear-gradient(135deg, rgba(var(--brand-purple-rgb),.12), rgba(var(--brand-purple-rgb),.03)); }
.h-na__head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: var(--sp-3);
  width: 100%;
  padding: 13px 14px;
  margin: 0;
  background: none;
  border: 0;
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  text-align: left;
}
.h-na__dot  { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px var(--green); }
.h-na__title{ font-size: 14px; font-family: var(--font-head); font-weight: 800; }
.h-na__count{ margin-left: auto; background: var(--brand-purple); color: var(--on-brand-purple); font-size: 11px; font-weight: 800; border-radius: var(--r-pill); padding: 1px 8px; }
.h-na__row  {
  display: flex; align-items: center; gap: 10px; padding: 8px 0;
  border-top: 1px solid var(--hairline); font-size: 13px;
}
/* The `[data-theme="light"] .h-na__row` override is gone — --hairline carries
   both values, and a token cannot disagree with the tokens beside it the way a
   theme-scoped SELECTOR can. */
/* The rule between rows is a SEPARATOR, so the first row has nothing to be
   separated from. (In the live app the first row sat under a panel header, so
   the stray line was invisible; the prototype's phone Home has no header.) */
.h-na__row:first-child { border-top: 0; padding-top: 2px; }
/* Severity chips. Kevin's rule: a colour placed on a tint made of ITSELF fails
   contrast in both themes, so the chip carries a tinted background and the ICON
   takes the solid colour — an icon is a non-text graphic (3:1), not body text,
   and at 16px on a 12% tint it clears comfortably. The URGENT pill beside it
   carries words, so it uses the --warn-solid / --on-warn pair instead. */
.h-na__ic   { width: 30px; height: 30px; border-radius: var(--r-sm); display: grid; place-items: center; flex: none; }
/* EACH SEVERITY SITS ON ITS OWN COLOUR. Both of these mixed their wash from
   the ACCENT — so a high-severity row painted danger-red ink on a green wash
   in dark and on a PURPLE one in light, which measured 4.40:1 at its very
   best and could not be rescued by moving the element. The tint comes from
   the status colour's own triple now: it reads as what it means, and it
   measures. (08-17w) */
.h-na__ic--high   { background: rgba(var(--danger-rgb), .12); color: var(--danger); }
.h-na__ic--medium { background: rgba(var(--warn-rgb), .10); color: var(--warn); }
.h-na__ic--low    { background: var(--panel2); color: var(--muted); }
.h-na__urgent {
  flex: none; font-size: 10px; font-weight: 800; letter-spacing: .3px;
  text-transform: uppercase; border-radius: var(--r-pill); padding: 2px 8px;
  background: var(--warn-solid); color: var(--on-warn);
}
.h-na__ic svg { width: 16px; height: 16px; }
.h-na__text { flex: 1; min-width: 0; }
.h-na__act  {
  margin-left: auto; font-size: 11px; font-weight: 800; border-radius: var(--r-sm);
  padding: 5px 11px; background: var(--green); color: var(--on-green);
  cursor: pointer; flex: none; border: 0;
  transition: transform var(--t-fast);
}
.h-na__act:active { transform: translateY(1px); }
.h-na__act--ghost { background: transparent; border: 1px solid var(--line2); color: var(--muted); }
.h-na__row.is-urgent {
  background: linear-gradient(90deg, rgba(var(--warn-wash-rgb),.14), transparent 70%);
  border-radius: var(--r-sm); padding-left: var(--sp-3); margin-left: calc(var(--sp-3) * -1);
  box-shadow: inset 3px 0 0 var(--warn);
}
/* Same treatment, same reason: this pill sits inside `.h-na__row.is-urgent`,
   which carries a wash of its own, so the two stacked and the light version
   measured 1.31:1 in a tinted row and 3.54 on a plain one. */
.h-urg-pill {
  background: linear-gradient(rgba(var(--warn-rgb), .2), rgba(var(--warn-rgb), .2)), var(--panel);
  color: var(--warn-ink-on-wash); font-size: 9px; font-weight: 800;
  letter-spacing: .03em; text-transform: uppercase; border-radius: var(--r-pill);
  padding: 2px 7px; flex: none;
}
/* The light half of this rule is a TOKEN now (`--warn-ink-on-wash`), not a
   `[data-theme="light"] …` selector — see the note in tokens.css. What is left
   here is only the wash strength, which light genuinely wants heavier. */
[data-theme="light"] .h-urg-pill {
  background: linear-gradient(rgba(var(--warn-rgb), .28), rgba(var(--warn-rgb), .28)), var(--panel);
}
.h-na__clear { display: flex; align-items: center; gap: 11px; padding: 5px 2px; }
.h-na__check {
  width: 30px; height: 30px; border-radius: 50%; background: var(--green); color: var(--on-green);
  display: grid; place-items: center; font-weight: 900; font-size: 15px; flex: none;
  box-shadow: 0 0 12px rgba(var(--green-rgb), .4);
}

/* ---------------------------------------------------------------------------
   8. SPACES STRIP
   ------------------------------------------------------------------------ */
.h-spaces { display: flex; gap: var(--sp-3); overflow-x: auto; padding-bottom: 3px; }
.h-space  { flex: none; width: 66px; text-align: center; cursor: pointer; position: relative; background: none; border: 0; padding: 0;
  transition: transform var(--t-fast); }
.h-space:active { transform: scale(.977); }
.h-space__disc {
  width: 54px; height: 54px; border-radius: 15px; display: grid; place-items: center;
  font-weight: 800; color: var(--on-colour); margin: 0 auto 4px; font-size: 14px;
  border: 1px solid var(--hairline-on-colour);
}
.h-space--program .h-space__disc { border: 1.5px solid var(--brand-purple); box-shadow: 0 0 0 3px rgba(var(--brand-purple-rgb),.16); }
.h-space__badge {
  position: absolute; top: -4px; right: 8px; background: var(--brand-purple); color: var(--on-brand-purple);
  font-size: 8px; font-weight: 800; letter-spacing: .3px; padding: 1px 5px;
  border-radius: var(--r-pill); border: 1.5px solid var(--panel); text-transform: uppercase;
}
/* The label wraps to at most two lines and the tile is tall enough to hold
   them. Before this a long space name ("Puck-Proof Goalie Academy") ran under
   the next section — the rail is horizontally scrolling, so nothing below it
   pushes down to make room. */
.h-space__label {
  font-size: 10px; color: var(--muted); line-height: 1.15; display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  min-height: calc(2 * 1.15em);
}

/* ---------------------------------------------------------------------------
   9. POSTS + ATTACHMENTS
   ------------------------------------------------------------------------ */
.h-post { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-2xl); padding: 12px 12px 10px; margin-bottom: 12px; }
.h-post__head {
  display: flex;
  align-items: center;
  gap: 10px;
}
.h-post__av {
  /* Centre Ice V11 (CI-2): the 40px header-row avatar. */
  width: 40px;
  height: 40px;
  padding: 0;
  border-radius: 50%;
  overflow: hidden;
  flex: none;
  position: relative;
  display: grid;
  place-items: center;
  cursor: pointer;
  background: var(--panel2);
  color: var(--ink);
  font-size: 14px;
  font-weight: 800;
  border: 0;
}
.h-post__who  { flex: 1; min-width: 0; font-weight: 600; font-size: 13px; }
.h-post__when {
  margin-left: auto;
  flex: none;
  color: var(--faint);
  font-size: 13px;
}   /* a timestamp is information */
.h-post__body {
  margin-top: 10px;
  font-size: 15px;
  color: var(--ink);
  opacity: .88;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.h-attach {
  margin-top: 10px;
  border: 1px solid var(--line2);
  border-radius: var(--r-md);
  padding: 9px 11px;
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--panel2);
}
.h-attach__ic { width: 32px; height: 32px; border-radius: var(--r-sm); display: grid; place-items: center; flex: none; background: var(--green-dim); color: var(--green); }
.h-attach__ic svg { width: 17px; height: 17px; }
.h-attach__name { font-weight: 600; font-size: 12.5px; }
.h-attach__sub  { color: var(--muted); font-size: 11px; }   /* "4 spots left" is the whole point of the row */

/* ---------------------------------------------------------------------------
   10. SEGMENTED CONTROLS
   ------------------------------------------------------------------------ */
.h-seg {
  display: flex; gap: 6px; background: var(--panel2); border: 1px solid var(--line2);
  border-radius: var(--r-md); padding: 4px; margin-bottom: 12px;
}
.h-seg button {
  flex: 1; background: transparent; border: none; color: var(--muted);
  border-radius: var(--r-sm); padding: 8px; font-size: 12.5px; font-weight: 700;
  cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px;
  /* ⚠️ `transform` ONLY. NO COLOUR TRANSITION HERE — READ BEFORE RESTORING THE
     SHORTHAND, WHICH READ
     `background var(--t-fast), color var(--t-fast), transform var(--t-fast)`.

     THIS IS THE ONE CONTROL IN THE BUILD THAT IS ON SCREEN WHILE THE THEME
     CHANGES. Every admin document hardcodes `data-theme="dark"` in its markup,
     so a light-theme boot paints dark and then flips — and any page that shows
     a segmented control at FIRST PAINT is mid-fade while it does.

     🎯 AND `--green` IS NOT THE SAME HUE IN THE TWO THEMES. Dark paints the
     active chip `#04120a` on `#07F708`; light paints it `#FFFFFF` on `#6B3AF5`,
     a purple fill. Both are deliberate and both are comfortable — 13.06:1 and
     5.92:1 at rest. But a cross-fade between them walks the ink and the fill
     through pairs that belong to NEITHER palette, and some of those pairs are
     unreadable. **No colour choice can fix that, because both endpoints are
     already correct.**

     📐 MEASURED TWICE, ON TWO DIFFERENT PAGES, BY TWO SEATS, TO THE SAME
     DIGIT: `tools/pages-contrast.mjs` reported `2.44:1 (needs 4.5)` on
     `button.h-seg__b` — ink `rgb(154,155,158)`, the DARK theme's `--muted`, on
     `rgb(240,240,240)`, the LIGHT theme's `--panel2`. At rest the same button
     measures 6.06:1. It held a deploy on `admin-invoices.html` and again on
     `content-admin.html`.

     🛑 BOTH COLOUR PROPERTIES GO, NEVER ONE. The first attempt at this dropped
     `color` and kept `background`: the ink snapped while the fill still
     crawled — white text on the OUTGOING theme's green — and the measured
     ratio went from 2.44:1 to **1.47:1**. De-synchronising a synchronised pair
     is worse than leaving it alone.

     📌 FIXED HERE RATHER THAN PER PAGE, AND THE TWO PER-PAGE COPIES THAT CAME
     FIRST ARE GONE. `admininvoices.css` and `admincontent.css` each carried
     this same one-line override, written months apart by seats who could not
     have known about each other. **A shared component's defect fixed on the
     pages that happened to expose it is a trap for the third page**, whose
     author has no reason to look. `transform` stays because it is the only one
     with an interaction behind it (`.h-seg button:active` nudges 1px). */
  transition: transform var(--t-fast);
}
.h-seg button svg { width: 15px; height: 15px; }
.h-seg button.is-on { background: var(--green); color: var(--on-green); }
.h-seg button:active { transform: translateY(1px); }

.h-seg--primary button.is-on {
  background: var(--panel2); color: var(--green);
  box-shadow: inset 0 0 0 1px rgba(var(--green-rgb), .36), 0 0 14px rgba(var(--green-rgb), .12);
}
.h-seg--secondary button.is-on {
  background: var(--panel2); color: var(--ink);
  box-shadow: inset 0 0 0 1px rgba(var(--purple-rgb), .42), 0 0 14px rgba(var(--purple-rgb), .14);
}

.h-seg--pill { border-radius: var(--r-pill); padding: 3px; flex: none; gap: 0; margin: 0; }
.h-seg--pill button { border-radius: var(--r-pill); padding: 6px 13px; font-size: 12px; }
.h-seg--pill button svg { width: 13px; height: 13px; }

/* `SYS-17` --- THE SEGMENTED FIELD. A LABEL AND A SCROLLER, AND NOTHING ELSE.

   ⛔ NO COLOUR, NO STATE, NO TRANSITION IS DECLARED HERE. `.h-seg` twenty
   lines up owns all three and the warning inside it was earned twice, on two
   pages, by two seats who each fixed this control where they happened to see
   it break. A third copy here would be the same trap for the next page.

   WHY THE SCROLLER EXISTS: `.h-seg button` is `flex: 1`, which is right for a
   two or three tab bar that should fill its row and wrong for a field whose
   option list the caller does not control. `.h-segf__s` releases that basis
   and scrolls instead, so six short chips sit on one line and never squash
   into unreadable slivers. `min-width: 0` is what lets the release mean
   anything at all --- a flex item defaults to `min-width: auto`, which
   resolves to min-content and quietly re-imposes the width it was given. */
.h-segf { display: flex; flex-direction: column; gap: 4px; }
.h-segf__l { font-size: 12px; font-weight: 600; color: var(--muted); }
.h-segf__s {
  flex: 1 1 auto; min-width: 0;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
}
.h-segf__s::-webkit-scrollbar { display: none; }
.h-segf__s .h-seg { margin: 0; width: max-content; min-width: 100%; }
.h-segf__s .h-seg button { flex: 0 0 auto; white-space: nowrap; }
/* A DISABLED FIELD HAS TO LOOK DISABLED. The hidden input cannot show it, so
   the chips carry it --- see the `off` branch in `ui.js`'s `segField`. */
.h-segf__s .h-seg button[disabled] { opacity: .45; cursor: not-allowed; }
/* THE FALLBACK IS A REAL FIELD, NOT A LESSER ONE. When the option list is too
   long for chips `segField` renders the `<select>` it was given, and it sits
   in the same label with the same spacing so a form does not visibly change
   shape at the threshold. */
.h-segf--sel .h-input { width: 100%; }

/* Role bar (purple selection) */
.h-rolebar { display: flex; gap: 7px; margin-bottom: 11px; }
.h-role {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--panel2); border: 1px solid var(--line); border-radius: var(--r-md);
  padding: 8px; font-size: 12px; font-weight: 700; cursor: pointer; color: var(--ink);
}
.h-role svg { width: 15px; height: 15px; }
.h-role.is-on { background: var(--brand-purple); border-color: var(--brand-purple); color: var(--on-brand-purple); }

/* Detail tabs */
.h-dtabs { display: flex; gap: 5px; margin-bottom: 13px; overflow-x: auto; padding-bottom: 2px; }
.h-dtab {
  flex: none; background: var(--panel2); border: 1px solid var(--line2); color: var(--muted);
  border-radius: var(--r-sm); padding: 7px 13px; font-size: 12px; font-weight: 700;
  cursor: pointer; white-space: nowrap;
}
.h-dtab.is-on { background: var(--green); color: var(--on-green); border-color: var(--green); }

/* ---------------------------------------------------------------------------
   11. SWITCH
   ------------------------------------------------------------------------ */
.h-switch {
  width: 44px; height: 26px; border-radius: var(--r-pill); background: var(--line2);
  position: relative; transition: background .16s; flex: none; border: 0; padding: 0; cursor: pointer;
}
.h-switch::after {
  content: ""; position: absolute; top: 2.5px; left: 2.5px; width: 21px; height: 21px;
  border-radius: 50%; background: var(--switch-knob); transition: transform .16s;
}
.h-switch.is-on { background: var(--green); }
.h-switch.is-on::after { transform: translateX(18px); }

/* ---------------------------------------------------------------------------
   12. FILTER BAR + BOTTOM SHEET
   ------------------------------------------------------------------------ */
.h-filterbar {
  display: flex; align-items: center; gap: 11px; background: var(--panel2);
  border: 1px solid var(--line2); border-radius: var(--r-md); padding: 10px 12px;
  margin-bottom: 12px; cursor: pointer; width: 100%; text-align: left;
  transition: transform var(--t-fast);
}
.h-filterbar:active { transform: scale(.977); }
/* 🔁 THE RESTING STATE STEPS BACK, `S2-03`. These two were `--green-dim` /
   `--green` unconditionally, so the bar looked lit whether or not anything
   was filtered — and a control that is always lit cannot signal that it is
   doing something. The live colours moved to `.is-on` below.
   ⛔ EDITED HERE RATHER THAN OVERRIDDEN UNDERNEATH: my first cut added a
   second `.h-filterbar__ic svg` rule and `css-dupes` refused it in one line
   — correctly, because two rules for one selector is how a colour ends up
   depending on which of them a later edit happens to find. */
.h-filterbar__ic { width: 33px; height: 33px; border-radius: var(--r-sm); background: var(--panel); border: 1px solid var(--line2); display: grid; place-items: center; flex: none; }
.h-filterbar__ic svg { width: 17px; height: 17px; color: var(--muted); }
/* The label and the count STACK. Left as bare siblings in a flex row they sit
   shoulder to shoulder and the bar reads as two unrelated captions. */
.h-filterbar__c { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.h-filterbar__t { font-weight: 700; font-size: 13.5px; }
.h-filterbar__s { font-size: 11px; color: var(--muted); margin-top: 1px; overflow-wrap: anywhere; }
.h-filterbar__dots { display: flex; gap: 3px; flex: none; }
.h-filterbar__dots i { width: 10px; height: 10px; border-radius: 3px; display: block; }

/* 🔑 `S2-03` — THE BAR IS ALWAYS THERE AND LOOKS DIFFERENT WHEN IT IS DOING
   SOMETHING. Karl: *“it should appear always but look more active when a filter
   is activated.”*

   ⚖️ THE GLYPH DOES NOT CHANGE, ONLY ITS WEIGHT. A control whose ICON changes
   with its state is a control a member has to learn twice; a control whose
   colour changes is one they read at a glance. So `is-on` lifts the chip to the
   live green this component already uses, and the resting state steps back to
   the neutral surface — which is what makes the lit one legible as lit.

   ⛔ AND THE COLOUR IS NOT THE ONLY SIGNAL. The dots beside it already count
   the active filters, and the summary line already names them in words, so a
   member who cannot tell these two greys apart still has two other answers.
   That is the same rule the week grid on the staff dashboard follows. */
.h-filterbar.is-on {
  background: var(--active-green-bg); border-color: var(--green);
  box-shadow: inset 0 0 0 1px rgba(var(--green-rgb), .24), 0 0 20px rgba(var(--green-rgb), .24);
}
.h-filterbar.is-on .h-filterbar__ic { background: var(--panel); border-color: var(--green); }
.h-filterbar.is-on .h-filterbar__ic svg { color: var(--active-green-ink); }
.h-filterbar.is-on .h-filterbar__t { color: var(--active-green-ink); }
/* 🔎 RO-01 (Karl, 2026-09-28) — THE STANDARD SEARCH ROW: the pill search (Find
   Training's), then the Filters button, then Sort when a list sorts. One set
   for every screen that searches and filters a list. */
.h-searchrow { display: flex; align-items: center; gap: 8px; margin: var(--sp-2) 0 6px; min-width: 0; }
/* GA-03 v41: the "my card" shortcut sits inside the search bar, on the right. */
.h-search__me {
  flex: none; width: 32px; height: 32px; margin-right: -6px; border-radius: 50%;
  display: grid; place-items: center; border: 1px solid var(--line2);
  background: var(--panel2); color: var(--green); cursor: pointer;
}
.h-search__me .h-ic { width: 16px; height: 16px; }
/* The field a failed Continue or Post is about (walk v41, Karl): an amber
   ring that fades, with focus moved to it. Shared by the wizards and sheets. */
.is-attn { outline: 2px solid var(--warn-solid); outline-offset: 3px; border-radius: var(--r-md); animation: h-attn 2.4s ease-out forwards; }
@keyframes h-attn { 0%, 70% { outline-color: var(--warn-solid); } 100% { outline-color: transparent; } }
@media (prefers-reduced-motion: reduce) { .is-attn { animation: none; } }
.h-search {
  display: flex; align-items: center; gap: 9px; flex: 1 1 auto; min-width: 0;
  padding: 0 12px; min-height: 46px; background: var(--panel);
  border: 1px solid var(--line2); border-radius: var(--r-pill);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.h-search:focus-within { border-color: var(--purple); box-shadow: 0 0 18px rgba(var(--purple-rgb), .18); }
.h-search .h-ic { width: 17px; height: 17px; color: var(--purple); flex: none; }
.h-search input {
  flex: 1; min-width: 0; padding: 0; border: 0; outline: 0; background: transparent;
  color: var(--ink); font: 16px var(--font-body);
}
.h-search input:focus { outline: 0; box-shadow: none; }
.h-sortwrap { position: relative; flex: none; }
.h-sortbtn { width: auto; min-height: 44px; padding: 6px 7px; margin-bottom: 0; }
.h-sortbtn .h-filterbar__ic { width: 30px; height: 30px; }
/* 🎛 FS-01 — THE STANDARD FILTER BUTTON (`H365UI.filterButton`): compact, to sit
   at the right of a search bar; the active filters' dots sit UNDER the word. */
.h-fbtn { width: auto; flex: none; min-height: 44px; padding: 6px 12px 6px 7px; gap: 8px; margin-bottom: 0; }
.h-fbtn .h-filterbar__ic { width: 30px; height: 30px; }
/* FS-01: the drawn Filters icon whose knobs wear the active filters' colours. */
.h-fbtn__knobs svg circle { transition: fill var(--t-fast), stroke var(--t-fast); }
.h-fbtn__c { display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 3px; min-width: 0; }
.h-fbtn .h-filterbar__t { line-height: 1.1; }
.h-fbtn__dots { gap: 3px; min-height: 7px; flex-wrap: wrap; max-width: 64px; }
.h-fbtn__dots i { width: 7px; height: 7px; border-radius: 50%; }
/* A filter group's heading in its sheet wears the same dot as the button. */
.h-sec-label--dot { display: flex; align-items: center; gap: 6px; }
.h-sec-label--dot .h-frow__dot { width: 9px; height: 9px; border-radius: 50%; }

/* STACKING ORDER, in one place so it stays coherent:
     20  the Hub's sticky section strip
     44  the detail panel (spaces / events / joining)
     50  the scrim
     51  a sheet — ALWAYS above whatever opened it, including the detail panel
     60  a toast — above everything, it reports on all of them
   The share sheet opened from a space used to render BEHIND the detail panel
   at z-index 41 and could not be clicked at all. */
.h-scrim {
  position: absolute; inset: 0; background: var(--scrim-modal); opacity: 0;
  pointer-events: none; transition: opacity .2s; z-index: 50;
}
.h-scrim.is-open { opacity: 1; pointer-events: auto; }

.h-sheet {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 51;
  background: var(--panel); border-top: 1px solid var(--line2);
  border-radius: var(--r-2xl) var(--r-2xl) 0 0;
  transform: translateY(112%); transition: transform var(--t-sheet);
  padding: 8px 16px calc(18px + env(safe-area-inset-bottom));
  max-height: 88%; overflow-y: auto;
}
.h-sheet.is-open { transform: translateY(0); }
.h-sheet__grip { width: 38px; height: 4px; border-radius: var(--r-pill); background: var(--line2); margin: 6px auto 12px; }
/* WL-01: a waitlist flow's sheet wears the waitlist purple at the top. */
.h-sheet--wait .h-sheet__grip { background: var(--purple); }
/* WL-01 second half (Karl, 2026-09-30): the ACCENT LINE on the sheet's
   leading edge goes purple too, so the handle and the line agree. */
[data-style] .h-sheet.h-sheet--wait { border-top-color: var(--purple); }
/* `SYS-39` — THE CLOSE CONTROL IS A GLYPH IN THE CORNER, NOT A BUTTON IN THE ROW.
   🗣️ KARL: *"a bit more simplistic X that sits tighter in the top right corner,
   rather than floating in the same row as the table."*
   🔎 WHAT HE IS LOOKING AT, MEASURED FROM THE RULE RATHER THAN THE PICTURE:
   it was `background: var(--panel2)` + `border: 1px solid var(--line2)` +
   `border-radius: var(--r-sm)` — a filled, bordered, 30x30 BOX — sitting as a
   flex item in `.h-sheet__head`, which is `space-between`. So it shared a
   baseline with the title and read as a control belonging to the card below.
   ✅ NOW: no fill, no border, no radius, and OUT of the head's flow entirely.
   `.h-sheet` is already `position: absolute`, so it is the containing block and
   this needs no new positioning context.
   ⚠️ THE HEAD KEEPS ITS `padding-right` FOR A REASON: the glyph now overlaps the
   head's right edge, so without it a long title runs underneath the X. That is
   the failure this shape actually has, and it only appears on a long title.
   ⛔ THE COLOUR IS DELIBERATELY UNCHANGED. `--ink` stays. Karl asked about the
   BOX, not the weight, and `contrast` is a browser ratchet I cannot run from
   here — dimming a control I cannot measure is how a polish row becomes an
   accessibility row. If it reads heavy on his walk, that is one token.
   📌 IT SCROLLS WITH THE SHEET, like the grip and the head above it, because
   `.h-sheet` is the scroll container. That is existing behaviour for everything
   in this head and is not what he reported. */
.h-sheet__head {
  display: flex; align-items: center; justify-content: space-between;
  padding-right: 30px;
}
.h-sheet__title { font-weight: 800; font-size: var(--fs-lg); font-family: var(--font-head); }
.h-sheet__x {
  position: absolute; top: 4px; right: 8px;
  background: none; border: 0; padding: 0;
  /* NR-14 (Partners Gate B): 36px, the house tap target (was 28px). */
  width: 36px; height: 36px; line-height: 1;
  color: var(--ink); font-size: 17px; cursor: pointer;
}
.h-sheet__sub { font-size: 12px; color: var(--muted); margin: 3px 0 6px; }
/* AR-01 (Karl's B57 walk): the approval sheet's facts, label left, value right. */
.h-req { margin: 6px 0 8px; display: grid; gap: 7px; }
.h-req__row { display: grid; grid-template-columns: 86px 1fr; gap: 10px; align-items: baseline; }
.h-req__row dt { font-size: 12px; color: var(--muted); }
.h-req__row dd { margin: 0; font-size: 14px; color: var(--ink); overflow-wrap: anywhere; }

/* ===========================================================================
   THE PANEL — THE SECOND PRESENTATION (MORE-05, Karl 2026-09-05)

   ⚖️ Karl keeps the bottom sheet and picks, per screen, which surfaces get
   this instead: "I can pick and choose which screens convert over to full
   screen and which stay a bottom sheet." Nothing here changes `.h-sheet`.

   📌 PORTED FROM LIVE rather than designed. `Overlays/Shared Overlay CSS.txt:32`
   in the FileMaker workspace: fixed, top to bottom, full width, sliding in
   from the left — and above 1025px, "Desktop: 400px wide panel instead of full
   screen". Live's Partners has been this shape for years; the rebuild is the
   thing that made it a sheet.
   ========================================================================= */
.h-panel {
  position: absolute; top: 0; left: 0; bottom: 0; width: 100%; z-index: 51;
  background: var(--panel); border-right: 1px solid var(--line2);
  display: flex; flex-direction: column; overflow: hidden;
  /* FROM THE LEFT, like live. The sheet rises; the panel slides. Two motions
     for two shapes, so the app tells you which one you are in before you
     read anything. */
  transform: translateX(-100%); transition: transform var(--t-sheet);
  /* 🛑 A RAW `rgba(0, 0, 0, 0.28)` DROP SHADOW WAS HERE AND IT IS REMOVED
     RATHER THAN TOKENISED, WHICH IS THE PART THAT NEEDED DECIDING.
     `css-raw-colour --check` flagged it, and the honest reason to act is not
     the lint: A HARD BLACK SHADOW CANNOT FLIP. In light it reads; in dark it
     is `rgba(0,0,0,.28)` against a `#0A0A0A` page and does nothing at all, so
     half the app got a separation the other half did not.
     ⚖️ AND THERE IS NO SHADOW TOKEN TO REACH FOR — `--scrim` is the wash BEHIND
     a sheet and inverts by design (dark in dark, LIGHT in light), so it is the
     wrong idea wearing the right colour.
     ✅ THE COMPONENT'S OWN MODEL SETTLES IT: `.h-sheet`, which this rule's
     header names as the other presentation of this idea, carries NO shadow —
     it separates with `border-top: 1px solid var(--line2)`. This panel already
     declares exactly that on its own edge, two lines up. The shadow was a
     second separator doing the first one's job in one theme only.
     📌 CASS wrote this rule for `MORE-05`; the seam is mine and so is the
     change. Flagged to her on the bus rather than done quietly. */
}
.h-panel.is-open { transform: translateX(0); }
.h-panel--right{left:auto;right:0;border-right:0;border-left:1px solid var(--line2);transform:translateX(100%)}
.h-panel--right.is-open{transform:translateX(0)}
/* `H365UI.panel({ full: true })` (Partners NR-02): full width at EVERY width (no desktop cap), over both navs
   (z 51 > the nav's 45-50), plain chrome — no drawer wash, a static head, a 36px X. Two classes, so it wins over
   hub.css's right-drawer dressing without a load-order dependency. */
.h-panel--right.h-panel--full { width: 100%; max-width: none; border-left: 0; background: var(--panel); }
.h-panel--right.h-panel--full .h-panel__head { min-height: 0; position: static; padding: 12px 16px; gap: 10px; justify-content: flex-start; }
.h-panel--right.h-panel--full .h-panel__x {
  position: static; width: 36px; height: 36px; flex: none; border: 1px solid var(--line2); background: var(--panel2);
  border-radius: var(--r-sm); font-size: 16px; line-height: 1; color: var(--ink);
}
.h-panel--right.h-panel--full .h-panel__body { padding: 12px 16px calc(28px + env(safe-area-inset-bottom)); }
.h-panel__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--line2);
  flex-shrink: 0;
}
.h-panel__title { font-weight: 800; font-size: var(--fs-lg); font-family: var(--font-head); }
/* Walk v45 (Karl): a Space's avatar left of the Manage drawer's title. */
.h-panel__av { flex: none; display: flex; width: 40px; height: 40px; border-radius: 12px; overflow: hidden; }
.h-panel__av .h-gav { width: 100%; height: 100%; }
.h-panel__av + .h-panel__title { flex: 1; }
/* A panel's round back (the Menu's groups): first in the head, title beside it. */
.h-panel__back { flex: none; width: 36px; height: 36px; }
.h-panel__back + .h-panel__title { flex: 1; min-width: 0; }
.h-panel__x {
  background: var(--panel2); border: 1px solid var(--line2); color: var(--ink);
  width: 30px; height: 30px; border-radius: var(--r-sm); cursor: pointer;
  font-size: 16px; line-height: 1; flex-shrink: 0;
}
/* THE BODY SCROLLS, NOT THE PANEL. A full-height surface whose whole self
   scrolls takes its own header away with it, and the close button is the one
   control a member must always be able to reach. */
.h-panel__body {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: 12px 16px calc(18px + env(safe-area-inset-bottom));
}
@media (min-width: 1025px) {
  /* Live's number, not a new one. */
  .h-panel { width: 400px; }
}

/* SYS-16, THE TOGGLE HALF, AND IT IS ONE MISSING DECLARATION.
   Every `.h-frow` in this app is rendered as a `<button>` (gamesheet.js:539,
   room.js:269/606/641/1284, schedule.js:674/1129), and a button carries the UA
   default `text-align: center`. `display: flex` fixes the axis for the row's
   direct children but does nothing to the text INSIDE them, and
   `.h-frow__label` is `flex: 1` — so it stretches the full width of the row and
   centres its own text in it. That is the top-half / bottom-half split in
   `Player in group roster.PNG`: Position and Jersey number are div rows and
   read clean; League Admin, Scheduler, Treasurer and Coach are button rows and
   float in the middle with the switch shoved to the far edge.
   It belongs HERE rather than with the button reset in hub.css, because
   `.h-frow`'s layout is defined here and kit.html does not load hub.css. */
/* ===========================================================================
   `SYS-26`(b) — ONE CONTAINER PER LIST, NOT A BOX PER ROW.
   Vera's ratified mock 1, lifted as she wrote it: *"every rule above uses only
   class names the app already has, so the build seat lifts it into
   components.css rather than reinterpreting a picture."*

   The wrapper is inserted by `H365UI.group()` over the assembled string — one
   transform rather than forty hand-placed tags, reasoning in `ui.js`.

   ⛔ ONLY THE STRUCTURE IS HERE. The mock's other three moves — the ON-row
   accent, the label type step, the switch geometry — are scoped to
   `[data-style="openice"]` in her file and are VALUES. Structure is mine and
   values are hers; lifting hers unscoped would be me shipping a style Karl
   approved as a style, as though it were the app's default. */
.h-grp {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  margin: 0 0 4px;
  /* The padding moved OFF the group and ONTO the row, so a row's own edge is
     the card's inner edge; `overflow: hidden` then makes the radius clip
     everything inside it. */
  padding: 0;
  overflow: hidden;
  /* `SYS-04`, the two CARD paint slots. Both default to nothing, so openice and
     gm render byte-for-byte as they did; street and pond are the only styles
     that set them.
     THE EDGE IS AN INSET SHADOW, NOT A BORDER, AND THAT IS THE WHOLE POINT. A
     second border would grow the box by its own width and move every child
     inside it — the style axis is repaint only. An inset shadow paints on the
     same edge and moves nothing.
     ORDER MATTERS: `background-image` must come AFTER the `background:`
     shorthand above, which resets background-image to none. Moving these three
     lines up silently kills the texture. */
  box-shadow: var(--card-edge);
  background-image: var(--card-tex);
  background-size: 9px 9px;
}
/* VERA'S FINDING 3. The padding named `.h-frow` and `.h-mtool` only, so a
   ROSTER row - `.h-mrow`, a div, neither of those - sat flush against the
   container edge. Her words: same cause as finding 2. */
.h-grp .h-frow, .h-grp .h-mtool { padding: 14px 12px; }
/* A ROSTER ROW IS DENSER THAN A SETTINGS ROW AND THE MOCK SAYS SO: 11, not 14.
   It carries an avatar, so its own content already sets the height; 14 on top
   of that made it the tallest row in the app. I landed 14 an hour ago by
   folding `.h-mrow` into the rule above - correcting to the ratified number. */
.h-grp .h-mrow { padding: 11px 12px; }
/* A SECTION WHOSE CONTENT IS NOT ROWS --- THE BOX CARRIES THE INSET.

   🪞 NAMED `--pad`, NOT `--prose`, AND THE FIRST NAME WAS WRONG WITHIN A DAY.
   It was coined for a list and two sentences in Settings; `SPC-08`'s Edit
   Player form needs exactly the same box round a run of `.h-lr__f` FIELDS, and
   a class called `--prose` wrapping a form is a name that lies to the next
   reader. The mechanism is what it is named for now: this box pads its own
   content, because there is no row inside it to do it.
 `SET-01`, Karl: *"The sections need
   some kind of outline or section framing around them so they are separated
   from one another."* -- SECTIONS, not rows. Two of Settings' twelve headings
   hold a list and a sentence rather than switches, so `H365UI.group()` leaves
   them alone by design and they sat bare on a screen of boxes.

   ⚠️ IT IS A MODIFIER RATHER THAN `.h-grp .h-lr__hint` AND THAT IS THE POINT.
   `.h-grp` carries no padding -- every row type buys its own inset through a
   rule named just above, which is Vera's finding 3: a row type nobody named
   sits flush against the edge. A bare `.h-grp .h-lr__hint` rule would pad any
   hint that ever lands inside ANY group, including the row cards where a hint
   does not belong and is currently kept out by the run rule. Scoping it to a
   class the caller opts into cannot leak. */
.h-grp--pad { padding: 12px; }
.h-grp--pad > :first-child { margin-top: 0; }
.h-grp--pad > :last-child { margin-bottom: 0; }

/* ===========================================================================
   THE REST OF VERA'S RATIFIED PATTERN, landed with the Recruitment rebuild
   because that screen is the first caller of every one of them.
   ========================================================================= */
/* A GROUP WHOSE CONTENT IS NOT ROWS - a pitch box, a paragraph - still wants to
   be the same card. The rows bring their own padding; this brings it for
   everything else. */
.h-grp--pad { padding: 12px; }

/* THE LINE UNDER A GROUP THAT EXPLAINS IT. It sits OUTSIDE the card on purpose:
   inside, it reads as another row and invites a tap. Karl's `SYS-26` complaint
   was a loose paragraph sitting between rows in no container - this is the
   other half of that answer, a paragraph that is deliberately not in one. */
.h-grp__foot { margin: 6px 2px 0; font-size: 11.5px; line-height: 1.35; color: var(--faint); }

/* AN INPUT THAT IS THE CARD'S OWN SURFACE rather than a box drawn on top of
   one. A bordered field inside a bordered card is two edges 12px apart, which
   is exactly the "extra outline" Karl reported on Recruitment.
   NO FONT-SIZE HERE. `.h-input`'s 16px stands, because a focused field under
   16px zooms mobile Safari and that floor is `SYS-07`. */
.h-input--flush { background: none; border: 0; border-radius: 0; padding: 0; }
.h-input--flush:focus { outline: none; }

/* THE NOTE THAT BELONGS TO THE ROW ABOVE IT. Indented to the row's own text
   column so it reads as that row's detail and not as a fourth role. */
.h-grp__note { padding: 0 12px 12px; }

/* THE ON ROW. 2px and INSET, so the container's radius clips it - a border
   would sit outside the corner and fail to follow it, which is the same
   mistake the accent made when it hung off the label. */
.h-frow.is-on { box-shadow: inset 2px 0 0 var(--action); }

/* VERA'S FINDING 2, AND SHE IS RIGHT TWICE OVER. I wrote
   `.h-frow:first-of-type, .h-mtool:first-of-type`, which (a) does not match
   `.h-mrow` at all, so the first row of every roster group kept a top border
   INSIDE the container, and (b) counts by ELEMENT TYPE, so it drifts the moment
   a group leads with anything else. `> :first-child` is one rule, covers every
   row type there will ever be, and cannot drift.
   She hit this exact trap herself in mock 3, where a `:first-of-type` guard
   never matched because a grip div came first. */
.h-grp > :first-child { border-top: 0; }

/* ===========================================================================
   VERA'S FINDING 1, AND IT IS MY OWN RULE QUOTED BACK AT ME. Three lines above
   the accent I wrote that structure is mine and values are hers, and that
   "lifting hers unscoped would be me shipping a style Karl approved as a style,
   as though it were the app's default." THEN I PUT HER ACCENT IN THE UNSCOPED
   BLOCK. In her mock the whole rule sits inside `[data-style="openice"]`.
   ⚖️ WHAT IT COST: every group on an unstyled page got a bright accent edge -
   including GENERAL MANAGER, whose entire identity is square, ruled and
   unfilled. A style that contradicts itself is worse than no style.
   📌 IT SITS HERE RATHER THAN UP IN THE OPENICE BLOCK ON PURPOSE: a reader of
   `.h-grp` has to be able to see that the accent is CONDITIONAL. If Vera wants
   it with the rest of her scoped values instead, it is hers to move.
   THE ACCENT IS THE CARD'S OWN TOP BORDER, not a rule hanging off the label -
   Karl's correction, 09-05: hung off the label it landed square in the gap
   above the card's rounded border, two lines that did not agree. Here the
   radius bends it and it cannot fail to fit.
   ========================================================================= */
/* ⚖️ `SET-01` REOPENED, 2026-09-21, Vera — THE ACCENT MOVES OFF THE CARD, AND
   MERYL'S OWN LINE FOUR PARAGRAPHS UP IS THE PERMISSION: *"If Vera wants it
   with the rest of her scoped values instead, it is hers to move."*

   🔴 WHY IT HAD TO MOVE, MEASURED ON A RENDER OF SETTINGS RATHER THAN ARGUED:
   **eleven cards on that screen, eleven 2px accents, ONE distinct computed
   signature between them.** An edge that appears at every level marks no level.
   It had become texture, and it was the loudest texture on the page.
   🔴 AND IT WAS PAINTING TWICE. `base.css` gives `.h-sec-label` a 2px rule from
   `--head-rule`; `.h-sec-label` has `margin: 0` and `.h-grp` has `margin-top:
   0`, so the two sat at a **measured 0px gap** — heading bottom 34.8, card top
   34.8 — a 42%-alpha green line stacked directly on a 100% one, with the
   straight rule overhanging the card's own radius at both ends. **Two lines
   that do not agree**, which is the exact trap Meryl's note above describes and
   avoids by putting the accent on the card. I put a second one back under the
   label on 09-19 and could not see it from either file. **That half is mine.**

   ✅ THE ACCENT IS NOW THE CHAPTER'S, VIA THE HEADING RULE IT ALREADY HAD:
   three green marks on Settings instead of fourteen. The card keeps a plain
   top border like its other three sides, so it is still a card and no longer
   also a banner.
   ⛔ REVERTING IS THIS ONE LINE. Karl has the before and after side by side in
   `Mock10_SET01_Sectioning.html`; if he wants the banner back it goes back. */
[data-style="openice"] .h-grp { border-top: 1px solid var(--line); }

.h-frow {
  display: flex; align-items: center; gap: 12px; padding: 13px 2px;
  border-top: 1px solid var(--line); cursor: pointer;
  text-align: left;
  transition: transform var(--t-fast);
}
.h-frow:active { transform: scale(.977); }
.h-frow__dot { width: 13px; height: 13px; border-radius: 4px; flex: none; }
.h-frow__label { flex: 1; font-size: 14px; font-weight: 600; }

/* ⏪ 2026-09-29 (Karl, walking on his MacBook): the sticky bar from V3.48 floated
   up the screen on short wizards (Recruitment, Create a Game) and covered the
   form. Karl's rule was narrower than what shipped: ONLY a true long edit form
   (opened to change one thing) pins its buttons, flush to the bottom, glass
   while more is below, heavier glass than the nav. Back at the end of every
   sheet until that opt-in is built. */
.h-sheet__actions { display: flex; gap: var(--sp-3); margin: 14px 0 2px; }
.h-sheet__actions:empty { display: none; }   /* a sheet that carries its own bar (RR-04) */
.h-sheet__actions .h-btn--ghost { flex: 1; }
.h-sheet__actions .h-btn:not(.h-btn--ghost) { flex: 1.6; }

/* ---------------------------------------------------------------------------
   13. CALENDAR (month grid + week strip + agenda)
   ------------------------------------------------------------------------ */
.h-cal { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 12px; }
.h-cal__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.h-cal__month { font-weight: 800; font-size: 15px; font-family: var(--font-head); }
.h-cal__nav {
  background: var(--panel2); border: 1px solid var(--line2); color: var(--ink);
  width: 28px; height: 28px; border-radius: var(--r-sm); cursor: pointer; font-size: 14px;
}
.h-cal__dow  { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; margin-bottom: 4px; }
.h-cal__dow span { text-align: center; font-size: 10px; color: var(--faint); font-weight: 700; }
.h-cal__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; }
.h-cal__cell {
  aspect-ratio: 1; border-radius: var(--r-sm); background: var(--cell-bg);
  display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
  padding-top: 5px; font-size: 11px; color: var(--muted); position: relative; border: 0; cursor: pointer;
}
.h-cal__cell.is-out   { opacity: .3; }
.h-cal__cell.is-today { background: var(--green-dim); color: var(--ink); font-weight: 800; box-shadow: inset 0 0 0 1px var(--green); }
.h-cal__dots { display: flex; gap: 2px; margin-top: 3px; flex-wrap: wrap; justify-content: center; }
.h-cal__dot  { width: 5px; height: 5px; border-radius: 50%; }
.h-cal__legend { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--line); }
.h-cal__legend-item { display: flex; align-items: center; gap: 5px; font-size: 10px; color: var(--muted); }

.h-week { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 6px; }
.h-week__day {
  border-radius: var(--r-sm); background: var(--cell-bg); padding: 7px 0 6px;
  text-align: center; cursor: pointer; border: 0; color: var(--ink);
}
.h-week__day.is-today { background: var(--green-dim); box-shadow: inset 0 0 0 1px var(--green); }
.h-week__dw { display: block; font-size: 8px; color: var(--faint); font-weight: 800; letter-spacing: .05em; }
.h-week__dn { display: block; font-family: var(--font-display); font-size: 17px; font-weight: 400; line-height: 1; margin-top: 4px; color: var(--ink); }
.h-week__dots { display: flex; gap: 2px; justify-content: center; margin-top: 4px; min-height: 6px; }
.h-week__dots i { width: 5px; height: 5px; border-radius: 50%; display: block; }

.h-agenda__day {
  font-size: 11px; letter-spacing: .05em; text-transform: uppercase;
  color: var(--faint); font-weight: 700; margin: 14px 2px 8px;
}
.h-agenda__row {
  display: flex; gap: 11px; align-items: stretch; background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--r-md); padding: 10px 11px; margin-bottom: 8px;
  transition: transform var(--t-fast);
}
.h-agenda__row:active { transform: scale(.977); }
.h-agenda__time { font-size: 11px; font-weight: 800; color: var(--muted); width: 56px; flex: none; padding-top: 1px; }
.h-agenda__bar  { width: 3px; border-radius: 2px; flex: none; }
.h-agenda__t    { font-weight: 700; font-size: 13.5px; }
.h-agenda__s    { font-size: 11.5px; color: var(--muted); margin-top: 1px; }
.h-agenda__empty{ text-align: center; color: var(--faint); font-size: 12.5px; padding: 24px 0; }

/* Schedule card (date block + meta + tag) */
/* SYS-16, THIRD INSTANCE — AND THIS ONE NO SCREENSHOT WAS FILED AGAINST.
   The scanner found it: `.h-scard` is a `<button>` (ice.js:218, room.js:381,
   schedule.js:310 and :328) laying out with flex, with a `flex: 1` child, and
   no `text-align` — the same three conditions as `.h-frow`. `Group Schedule.PNG`
   shows it once you know to look: "Game" and "8:00 AM · Test rink" float in the
   middle of the card.

   AND THE SECOND FAULT IN THE SAME RULE, which is what makes that card look
   wrong even to someone not reading the text: a `<button>` with no width
   SHRINK-WRAPS. Every other block on that screen runs the full column and this
   one stops short, which is half of what `SYS-09` names on this exact screen
   ("Create game, Upcoming label and the game card"). `flex: 1` on `__main`
   filled the button; nothing filled the button itself. */
.h-scard {
  display: flex; gap: 11px; background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: 10px 11px; margin-bottom: var(--sp-3); align-items: center;
  width: 100%; text-align: left;
  cursor: pointer; transition: transform var(--t-fast);
}
.h-scard:active { transform: scale(.977); }
.h-scard__date { width: 44px; text-align: center; flex: none; background: var(--panel2); border-radius: var(--r-md); padding: 6px 0; }
.h-scard__day  { font-size: 17px; line-height: 1; font-family: var(--font-head); font-weight: 800; }
.h-scard__mon  { font-size: 9px; color: var(--muted); text-transform: uppercase; font-weight: 700; margin-top: 2px; }
.h-scard__title{ font-weight: 600; font-size: 13.5px; }
.h-scard__meta { color: var(--faint); font-size: 11.5px; margin-top: 1px; }
.h-scard__tag  { font-size: 9.5px; font-weight: 800; border-radius: var(--r-pill); padding: 2px 8px; flex: none; }

.h-legend { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-bottom: 12px; font-size: 11px; color: var(--muted); }
.h-legend span { display: flex; align-items: center; gap: 5px; }
.h-legend i { width: 9px; height: 9px; border-radius: 3px; display: inline-block; }

/* ---------------------------------------------------------------------------
   14. HERO + MODEL PICKER + AI BAR
   ------------------------------------------------------------------------ */
.h-hero { border-radius: var(--r-xl); padding: 16px; margin-bottom: 13px; position: relative; overflow: hidden; border: 1px solid var(--line2); }
.h-hero--green { background: linear-gradient(135deg, var(--hero-green-deep), rgba(var(--brand-green-rgb),.16)); }
[data-theme="light"] .h-hero--green { background: linear-gradient(135deg, rgba(var(--brand-purple-rgb),.22), rgba(var(--brand-purple-rgb),.06)); }
.h-hero__eyebrow { font-size: 10px; letter-spacing: .1em; text-transform: uppercase; font-weight: 800; color: var(--green); }
.h-hero__title   { font-size: 19px; font-weight: 800; margin-top: 4px; font-family: var(--font-head); }
.h-hero__sub     { font-size: 12px; color: var(--muted); margin-top: 3px; }

.h-models { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-bottom: 13px; }
.h-model {
  background: var(--panel2); border: 1px solid var(--line); border-radius: var(--r-md);
  padding: 10px 9px; text-align: center; cursor: pointer;
  transition: transform var(--t-fast), border-color var(--t-fast);
}
.h-model:hover  { border-color: var(--green); }
.h-model:active { transform: scale(.977); }
.h-model__ic { width: 30px; height: 30px; border-radius: var(--r-sm); margin: 0 auto 6px; display: grid; place-items: center; background: var(--green-dim); color: var(--green); }
.h-model__ic svg { width: 17px; height: 17px; }
.h-model__t  { font-weight: 700; font-size: 11.5px; }
.h-model__d  { color: var(--faint); font-size: 10px; margin-top: 2px; }

.h-aibar {
  display: flex; align-items: center; gap: var(--sp-3); background: var(--panel2);
  border: 1px solid var(--brand-purple); border-radius: var(--r-lg); padding: 11px 13px;
  margin-bottom: 6px; box-shadow: 0 0 0 3px rgba(var(--brand-purple-rgb),.16);
}
.h-aibar__ic { color: var(--brand-purple); }
.h-aibar__ic svg { width: 18px; height: 18px; }
.h-aibar__q  { color: var(--muted); font-size: 13px; }
.h-aians { background: var(--panel); border: 1px solid var(--line2); border-radius: var(--r-lg); padding: 12px; margin-bottom: 12px; }
.h-aians__label {
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--brand-purple); font-weight: 800; margin-bottom: 6px;
  display: flex; align-items: center; gap: 6px;
}
.h-aians__row { display: flex; align-items: center; gap: var(--sp-3); padding: 7px 0; border-top: 1px solid var(--line); font-size: 12.5px; }
.h-aians__row:first-of-type { border-top: none; }

/* ---------------------------------------------------------------------------
   15. PUSH-DETAIL OVERLAY (space / game / program / settings)
   ------------------------------------------------------------------------ */
.h-detail {
  position: absolute;
  inset: 0;
  z-index: 44;
  background: var(--bg);
  transform: translateX(100%);
  transition: transform var(--t-sheet);
  display: flex;
  flex-direction: column;
  visibility: hidden;
  max-width: 100%;
  overflow-x: hidden;
}
.h-detail.is-open { transform: none; visibility: visible; }
.h-detail__head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  flex: 0 0 auto;
}
/* `.h-detail__back` IS NOW DEFINED ONLY HERE. It was in this file AND in
   `hub.css`, and the two boxes AGREED -- which is worse than disagreeing,
   because nothing was red and the next edit to either one is where they part.

   ⚠️ AND THEY HAD ALREADY PARTED IN THE PLACE THAT MATTERS. Only `hub.css`
   carried the 180-degree rotation below. The icon is `chevronRight`, turned to
   point left, so on the SEVEN pages that load this file WITHOUT `hub.css` --
   checkout, claim, login, setup, signin-link, staff-payments, verify -- a Back
   button would have pointed RIGHT. Latent only because `spaces.js` is the one
   caller today and it renders on `index.html`; `Q-02` puts this class in the
   SHARED overlay head, which is what would have made it live.

   🪞 THE 08-17z SWEEP ALREADY DID THIS FOR THE SIBLINGS AND MISSED THIS BLOCK.
   `.h-detail__head`, `.h-detail__title` and `.h-detail__body` were each moved
   here for exactly this reason, and `hub.css` still carries their gravestone
   comments -- one of which sat directly above the copy of `.h-detail__back`
   that was left behind, describing a different selector.

   📌 `.h-detail__back svg` IS KEPT AND IS CURRENTLY INERT FOR THE ONLY CALLER:
   `.h-ic svg { width: 100%; height: 100% }` at `:1915` is the same specificity
   and later in this file, so it wins for any icon rendered the house way. It
   still covers a raw `<svg>` dropped straight into the button, which is why it
   is recorded here rather than deleted in a landing whose whole value is that
   it changes nothing on screen. */
/* Stage 1 (Rowan): the circular, arrow-only Back of the approved Space head.
   Its accessible name carries the destination (`spaces.js` `show()`). */
.h-detail__back {
  width: 40px; height: 40px; border-radius: 50%; background: var(--panel2);
  border: 1px solid var(--line2); color: var(--ink); display: grid; place-items: center;
  cursor: pointer; flex: none;
}
.h-detail__back svg { width: 18px; height: 18px; }
/* The chevron points right in the library; a Back control points left. */
.h-detail__back .h-ic { width: 16px; height: 16px; transform: rotate(180deg); }
/* `Q-02` REVERTED HERE TOO. `.h-sheet__back` and `.h-panel__back` placed an
   automatic back control in the two shared heads for one round; Karl found it
   on the first screen. There is no slot in either head now -- a screen with a
   parent renders `.h-back` as the first thing in its own body, which is where
   guides puts it. The `.h-detail__back` merge above STAYS: it was a real
   duplicate body and merging it was right whatever sits in the slot. */
/* THE SHARED SPACE TITLE BLOCK (Stage 1): the kind of Space, small, above its
   name. The block owns the flexible width so the title keeps its ellipsis. */
.h-detail__ttl { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.h-detail__kicker {
  font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--green); line-height: 1.2;
}
.h-detail__kicker[hidden] { display: none; }
.h-detail__title {
  font-weight: 800;
  font-size: 16px;
  font-family: var(--font-head);
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin: 0;
}
/* `.h-detail__act` IS NOW DEFINED ONLY HERE, MERGED ON THE RENDERED READING.
   It was in this file AND in hub.css and the two DISAGREED about what the
   element even is.

   KEPT (hub.css's, because it is what reaches a screen):
       flex: none; display: flex; gap: 6px;
   DROPPED (this file's, written when `.h-detail__act` was itself a BUTTON):
       font-size: 11.5px; font-weight: 700; color: var(--green);
       cursor: pointer; background: none; border: 0;

   🔍 WHY THE DROP IS SAFE AND NOT A JUDGEMENT CALL: the element is a
   CONTAINER -- `spaces.js:156` emits `<div class="h-detail__act"></div>` and
   `:168` fills it from `show()`'s third argument. THAT ARGUMENT HAS NO CALLER.
   All nine real `show()` calls pass two arguments, so the div is filled with
   `''` on every overlay in the app. There is nothing inside it to inherit the
   ink, the weight or the size, which is why dropping them changes nothing that
   renders. Measured by reading every call site, not inferred.

   ⚠️ AND THAT IS ITSELF A FINDING, ROUTED RATHER THAN FIXED HERE: an action
   slot in the overlay head with no caller is a rule with no caller, in the one
   header `Q-02` just standardised. `spaces.js` is Theo's and adding or
   retiring an action slot is a design call, so it is named on the bus. */
.h-detail__act { flex: none; display: flex; align-items: center; gap: 10px; }  /* V3.32: holds the app bar's buttons too (B03) */

/* THE BACK CONTROL --- AND IT IS GUIDES', PROMOTED, NOT A NEW ONE.

   🗣️ KARL: *"The small back button that is present on screens like guides
   overlay is the right one to use. The new one is terrible."*

   THIS BODY IS `content.css`'s `.h-back`, VERBATIM. The guides reader has had
   it all along and it was already right; the sweep's job was to adopt it, and
   for one round it did the opposite -- it invented a chevron-and-words control
   and gave that to twenty-two sites including guides, which already had this.

   🪞 THE STANDING RULE THIS BROKE IS THE ONE THE PROJECT KEEPS RE-LEARNING:
   look for the thing that already does it. `.h-detail__back` was a duplicate
   body worth merging; this was a working control worth COPYING, and the two
   jobs got treated as one.

   📌 ONE MODIFIER, FOR THE ONE CONTEXT WITH NO ROOM FOR WORDS: a chat
   conversation header beside an avatar and a name. Chat's own visibility rules
   for it stay in hub.css, because hiding by default is chat's layout decision
   and not this control's. */
.h-back {
  background: none; border: 0; padding: var(--sp-2) 0;
  color: var(--green); font-size: var(--fs-xs); cursor: pointer;
}
.h-back--icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; margin-left: -6px;
  border-radius: 50%; color: inherit;
}
.h-back--icon:hover { background: var(--panel2); }
.h-back--icon .h-ic { width: 20px; height: 20px; }
.h-screenback {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1);
  min-width: 44px; min-height: 44px; padding: 0 var(--sp-2);
  border: 0; border-radius: var(--r-pill); background: transparent;
  color: var(--ink); font-size: 12px; font-weight: 800; cursor: pointer;
}
.h-screenback__ic, .h-screenback__ic svg { width: 18px; height: 18px; }
@media (max-width: 430px) {
  .h-screenback { width: 44px; padding: 0; border: 1px solid var(--line2); background: var(--panel2); }
  .h-screenback__label {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
  }
}
.h-detail__body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 14px 16px 30px;
  min-height: 0;
}
/* THE WIDE-SCREEN SPACE FRAME (Stage 1). The panel still owns the whole app
   viewport; on a wide screen its head and body hold one readable column
   instead of stretching rows and tab bars edge to edge. Padding, not a
   max-width box, so the scrollbar stays at the window's edge. */
@media (min-width: 900px) {
  .h-detail__head, .h-detail__body {
    padding-left: max(16px, calc((100% - 820px) / 2));
    padding-right: max(16px, calc((100% - 820px) / 2));
  }
}
/* THE PRESET HERO PHOTO (Karl, 2026-09-26: approved presets only). One frame
   for every Space: the photo behind, the shared photo veil over it, and the
   Space's own identity block sitting on the bottom edge. The image is a
   decorative background (`--hero`), so it carries no alt text by design. */
.h-sphoto {
  /* The photo arrives inline per Space (`heroFrame`); `none` is the declared
     default so the variable always resolves. */
  --hero: none;
  --hero-wash: linear-gradient(transparent, transparent);
  position: relative; overflow: hidden; border-radius: var(--r-lg);
  min-height: 150px; margin-bottom: 12px; padding: 12px;
  display: flex; flex-direction: column; justify-content: flex-end;
  /* B65 G4: a designed Space's colour-set wash lies over its header (Karl ruling 6); none by default. */
  background: var(--hero-wash), var(--hero) center 42% / cover no-repeat, var(--panel);
}
.h-sphoto::before {
  content: ""; position: absolute; inset: 0;
  /* Transparent at the top so the photo reads, the identity scrim at the
     bottom where the name sits, so white text holds contrast on any preset. */
  background: linear-gradient(180deg, var(--veil-photo-top) 20%, var(--scrim-identity));
}
.h-sphoto > * { position: relative; z-index: 1; }
.h-sphoto .h-room__head, .h-sphoto .h-ehero, .h-sphoto .h-shero {
  margin: 0; padding: 0; border: 0; background: none;
}
.h-sphoto .h-shero::before { content: none; }
.h-sphoto .h-room__n, .h-sphoto .h-ehero__n, .h-sphoto .h-shero__n { text-shadow: var(--sh-ink-on-photo); }
.h-sphoto .h-room__s, .h-sphoto .h-ehero__where, .h-sphoto .h-shero__s { color: var(--ink); opacity: .86; }
@media (min-width: 900px) { .h-sphoto { min-height: 200px; } }

/* THE CONTEXTUAL REGISTRATION SHEET (`regsheet.js`, Karl 2026-09-26). Shared
   tokens only; the sheet itself is `H365UI.open`. */
.h-reg { display: block; }
.h-reg__back { margin-bottom: 4px; }
.h-reg__pr { margin-bottom: 4px; }
/* The Game Space's parent chip and its avatar; the drawer body. */
.h-ehero__parent { max-width: 100%; }
.h-ehero__pav { flex: none; border-radius: 5px; overflow: hidden; }
.h-mdrawer { display: flex; flex-direction: column; }
.h-reg__steps { display: flex; gap: 6px; list-style: none; margin: 0 0 12px; padding: 0; }
.h-reg__steps li {
  flex: 1; text-align: center; font-size: 11px; font-weight: 800; padding: 5px 0;
  border-radius: var(--r-pill); background: var(--panel2); color: var(--muted);
}
.h-reg__steps li.is-on { background: var(--green); color: var(--on-green); }
.h-reg__steps li.is-done { color: var(--ink); }
.h-reg__for { display: flex; flex-direction: column; margin-bottom: 12px; }
.h-reg__kind { font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--green); }
.h-reg__name { font-family: var(--font-head); font-weight: 800; font-size: 17px; }
.h-reg__when { font-size: 12.5px; color: var(--muted); }
.h-reg__people { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.h-reg__person {
  display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 8px 12px;
  border: 1px solid var(--line2); border-radius: var(--r-md); background: var(--panel); cursor: pointer;
}
.h-reg__person input { width: 20px; height: 20px; accent-color: var(--green); flex: none; }
.h-reg__person.is-off { opacity: .6; cursor: default; }
.h-reg__pname { flex: 1; min-width: 0; font-weight: 700; overflow-wrap: anywhere; }
.h-reg__pname small { color: var(--muted); font-weight: 600; margin-left: 4px; }
/* SS-04 (Karl, 2026-09-29): one compact line per guest; the whole form lives
   in the Edit panel, and adding one walks small step sheets. */
.h-reg__gline {
  display: flex; align-items: center; gap: 6px; padding: 8px 10px; margin-bottom: 8px;
  border: 1px solid var(--line2); border-radius: var(--r-md); background: var(--panel);
}
.h-reg__gline.is-missing { border-color: var(--warn); }
.h-reg__gwho { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.h-reg__gwho b { overflow-wrap: anywhere; }
.h-reg__gwho small { color: var(--muted); font-weight: 600; }
.h-reg__gline.is-missing .h-reg__gwho small { color: var(--warn); }
.h-reg__wstep { margin: 0 0 8px; color: var(--muted); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
.h-reg__gedit { display: flex; flex-direction: column; gap: 2px; }
/* SS-03/04: the guest's birth-date picker, and Use me above the contact fields. */
.h-reg__dob { margin-top: 2px; }
.h-reg__emgme { margin: 2px 0 8px; }
.h-reg__roles { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 8px; margin-bottom: 10px; }
.h-reg__role {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px; min-height: 44px;
  padding: 9px 11px; border: 1px solid var(--line2); border-radius: var(--r-md);
  background: var(--panel); color: var(--ink); font: inherit; cursor: pointer; text-align: left;
}
.h-reg__role.is-on { border-color: var(--green); box-shadow: 0 0 0 1px var(--green) inset; }
.h-reg__rl { font-weight: 800; font-size: 13px; }
.h-reg__rp { font-size: 13px; }
.h-reg__rs { font-size: 11px; color: var(--muted); }
.h-reg__role.is-full .h-reg__rs { color: var(--warn); }
.h-reg__lines { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.h-reg__line {
  display: flex; align-items: center; gap: 10px; padding: 9px 12px;
  border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel);
}
.h-reg__line .h-tag { margin-top: 0; cursor: default; }
.h-reg__ln { flex: 1; min-width: 0; font-weight: 700; display: flex; flex-direction: column; overflow-wrap: anywhere; }
.h-reg__ln small { font-weight: 600; color: var(--muted); }
.h-reg__lp { font-weight: 700; white-space: nowrap; }
.h-reg__total { display: flex; justify-content: space-between; align-items: baseline; padding: 4px 2px 8px; }
.h-reg__consent {
  border: 1px solid var(--warn); border-radius: var(--r-md); padding: 10px 12px; margin: 8px 0;
  font-size: 13px;
}
.h-reg__consent p { margin: 0 0 8px; }
.h-reg__agree { display: flex; align-items: center; gap: 8px; font-weight: 700; min-height: 44px; cursor: pointer; }
.h-reg__agree input { width: 20px; height: 20px; accent-color: var(--green); }
.h-reg__go { margin-top: 12px; }
.h-reg__done { font-family: var(--font-head); font-weight: 800; font-size: 20px; margin-bottom: 8px; outline: none; }

/* ===========================================================================
   THE GAME SPACE BODY (Rowan, 2026-09-26) — the approved preview's game
   surface (`previews/Hockey365_Full_Interactive_Preview.html`
   `main[data-game-surface]`) built on the app's own tokens. Scoped to `.gs`
   so nothing else on the push-detail panel changes.
   ======================================================================== */
.gs { display: flex; flex-direction: column; gap: 12px; }
.gs-body { display: flex; flex-direction: column; gap: 10px; }
.gs .h-ic svg { width: 100%; height: 100%; }

/* Hero: photo, a veil, the date card on the left and the copy beside it. */
.gs-hero {
  --hero: none;
  --hero-wash: linear-gradient(transparent, transparent);
  position: relative; overflow: hidden; border-radius: var(--r-xl);
  min-height: 176px; display: grid; grid-template-columns: 64px minmax(0, 1fr);
  align-items: end; gap: 12px; padding: 18px 16px 34px;
  background: var(--hero-wash), var(--hero) center 42% / cover no-repeat, var(--panel);
  margin: 0;
}
.gs-hero::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(0deg, color-mix(in srgb, var(--bg) 92%, transparent),
                              color-mix(in srgb, var(--bg) 10%, transparent) 78%);
}
.gs-hero > * { position: relative; z-index: 1; }
.gs-date {
  width: 58px; padding: 9px 0; text-align: center; border-radius: 12px;
  border: 1px solid var(--hairline-on-scrim); background: color-mix(in srgb, var(--bg) 72%, transparent);
  display: flex; flex-direction: column; line-height: 1.05;
}
.gs-date span { color: var(--green); font-size: 11px; font-weight: 800; letter-spacing: .06em; }
.gs-date strong { font-family: var(--font-display); font-weight: 400; font-size: 28px; color: var(--ink); }
.gs-date small { color: var(--muted); font-size: 11px; font-weight: 800; letter-spacing: .06em; }
.gs-hero__copy { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
/* Walk v45 (Karl): the Game Space hero puts the date card on the RIGHT,
   under the status chip, beside the title and lines. */
.gs-hero--dright { grid-template-columns: minmax(0, 1fr); }
.gs-hero__mid { display: flex; align-items: flex-start; gap: 12px; min-width: 0; margin-top: 2px; }
.gs-hero__txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.gs-hero__mid .gs-date { flex: none; margin-top: 4px; }
/* Preview v2: the game title is the switcher button (`.h-swt` look). */
.gs-hero__swt { align-self: flex-start; max-width: 100%; }
.gs-hero__swt .gs-hero__title { display: block; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gs-hero__meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
.gs-parent {
  display: inline-flex; align-items: center; gap: 7px; min-width: 0; padding: 0;
  border: 0; background: none; color: var(--ink); cursor: pointer; font: inherit;
  font-size: 10px; font-weight: 900; letter-spacing: .06em; text-transform: uppercase;
  text-shadow: var(--sh-ink-on-photo);
}
.gs-parent span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gs-parent .h-gav { flex: none; border-radius: 8px; overflow: hidden; }
.gs-status {
  flex: none; padding: 4px 9px; border-radius: var(--r-pill); font-size: 11px; font-weight: 800;
  background: linear-gradient(var(--purple-dim), var(--purple-dim)), var(--panel); border: 1px solid color-mix(in srgb, var(--purple) 60%, transparent);
  color: var(--active-purple-ink);
}
.gs-status.is-off { background: linear-gradient(color-mix(in srgb, var(--danger) 14%, transparent), color-mix(in srgb, var(--danger) 14%, transparent)), var(--panel); border-color: var(--danger); color: var(--danger); }
.gs-hero__title {
  margin: 6px 0 4px; font-family: var(--font-display); font-weight: 400; font-size: 26px;
  line-height: 1; letter-spacing: .025em; text-transform: uppercase; color: var(--ink);
  overflow-wrap: anywhere; text-shadow: var(--sh-ink-on-photo);
}
.gs-hero__line {
  display: flex; align-items: center; gap: 6px; margin: 0; font-size: 13px;
  color: var(--ink); opacity: .86;
}
.gs-hero__line .h-ic { width: 14px; height: 14px; flex: none; color: var(--green); }

/* The member's strip, lifted over the hero's bottom edge. */
.gs-strip {
  position: relative; z-index: 2; margin: -30px 10px 0; min-height: 82px;
  display: flex; align-items: center; gap: 12px; padding: 14px 13px 14px 14px;
  border: 1px solid var(--line); border-radius: var(--r-xl);
  background: linear-gradient(100deg, var(--panel2) 0%, var(--panel2) 68%, var(--elev) 100%);
  box-shadow: 0 15px 30px color-mix(in srgb, var(--bg) 60%, transparent);
}
.gs-strip.has-jersey { padding-left: 110px; }
.gs-jersey {
  /* GB-02 (Karl, 2026-09-29): the PAINTED jersey, one finished file per colour
     (`tools/jersey-variants.mjs`); the row sets `--jersey` to its side's file. */
  --jersey: url('../img/spaces/jerseys/jersey-white.png');
  position: absolute; left: 8px; bottom: -4px; width: 96px; height: 100px;
  background: var(--jersey) center bottom / contain no-repeat;
  filter: drop-shadow(0 12px 10px color-mix(in srgb, var(--bg) 70%, transparent));
}
.gs-strip__copy { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.gs-strip__copy small, .gs-head small, .gs-callout small, .gs-apps small, .gs-recruit small {
  font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
}
.gs-strip__copy strong {
  font-family: var(--font-display); font-weight: 400; font-size: 20px; letter-spacing: .018em;
  line-height: 1.05; overflow-wrap: anywhere;
}
.gs-strip__price { font-size: 11px; font-weight: 800; color: var(--active-green-ink); }
.gs-strip__acts { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
/* TB-03 (Karl, 2026-09-28): the check-in says what it is — a caption over it,
   and an unset state that is an obvious green-outlined action, not a grey TBD. */
.gs-attwrap { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.gs-att {
  min-width: 76px; min-height: 36px; padding: 7px 12px; border-radius: var(--r-pill); cursor: pointer;
  font: inherit; font-weight: 800; font-size: 13px; white-space: nowrap;
  /* B17 (Karl): not answered yet reads TBD and stays GREY; only an answer
     (In / Out) or the arrival Check in carries colour. */
  border: 1.5px solid var(--line2); background: transparent; color: var(--muted);
}
.gs-att--arrive { border-color: var(--green); color: var(--active-green-ink); }
/* Small text buttons under the fee line: they add no box and move nothing. */
.gs-strip__links { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 1px; }
.gs-tlink {
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
  font: inherit; font-size: 11.5px; font-weight: 800; line-height: 1.3;
}
.gs-tlink--leave { color: var(--danger); text-decoration: underline; text-underline-offset: 2px; display: inline-flex; align-items: center; gap: 3px; }
.gs-tlink--leave .h-ic { width: 11px; height: 11px; }
/* TB-03 second pass: Exempt me from fees sits under the attendance answer. */
.gs-exempt { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; margin-top: 2px; }   /* TB-03: one line */
.gs-exempt .h-ic { width: 13px; height: 13px; flex: none; }
.gs-tlink--exempt { color: var(--warn); }
.gs-tlink:hover { text-decoration: underline; }
.gs-tlink:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; border-radius: 3px; }
.gs-att.is-yes { border-color: var(--green); background: linear-gradient(var(--green-dim), var(--green-dim)), var(--panel2); color: var(--active-green-ink); }
.gs-att.is-no { border-color: var(--danger); background: linear-gradient(color-mix(in srgb, var(--danger) 12%, transparent), color-mix(in srgb, var(--danger) 12%, transparent)), var(--panel2); color: var(--danger); }

/* Callouts: an eligible group member, a visitor, a session prospect. */
.gs-callout {
  margin: 0 10px; padding: 12px; border-radius: var(--r-xl);
  border: 1px solid var(--line); background: var(--panel2);
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px;
}
.gs-callout--join { border-color: color-mix(in srgb, var(--green) 38%, var(--line)); background: var(--active-green-bg); }
.gs-callout--join small { color: var(--green); }
.gs-callout--session { border-color: color-mix(in srgb, var(--purple) 50%, var(--line)); background: var(--active-purple-bg); }
.gs-callout--visitor { border-color: var(--line2); }
.gs-callout--rsvp { padding: 0; border: 0; background: none; }
.gs-rgroup--wait .gs-rgroup__head .h-sec-label { color: var(--active-purple-ink); }
.gs-callout__copy { flex: 1 1 200px; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.gs-callout__copy strong { font-family: var(--font-display); font-weight: 400; font-size: 19px; line-height: 1.05; }
.gs-callout__copy span { font-size: 12px; color: var(--muted); }
.gs-callout__copy em { font-style: normal; font-size: 12px; color: var(--muted); }
.gs-callout .h-rsvp { flex: 1 1 100%; margin: 0; padding: 0; border: 0; background: none; }
.gs-apply {
  min-height: 44px; padding: 12px 18px; border: 0; border-radius: 12px; cursor: pointer;
  background: var(--green); color: var(--on-green); font: inherit; font-weight: 900;
}

/* Tabs: an icon above every label, one row. */
.gs-tabs {
  --gs-tabs: 5; /* the tab count; declared here so the default is a real token (tokens lint) */
  display: grid !important; grid-template-columns: repeat(var(--gs-tabs), minmax(0, 1fr)); gap: 2px;
  margin: 0 !important; padding: 4px !important; border: 1px solid var(--line) !important;
  border-radius: 15px; background: var(--panel2);
}
.gs-tabs .h-room__tab {
  flex-direction: column; gap: 4px; padding: 9px 2px 8px; border-radius: 11px; font-size: 10.5px;
}
.gs-tabs .h-room__tab .h-room__tab-lb { display: block; }
.gs-tabs .h-room__tab.is-on { background: var(--active-green-bg); color: var(--active-green-ink); }

/* Overview. */
.gs-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 10px; }
.gs-head small { color: var(--green); }
.gs-head h3 {
  margin: 2px 0 0; font-family: var(--font-display); font-weight: 400; font-size: 21px;
  letter-spacing: .025em; text-transform: uppercase;
}
.gs-share, .gs-btn {
  display: inline-flex; align-items: center; gap: 6px; min-height: 34px; padding: 7px 11px;
  border: 1px solid var(--line); border-radius: 10px; background: var(--panel2);
  color: var(--ink); font: inherit; font-size: 12px; font-weight: 700; cursor: pointer;
}
.gs-share .h-ic { width: 14px; height: 14px; }
.gs-btn--ok { border-color: var(--green); background: linear-gradient(var(--green-dim), var(--green-dim)), var(--panel2); color: var(--active-green-ink); }
.gs-btn--no { color: var(--danger); }
.gs-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.gs-metric {
  min-width: 0; padding: 11px; border: 1px solid var(--line); border-radius: 13px; background: var(--panel2);
  display: flex; flex-direction: column; gap: 2px;
}
.gs-metric > span:first-child { font-size: 10px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.gs-metric strong { font-family: var(--font-display); font-weight: 400; font-size: 21px; color: var(--active-green-ink); }
.gs-metric small, .gs-metric .h-needs__c { font-size: 11px; color: var(--muted); overflow-wrap: anywhere; }
.gs-metric.is-short strong { color: var(--warn); }
@media (max-width: 390px) {
  .gs-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .gs-metrics .gs-metric:last-child { grid-column: 1 / -1; }
}
.gs-recruit {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; cursor: pointer;
  padding: 12px; border-radius: var(--r-lg); color: var(--ink); font: inherit;
  border: 1px solid color-mix(in srgb, var(--purple) 70%, transparent);
  background: linear-gradient(118deg, var(--active-purple-bg), var(--panel2) 68%, var(--active-green-bg));
}
.gs-recruit__mark {
  flex: none; width: 42px; height: 42px; border-radius: 12px; display: grid; place-items: center;
  background: var(--purple-solid); color: var(--on-purple);
}
.gs-recruit__mark .h-ic { width: 20px; height: 20px; }
.gs-recruit__copy { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.gs-recruit__copy small { color: var(--active-purple-ink); }
.gs-recruit__copy strong { font-size: 14px; }
.gs-recruit__copy em { font-style: normal; font-size: 12px; color: var(--muted); }
.gs-go { flex: none; color: var(--muted); font-size: 18px; }
.gs-apps {
  padding: 12px; border-radius: 15px; display: flex; flex-direction: column; gap: 10px;
  border: 1px solid color-mix(in srgb, var(--purple) 45%, var(--line));
  background: linear-gradient(145deg, var(--active-purple-bg), var(--panel2));
}
.gs-apps header { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.gs-apps header small { color: var(--active-purple-ink); }
.gs-apps header strong { display: block; font-size: 14px; }
.gs-apps__n { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; background: var(--purple-solid); color: var(--on-purple); font-weight: 800; font-size: 12px; }
.gs-app { display: grid; grid-template-columns: 36px minmax(0, 1fr); gap: 10px; }
.gs-app b { display: block; font-size: 13.5px; }
.gs-app small { display: block; text-transform: none; letter-spacing: 0; font-weight: 600; }
.gs-app__acts { display: flex; gap: 6px; margin-top: 7px; flex-wrap: wrap; }
.gs-av {
  width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center;
  background: var(--elev); color: var(--ink); font-weight: 800; font-size: 13px;
}
.gs-row {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left; cursor: pointer;
  padding: 12px; border: 1px solid var(--line); border-radius: 13px; background: var(--panel2);
  color: var(--ink); font: inherit;
}
.gs-row > .h-ic { width: 18px; height: 18px; flex: none; color: var(--active-green-ink); }
.gs-row > span:nth-child(2) { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.gs-row b { font-size: 13.5px; }
.gs-row small { font-size: 11.5px; color: var(--muted); }
.gs-card { padding: 13px; border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel2); display: flex; flex-direction: column; gap: 9px; }
.gs-card p { margin: 0; font-size: 13px; color: var(--muted); }
.gs-outsider__host { display: flex; align-items: center; gap: 10px; }
.gs-outsider__host small { display: block; font-size: 10px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.gs-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.gs-chips > span, .gs-chips .h-needs__c {
  padding: 5px 9px; border-radius: 8px; font-size: 11px; font-weight: 800;
  background: linear-gradient(var(--green-dim), var(--green-dim)), var(--panel2); color: var(--active-green-ink);
}
.gs-muted { font-size: 12px !important; }

/* (The `.gs-stars` / `.gs-star*` card RETIRED 2026-10-04: the published Three Stars card is `statsws.css`'s `.three-stars`.) */

/* Roster: each role in its own card; the team key above. */
.gs-teamkey { display: flex; gap: 14px; align-items: center; font-size: 12.5px; color: var(--muted); }
.gs-teamkey span { display: inline-flex; align-items: center; gap: 6px; }
.gs-teamkey b { color: var(--ink); }
.gs-teamkey i { width: 9px; height: 9px; border-radius: 50%; border: 1px solid var(--line2); }
.gs-teamkey i.is-light { background: var(--ink); }
.gs-teamkey i.is-dark { background: var(--brand-charcoal); }
.gs-rgroup { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel2); overflow: hidden; }
.gs-rgroup__head { display: flex; align-items: center; justify-content: space-between; padding: 11px 12px; margin: 0 !important; border-bottom: 1px solid var(--line); }
.gs-rgroup__head .h-sec-label { margin: 0; font-family: var(--font-display); font-weight: 400; font-size: 15px; letter-spacing: .04em; }
.gs-rgroup .h-mrow { padding: 10px 12px; border-bottom: 1px solid var(--line); margin: 0; background: none; border-radius: 0; }
.gs-rgroup .h-mrow:last-child { border-bottom: 0; }
.gs-rgroup .h-mrow__av { width: 36px; height: 36px; border-radius: 50%; }
.gs .h-att { border-radius: var(--r-pill); font-weight: 800; }
.gs .h-att.is-yes { border-color: var(--green); color: var(--active-green-ink); }
.gs .h-att.is-no { border-color: var(--danger); color: var(--danger); }

/* Info: one list, a hairline between rows; the game code dashed. */
.gs-info > .h-mrow, .gs-info .h-grp > .h-mrow { padding: 12px 13px; }
.gs-info { display: flex; flex-direction: column; gap: 10px; }

/* The manager drawer's first line: which Space these tools act on. */
.h-mdrawer__for {
  margin: 0 2px 10px; font-size: 12px; font-weight: 700; color: var(--muted);
  overflow-wrap: anywhere;
}

.h-dsec { font-size: 11px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin: 16px 2px 9px; }
.h-dsec:first-child { margin-top: 2px; }

/* Space hero */
.h-shero {
  border-radius: var(--r-lg);
  padding: 16px;
  color: var(--on-colour);
  position: relative;
  overflow: hidden;
  margin-bottom: 12px;
  display: flex;
  align-items: center;
  gap: 14px;
}
.h-shero::before { content: ""; position: absolute; inset: 0; background: linear-gradient(135deg, var(--veil-photo-top), var(--veil-photo-base)); }
.h-shero > * { position: relative; z-index: 1; }
.h-shero__eyebrow { font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; opacity: .85; }
.h-shero__name { font-size: 20px; font-weight: 800; font-family: var(--font-head); margin: 3px 0 2px; line-height: 1.1; text-shadow: var(--sh-ink-on-photo); }
.h-shero__sub  { font-size: 12px; opacity: .9; text-shadow: var(--sh-ink-on-photo); }
.h-shero__role {
  position: absolute; top: 13px; right: 13px; background: var(--chip-on-photo);
  backdrop-filter: blur(4px); font-size: 10px; font-weight: 800; padding: 3px 9px; border-radius: var(--r-pill);
}

/* Next-up callout */
.h-nextup {
  background: linear-gradient(135deg, rgba(var(--green-rgb), .14), rgba(var(--green-rgb), .03));
  border: 1px solid rgba(var(--green-rgb), .34); border-radius: var(--r-lg);
  padding: 13px; margin-bottom: 13px;
}
.h-nextup__label { font-size: 10px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--green); }
.h-nextup__name  { font-weight: 800; font-size: 15px; margin: 3px 0 1px; }
.h-nextup__meta  { font-size: 12px; color: var(--muted); }

/* ---------------------------------------------------------------------------
   16. STAT TILES
   ------------------------------------------------------------------------ */
.h-stats { display: flex; gap: 8px; margin-bottom: 13px; }
.h-stat  { flex: 1; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md); padding: 10px; text-align: center; }
.h-stat__v { font-size: 19px; font-weight: 800; font-family: var(--font-head); }
.h-stat__l { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; font-weight: 700; margin-top: 1px; }

/* ---------------------------------------------------------------------------
   17. ROSTER ROWS + RSVP PILLS
   ------------------------------------------------------------------------ */
.h-roster { display: flex; flex-direction: column; }
.h-rrow { display: flex; align-items: center; gap: 11px; padding: 10px 2px; border-top: 1px solid var(--line); }
.h-rrow__av {
  width: 34px; height: 34px; border-radius: 50%; overflow: hidden; flex: none;
  display: grid; place-items: center; color: var(--on-colour); font-weight: 800; font-size: 12px;
}
.h-rrow__name { flex: 1; min-width: 0; font-size: 13.5px; font-weight: 600; }
.h-rrow__tag  { font-size: 10px; color: var(--muted); font-weight: 600; margin-left: 6px; }

.h-rsvp {
  font-size: 10px;
  font-weight: 800;
  border-radius: var(--r-lg);
  padding: 13px;
  text-transform: uppercase;
  letter-spacing: .02em;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 12px 0 4px;
  background: var(--panel);
  border: 1px solid var(--line);
}
/* EVERY WASH ON AN OPAQUE --panel, so a pill dropped into a tinted row is the
   same colour as one dropped into a plain one — the stacking that made the
   game sheet's In/Out chips 4.13:1 (08-18d). And `--out` mixed its wash from a
   hardcoded `rgba(255,90,90,…)`, which is neither theme's danger colour and
   could not flip; `--role` did the same with a hardcoded amber. Both now use
   the triples, and `--out` drops from .16 to .12 because at .16 the light ink
   lands on 4.24 and the bar is 4.5. (08-18e) */
.h-rsvp--in {
  background: linear-gradient(rgba(var(--green-rgb), .16), rgba(var(--green-rgb), .16)), var(--panel);
  color: var(--green);
}
.h-rsvp--out {
  background: linear-gradient(rgba(var(--danger-rgb), .12), rgba(var(--danger-rgb), .12)), var(--panel);
  color: var(--danger);
}
.h-rsvp--noreply { background: var(--panel2); color: var(--faint); border: 1px solid var(--line2); }
.h-rsvp--role {
  background: linear-gradient(rgba(var(--warn-rgb), .16), rgba(var(--warn-rgb), .16)), var(--panel);
  color: var(--warn);
}

/* ---------------------------------------------------------------------------
   18. MANAGER TOOLS + MONEY ROWS + RECRUIT CARD
   ------------------------------------------------------------------------ */
.h-mtools { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); margin-bottom: 14px; }
/* SYS-16, THE CHEVRON HALF. `.h-mtool` WAS ONE CLASS SERVING TWO LAYOUTS AND
   THE 08-17z MERGE KEPT THE WRONG ONE.

   There are two shapes here and there always were: a two-up TILE (icon over
   title, inside the `.h-mtools` grid) and a full-width ROW (title + sub on the
   left, chevron on the right). When the duplicate definitions in this file and
   hub.css were merged, the TILE body won outright — `flex-direction: column`
   with `align-items: center` — and every app surface that renders the ROW has
   been wearing the tile's layout since.

   THE COUNT IS THE ARGUMENT: `gamesheet.js:704`, `room.js:979` and
   `spaces.js:836` all emit the ROW (`__main` + `__go`). The TILE is emitted in
   exactly one place, `kit.html:393` — the pattern-library page. The app has
   been rendering the documentation's layout, not its own.

   AND THE LEFTOVERS IN hub.css PROVE IT: `.h-mtool__main { flex: 1 }` and
   `.h-mtool__go { flex: none }` are flex-ROW child properties. Under
   `flex-direction: column` they do nothing, which is why `Manager Tools.png`
   shows the `›` sitting CENTRED UNDERNEATH the text instead of at the row's
   right edge.

   `text-align: left` was already declared and was already dead: `align-items:
   center` shrink-wraps the text block to its content, so there is no box left
   for the text to be left-aligned inside. A declaration that cannot bite is
   not a guard, and this one read like one for nineteen days.

   THE ROW IS NOW THE DEFAULT because it is what the app emits; the TILE is a
   container override, so the shape is chosen by the thing that knows which
   shape it wants. The override's values are byte-identical to what shipped, so
   kit.html renders exactly as it does today. */
/* `SYS-26`(b) — KARL'S "ODD GREY LINE ABOVE EACH BUTTON", AND IT IS ONE
   DECLARATION RATHER THAN THE STRUCTURE.
   ⚠️ IT IS NOT TWO ADJACENT BORDERS AND IT IS NOT A BOX PER ROW — I checked the
   rule before building to that reading. This row has `border: 0` and a single
   `border-top`, which is one hairline, correctly. **What deforms it is the
   `border-radius: 12px` sitting beside it: a border-top on a rounded box
   follows the radius, so the divider CURLS UP AT BOTH ENDS** and reads as the
   top edge of a broken container instead of a line between two rows. That is
   exactly what `Manager Tools.PNG` shows.
   The radius drew nothing else — there is no background and no full border for
   it to round — so it goes, and the divider becomes the straight hairline it
   was always meant to be.
   🪞 I FIRST WROTE A SECOND CHANGE HERE — inset the divider — AND BACKED IT OUT
   BEFORE COMMITTING: the rule I added did not actually inset anything, and the
   sentence describing it would have outlived its own code by one minute. That
   is the defect I have spent two days filing against other people's files.
   Whether the divider should be inset is a grouping decision and belongs with
   the pattern, not with this.
   ⛔ THE GROUPING QUESTION IS NOT ANSWERED HERE. Whether these rows want one
   container, section headers and a larger section gap is the pattern Vera's
   Settings mock defines, and Recruitment is sequenced behind that mock for
   exactly this reason. This is the part that needs no pattern to be wrong. */
.h-mtool {
  background: none;
  border: 0;
  padding: 12px 2px;
  cursor: pointer;
  display: flex;
  flex-direction: row;
  gap: 11px;
  text-align: left;
  align-items: center;
  width: 100%;
  border-top: 1px solid var(--line);
  font: inherit;
  color: var(--ink);
}
/* THE TILE, and only inside the grid that asks for it. */
.h-mtools > .h-mtool { flex-direction: column; align-items: center; }
.h-mtool__ic { width: 32px; height: 32px; border-radius: var(--r-sm); display: grid; place-items: center; background: var(--green-dim); color: var(--green); }
.h-mtool__ic svg { width: 17px; height: 17px; }
.h-mtool--warn .h-mtool__ic { background: rgba(var(--warn-wash-rgb),.16); color: var(--warn); }
.h-mtool__t {
  font-weight: 700;
  font-size: 13.5px;
  overflow-wrap: anywhere;
}
.h-mtool__s {
  font-size: 11.5px;
  color: var(--muted);
  margin-top: 1px;
  overflow-wrap: anywhere;
}

.h-money { display: flex; align-items: center; gap: 11px; padding: 11px 12px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md); margin-bottom: 8px; }
.h-money__amt { font-weight: 800; font-size: 14px; font-family: var(--font-head); flex: none; }
.h-money__who { flex: 1; min-width: 0; }
.h-money__st  { font-size: 10px; font-weight: 800; text-transform: uppercase; border-radius: var(--r-pill); padding: 3px 9px; }
.h-money__st--paid   { background: rgba(var(--green-rgb), .16); color: var(--green); }
.h-money__st--unpaid { background: rgba(var(--warn-wash-rgb),.18); color: var(--warn); }

.h-recruit {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 4px 0 10px;
  margin-bottom: var(--sp-3);
  border-top: 1px solid var(--line);
}
.h-recruit__top { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.h-recruit__ic  { width: 34px; height: 34px; border-radius: var(--r-sm); display: grid; place-items: center; flex: none; }
.h-recruit__n   { font-weight: 700; font-size: 13.5px; }
.h-recruit__s   { font-size: 11px; color: var(--muted); }
.h-feepill { font-size: 10px; font-weight: 800; border-radius: var(--r-pill); padding: 3px 9px; margin-left: auto; flex: none; }

/* ---------------------------------------------------------------------------
   19. CHAT
   ------------------------------------------------------------------------ */
.h-chat { display: flex; flex-direction: column; gap: var(--sp-3); }
.h-bubble { max-width: 80%; padding: 9px 12px; border-radius: var(--r-lg); font-size: 13px; line-height: 1.4; }
.h-bubble__who { font-size: 10.5px; font-weight: 700; color: var(--muted); margin-bottom: 2px; }
.h-bubble--them { background: var(--panel); border: 1px solid var(--line); align-self: flex-start; border-bottom-left-radius: 5px; }
.h-bubble--me   { background: var(--green); color: var(--on-green); align-self: flex-end; border-bottom-right-radius: 5px; }
.h-chatin { display: flex; gap: 8px; margin-top: 12px; }
.h-chatin input {
  flex: 1; background: var(--panel2); border: 1px solid var(--line2); border-radius: var(--r-pill);
  padding: 10px 14px; color: var(--ink);
  font-size: 16px; /* SYS-07: was 13px; under 16 the field zooms iOS */
}
.h-chatin input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px rgba(var(--green-rgb), .25); } /* `S2-109` */
.h-chatin__send {
  width: 40px; height: 40px; border-radius: 50%; background: var(--green); color: var(--on-green);
  border: none; display: grid; place-items: center; cursor: pointer; flex: none;
}
.h-chatin__send svg { width: 18px; height: 18px; }

/* ---------------------------------------------------------------------------
   20. APPLY / JOIN STEP SCREENS
   ------------------------------------------------------------------------ */
.h-stepq   { font-size: var(--fs-lg); font-weight: 800; font-family: var(--font-head); margin: 4px 0 3px; }
.h-stepsub { font-size: 12.5px; color: var(--muted); margin-bottom: 15px; }
.h-opt {
  display: flex; align-items: center; gap: 12px; background: var(--panel);
  border: 1.5px solid var(--line2); border-radius: var(--r-lg); padding: 13px;
  margin-bottom: 10px; cursor: pointer; width: 100%; text-align: left;
  transition: transform var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.h-opt:active { transform: scale(.977); }
.h-opt.is-sel { border-color: var(--green); background: var(--green-dim); }
.h-opt__ic { width: 38px; height: 38px; border-radius: var(--r-md); display: grid; place-items: center; flex: none; background: var(--panel2); }
.h-opt.is-sel .h-opt__ic { background: var(--green); color: var(--on-green); }
/* Name over description, not name-then-description on one line.
   Both were inline spans, so on a phone they ran together as
   "Group OrganizerManage the groups you're already in" — no space, because
   there is no whitespace between the tags in the template and inline
   elements do not create one. Karl caught it in the setup wizard. Blocks are
   what this component always meant to be. */
.h-opt__n { display: block; font-weight: 700; font-size: 14px; }
.h-opt__s { display: block; margin-top: 2px; font-size: 11.5px; color: var(--muted); line-height: 1.35; }

/* "Recommended", on the option we picked for them. */
.h-opt__rec {
  display: inline-block; margin-left: 6px; padding: 1px 6px; border-radius: var(--r-pill);
  background: var(--green); color: var(--on-green);
  font-size: 10px; font-weight: 700; letter-spacing: .02em; vertical-align: 1px;
}
.h-opt__chk {
  margin-left: auto; width: 22px; height: 22px; border-radius: 50%;
  border: 1.5px solid var(--line2); flex: none; display: grid; place-items: center; color: transparent;
}
.h-opt.is-sel .h-opt__chk { background: var(--green); border-color: var(--green); color: var(--on-green); }

.h-feebox { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 14px; margin: 6px 0 15px; }
.h-feebox__row { display: flex; justify-content: space-between; align-items: center; padding: 7px 0; font-size: 13px; }
.h-feebox__row + .h-feebox__row { border-top: 1px solid var(--line); }
.h-feebox__k { color: var(--muted); }
.h-feebox__v { font-weight: 700; }
.h-feebox__total { font-size: var(--fs-lg); font-weight: 800; font-family: var(--font-head); }

.h-success { text-align: center; padding: 26px 10px; }
.h-success__ic {
  width: 66px; height: 66px; border-radius: 50%; background: var(--green); color: var(--on-green);
  display: grid; place-items: center; margin: 0 auto 14px; box-shadow: 0 0 22px rgba(var(--green-rgb), .45);
}
.h-success__ic svg { width: 32px; height: 32px; }
.h-success__h { font-size: 19px; font-weight: 800; font-family: var(--font-head); }
.h-success__p { font-size: 13px; color: var(--muted); margin: 6px auto 0; max-width: 260px; line-height: 1.5; }

/* ---------------------------------------------------------------------------
   21. SETTINGS ROWS + PROFILE CARD + STAFF CARD
   ------------------------------------------------------------------------ */
.h-setrow { display: flex; align-items: center; gap: 12px; padding: 14px 2px; border-top: 1px solid var(--line); cursor: pointer; }
.h-setrow__ic {
  width: 34px; height: 34px; border-radius: var(--r-sm); background: var(--panel2);
  border: 1px solid var(--line2); display: grid; place-items: center; flex: none; color: var(--muted);
}
.h-setrow__ic svg { width: 17px; height: 17px; }
.h-setrow__body { flex: 1; }
.h-setrow__t { font-size: 14px; font-weight: 600; }
.h-setrow__s { font-size: 11px; color: var(--muted); }
.h-setrow__chev { color: var(--faint); }

.h-profcard { display: flex; align-items: center; gap: 12px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 13px; margin-bottom: 12px; }
.h-profcard__av { width: 56px; height: 56px; border-radius: var(--r-lg); overflow: hidden; flex: none; border: 1.5px solid var(--green); }
.h-profcard__name { font-weight: 800; font-size: var(--fs-lg); font-family: var(--font-head); }
.h-profcard__meta { color: var(--muted); font-size: 12px; }

.h-staffcard {
  background: linear-gradient(135deg, rgba(var(--brand-purple-rgb),.18), rgba(var(--brand-purple-rgb),.04));
  border: 1px solid var(--brand-purple); border-radius: var(--r-lg);
  padding: 13px; margin-bottom: 12px; position: relative;
}
.h-staffcard__t { font-weight: 800; font-size: 14px; display: flex; align-items: center; gap: 7px; font-family: var(--font-head); }
.h-staffcard__t svg { width: 17px; height: 17px; color: var(--brand-purple); }

/* ---------------------------------------------------------------------------
   22. FORM FIELDS  (used by the custom login — T-003)
   ------------------------------------------------------------------------ */
.h-field { margin-bottom: 12px; }
/* CONTRAST RULE (site-wide): a label a member has to READ never uses --faint.
   --faint (#6D6E71) on the dark background is ~3.8:1, which fails WCAG AA for
   small text and disappears entirely under sun glare on a phone at a rink.
   --muted (#9a9b9e) is ~7:1. --faint is for decorative and tertiary marks only. */
.h-label {
  display: block; font-size: 11.5px; letter-spacing: .07em; text-transform: uppercase;
  color: var(--muted); font-weight: 700; margin-bottom: 6px;
}
.h-input {
  width: 100%; background: var(--panel2); border: 1px solid var(--line2);
  border-radius: var(--r-md); color: var(--ink); padding: 13px 14px; font-size: 16px;
  transition: border-color var(--t-fast);
}
/* ===========================================================================
   `SYS-02` — DATE AND TIME FIELDS RUN PAST THE EDGE OF THE SHEET, AND KARL HAS
   NOW REPORTED IT TWICE: *"All date fields throughout the app are still having
   this issue"* — with the fix in his own words, *"the field just needs to be
   the same length as the where and what is it called fields."*

   ⚠️ WHAT I CAN AND CANNOT PROVE, STATED FIRST BECAUSE IT DECIDES HOW MUCH TO
   TRUST THIS RULE. `input[type=datetime-local]` is a NATIVE control and mobile
   Safari sizes it from its own formatted text, ignoring a `width: 100%` that
   would shrink it below that intrinsic size. THAT IS A WEBKIT BEHAVIOUR AND MY
   HARNESS IS CHROMIUM: measured there, all five fields in a sheet come out
   identical at 288px on a 320 viewport with no overflow, which is exactly what
   Karl's screenshot does NOT show. **So I cannot reproduce this defect with the
   instrument I have, and I am not going to claim I verified it.** What is below
   targets the known WebKit mechanism and is a no-op everywhere the bug is
   absent — Chromium widths are byte-identical before and after.

   `min-width: 0` is the load-bearing line: it is what lets a native control
   shrink to its container instead of to its content. `max-width: 100%` is the
   ceiling that holds even if a future control reports a larger intrinsic size,
   and `text-align: left` answers the other half of what Karl saw — his words,
   *"you end up Center aligning the text instead of keeping it aligned the same
   way as the other text fields."*

   🛑 AND THE ANTI-PATTERN HE NAMED HIMSELF IS NOT TAKEN: this does not centre
   the field harder to hide the overflow. It makes it the same width as its
   neighbours, which is what he asked for.
   ========================================================================= */
.h-input[type="date"],
.h-input[type="time"],
.h-input[type="datetime-local"],
.h-input[type="month"],
.h-input[type="week"] {
  min-width: 0;
  max-width: 100%;
  text-align: left;
}
/* ===========================================================================
   `SYS-02`, THE HALF THAT WAS MISSING, AND THE REASON IT WAS MISSING IS WRITTEN
   TWENTY LINES ABOVE: my harness is Chromium and this is WebKit-only.

   🔬 ON iOS THE VALUE INSIDE A DATE INPUT IS NOT DRAWN BY THE INPUT. It is
   drawn by `::-webkit-date-and-time-value`, a pseudo-element, and `text-align`
   on the input does not reach it. So the rule above set the field's width
   correctly and the TEXT inside it stayed centred - which is precisely what
   Karl reported and precisely the half I could not see: "you end up Center
   aligning the text instead of keeping it aligned the same way as the other
   text fields."
   🔑 Grepped the whole of `assets/css` before writing this: neither
   `-webkit-date-and-time-value` nor `-webkit-datetime-edit` appears ANYWHERE in
   this tree. Nothing has ever told iOS not to centre that value.

   ⚠️ AND NOBODY MAY CALL THIS GREEN FROM A CONTAINER. It is invisible in
   Chromium by construction - the pseudo-element does not exist there, so the
   rule is inert and my measurements before and after are byte-identical. That
   is not evidence it works; it is evidence it is safe. KARL'S PHONE IS THE ONLY
   INSTRUMENT THAT CAN CLOSE THIS.

   🛑 `-webkit-appearance: none` IS DELIBERATELY NOT HERE. It would also strip
   the native picker's own chrome on iOS, which is a bigger change than the one
   Karl asked for, and `min-width: 0` above is already the line that lets the
   control shrink below its intrinsic width. If his re-check shows the field
   STILL wider than its neighbours, that is the next lever - and it is a
   separate decision, not a line to add pre-emptively.
   ========================================================================= */
.h-input[type="date"]::-webkit-date-and-time-value,
.h-input[type="time"]::-webkit-date-and-time-value,
.h-input[type="datetime-local"]::-webkit-date-and-time-value,
.h-input[type="month"]::-webkit-date-and-time-value,
.h-input[type="week"]::-webkit-date-and-time-value {
  text-align: left;
  /* THE WIDTH HALF, THIRD REPORT. The value element is what iOS sizes the host
     from, so giving IT the full width stops the host being sized by its
     content. Tried FIRST because it costs nothing anywhere else: in Chromium
     this pseudo-element does not exist and the rule is inert. */
  width: 100%;
}

/* ===========================================================================
   `SYS-02` / `GAME-07`, THE WIDTH HALF — AND THE SCREENSHOT IS WHY THIS LINE
   EXISTS AT ALL.

   Karl's phone, 2026-09-16: "What is it called?" and "Where?" are CONTAINED;
   "What day?", "Starts at" and "Ends at" all OVERFLOW. Same wrapper class, same
   input class, same parent - the only difference is the `type` attribute.
   🔑 AND "ENDS AT" IS EMPTY AND STILL OVERFLOWS, WHICH KILLS THE CONTENT-WIDTH
   EXPLANATION DEAD. An empty control has no content to be sized by, so this is
   the native control's own intrinsic width, and `min-width: 0` on the host did
   not defeat it across three reports. That is also why I do not expect the
   pseudo-element width above to be enough on its own, and say so rather than
   letting a fourth report discover it.

   ⚖️ `-webkit-appearance: none` WAS AUTHORISED AND I DECLINED IT TWICE, FOR A
   REASON THAT HAS NOT CHANGED: it strips the native picker chrome. SO IT IS
   SCOPED TO THE ONLY BROWSER THAT HAS THE BUG. `-webkit-touch-callout` is
   supported by iOS Safari and by nothing else, so this block is skipped
   entirely in desktop Chrome - which never had the overflow and DOES lose its
   calendar-picker indicator to an unscoped reset. The cost we were both worried
   about never lands on the browser that would have paid it.
   🛑 THE PICKER STILL OPENS ON iOS. `appearance` changes how the control is
   PAINTED, not what tapping it does, and the field's own background, border,
   radius and padding are `.h-input`'s already - so there is nothing left for
   the native appearance to contribute here.
   ⚠️ AND NOBODY MAY CALL THIS GREEN FROM A CONTAINER, FOR THE THIRD TIME.
   Measured in Chromium before and after: the fields are byte-identical. That is
   evidence it is SAFE, not evidence it works. Karl's phone is the instrument.
   ========================================================================= */
@supports (-webkit-touch-callout: none) {
  .h-input[type="date"],
  .h-input[type="time"],
  .h-input[type="datetime-local"],
  .h-input[type="month"],
  .h-input[type="week"] {
    -webkit-appearance: none;
    appearance: none;
  }
}

/* ===========================================================================
   `SYS-02` REACHES `type="number"` TOO, AND IT TOOK KARL'S CAMERA TO FIND IT.
   The row is called "DATE input fields extend off screen", so a number input
   was never in scope - and the Skaters counter overflows in the same photo.

   🔬 THE MECHANISM IS THE ONE THE DATE FIELDS HAVE, WEARING A FLEX BOX:
   `.h-need__c` is `flex: 0 0 72px` inside `.h-need__step`, but a flex item's
   default `min-width: auto` is its MIN-CONTENT size, and a native number
   control on iOS reports one larger than 72px. The basis is then ignored, the
   item refuses to shrink, and the row pushes past the sheet. `min-width: 0` is
   what lets a basis mean what it says.

   ⛔ WIDTH ONLY, NO `text-align`, AND THAT OMISSION IS DELIBERATE. The block
   above sets `text-align: left` on the date types; doing the same here would be
   (0,2,0) against `.h-need__c`'s (0,1,0) and would silently un-centre Tony's
   counter, which is centred on purpose between its two nudgers. A fix that
   breaks a neighbouring control is not a fix.
   📊 MEASURED IN CHROMIUM, before and after: counter 72px -> 72px, sheet
   scrollWidth 320 -> 320. Inert here, like the rest of this block.
   ========================================================================= */
.h-input[type="number"] { min-width: 0; max-width: 100%; }
/* ===========================================================================
   `SYS-08` — "THE INNER GREY FOCUS RING ON INPUTS IS UNWANTED." Karl saw a
   ring nobody designed: inputs were getting HALF the two-tone focus ring.

   🔬 THE MECHANISM, MEASURED RATHER THAN GUESSED. `base.css`'s `:focus-visible`
   paints BOTH an `outline` (inner) and a `box-shadow` (outer). The rule below
   used to be `.h-input:focus { outline: none; … }`, which is specificity
   (0,2,0) against `:focus-visible`'s (0,1,0) — so it WON on `outline` and
   removed the inner ring, and could not touch `box-shadow`, which stayed.
   **The result was the outer ring with no inner one: an orphaned halo on every
   input, on mouse click as well as on keyboard.**

   ⚖️ THE FIX KEEPS THE ACCESSIBILITY WORK OF 2026-09-04 AND STILL GIVES KARL
   WHAT HE ASKED FOR, because the two cases are genuinely different:
     · POINTER focus  → no ring at all, just the green border. What he wants.
     · KEYBOARD focus → the FULL two-tone ring, unchanged and still measured at
       4.25:1 worst case. Removing it here would undo a WCAG 2.2 fix to satisfy
       a complaint that was never about keyboard users.
   `:focus-visible` is exactly the switch for that, and the old rule was
   suppressing half of it by accident rather than by design. */
/* 🔁 `S2-109`, 2026-09-23 (Rita) — THE PAIR ABOVE WAS RIGHT ABOUT BUTTONS AND
   COULD NEVER APPLY TO A TEXT FIELD. Measured in Chromium: tapping a text or
   search box MATCHES `:focus-visible` (a tapped button does not), because a
   field that takes typing is always a keyboard target. So
   `:focus:not(:focus-visible)` never matched a field, and every tap drew the
   full detached ring: an inner outline, a gap, and an outer halo, squared off
   inside the box's own rounded border. That is what Karl called ugly.
   ✅ KARL'S RULING: the rounded border the field already has is the indicator,
   and it glows. One outline, not two. `outline: none` and a `box-shadow` that
   REPLACES the global one (so it follows the field's own radius, pill or not).
   ⚖️ STILL A FOCUS INDICATOR, which is the part the 2026-09-04 accessibility
   fix protects: the border turns the accent colour and a 3px glow is drawn
   around it, on keyboard focus as well as a tap. Buttons, chips and every other
   control keep base.css's measured two-tone ring untouched. */
.h-input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px rgba(var(--green-rgb), .25); }
/* A FLUSH FIELD HAS NO BORDER TO GLOW (it sits inside a `.h-grp` row), so it
   gets an attached underline instead of a square halo around a borderless box. */
.h-input--flush:focus { box-shadow: inset 0 -2px 0 var(--green); }
.h-input::placeholder { color: var(--faint); }
.h-input.is-error { border-color: var(--danger); }
.h-help  { font-size: 11.5px; color: var(--muted); margin-top: 5px; }
.h-error { font-size: 12.5px; color: var(--danger); margin: 0 0 10px; min-height: 1em; }
.h-divider { display: flex; align-items: center; gap: 10px; color: var(--faint); font-size: 11px; margin: 16px 0; }
.h-divider::before, .h-divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }

/* ---------------------------------------------------------------------------
   23. GREETING / SECTION HEADER
   ------------------------------------------------------------------------ */
.h-greet { font-size: var(--fs-xl); margin: 2px 0 1px; font-family: var(--font-head); font-weight: 800; }
.h-greet .h-greet__name { color: var(--green); }
.h-wsec  { margin: 14px 0 7px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.h-wsec__link { font-size: 11px; color: var(--green); font-weight: 700; cursor: pointer; background: none; border: 0; }
/* The section label itself. index.html and the hub both emit `.h-sec-label`
   and it was never defined — it has been rendering as unstyled 16px body text.
   The prototype's `.wsec .wt` uses --faint; a section label is something a
   member READS, so it takes --muted here. That is the contrast rule in
   base.css, not a redesign. */
/* `.h-sec-label` MOVED TO base.css IN 08-17z. It was defined in both files with different values,
   so it rendered one way in the app and another on every page that does not load
   this file — including kit.html, the page that documents it. */
/* A horizontally-scrolling rail with no visible scrollbar (the spaces rail). */
.h-noscroll { scrollbar-width: none; -ms-overflow-style: none; }
.h-noscroll::-webkit-scrollbar { display: none; }

/* ---------------------------------------------------------------------------
   24. ICONS  (consumed via data-icon + H365Icons.inject())
   Most library icons ship viewBox-only, so they are painted here.
   ------------------------------------------------------------------------ */
.h-ic {
  display: inline-grid; place-items: center; flex: none;
  width: 20px; height: 20px; color: inherit;
}
/* Sizing applies to EVERY icon. Paint does not — see below. */
.h-ic svg { width: 100%; height: 100%; }

/* PAINT, and only for the icons that need painting.
   ---------------------------------------------------------------------------
   Root cause found and measured by Kevin (Website seat), 08-06. This rule used
   to paint every icon and then try to un-paint the self-painted ones with
   `fill: revert`. That never worked, in EITHER theme — light is only where it
   is obvious. `revert` rolls a property back to the USER-AGENT origin; it does
   not mean "as if no author CSS applied". There is no UA declaration for
   `fill`, and `fill` is an INHERITED property, so it resolved to the parent's
   computed value — an HTML <span>, whose `fill` is the initial value: BLACK.
   And a CSS declaration of any origin outranks a presentation attribute, so
   each icon's own fill="currentColor" never got a look in.

   The fix is to never touch them: EXCLUDE the self-painted icons from the paint
   rule, so their own attributes govern. The old selector also over-matched on
   [data-icon^="pro"], which forced `programs` and `proShop` — ordinary outline
   icons — to black as well.

   THIS LIST MUST MATCH H365Icons.SELF_PAINTED in assets/js/icons.js EXACTLY.
   Enumerated rather than prefix-matched for that reason: a prefix cannot stay
   in sync with a JS array, and the last time these two drifted the result was a
   black jersey on dark green. `tests/icons.test.mjs` asserts the two lists are
   identical, so drift fails the build rather than shipping.
   ------------------------------------------------------------------------ */
.h-ic:not(
  [data-icon="sun"], [data-icon="moon"],
  [data-icon="jersey"], [data-icon="refJersey"],
  [data-icon="ppStampNet"], [data-icon="ppNetFront"], [data-icon="ppStampBumper"],
  [data-icon="proBadge"], [data-icon="proMini"], [data-icon="proStar"],
  [data-icon="proCrown"], [data-icon="proLock"], [data-icon="proCheck"],
  [data-icon="staffBadge"], [data-icon="staffMini"], [data-icon="staffMiniH"]
) svg {
  fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
}
.h-ic--xs { width: 14px; height: 14px; }
.h-ic--sm { width: 17px; height: 17px; }
.h-ic--md { width: 21px; height: 21px; }
.h-ic--lg { width: 24px; height: 24px; }
.h-ic--xl { width: 32px; height: 32px; }


/* ---------------------------------------------------------------------------
   25. DATE OF BIRTH — three fields, year first
   Year leads because it is the one a member knows instantly and the one that
   makes a calendar painful. Never one box; never a month-stepping calendar.
   ------------------------------------------------------------------------ */
.h-dob__row { display: grid; grid-template-columns: 1fr 1.3fr .9fr; gap: 8px; }
.h-dob__f { min-width: 0; }
.h-dob .h-input { width: 100%; }
.h-dob select.h-input {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
                    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 16px) 55%, calc(100% - 11px) 55%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 30px;
}
.h-dob__echo { min-height: 1.2em; margin-top: 7px; transition: color var(--t-fast); }
.h-dob__echo.is-ok { color: var(--green); font-weight: 600; }
@media (max-width: 360px) { .h-dob__row { grid-template-columns: 1fr 1fr; } .h-dob__f--d { grid-column: 1 / -1; } }

/* ---------------------------------------------------------------------------
   26. HERO LOCKUP — wordmark + player/goalie silhouette
   Takes the SEMANTIC accent, so green in dark and Power Play Purple in light.
   ------------------------------------------------------------------------ */
.h-lockup { display: block; color: var(--green); cursor: pointer; background: none; border: 0; padding: 0; }
.h-lockup svg { width: 100%; height: auto; display: block; }
.h-lockup.is-flip svg { animation: h-lockup-in .32s cubic-bezier(.2,.7,.2,1); }
@keyframes h-lockup-in {
  from { opacity: 0; transform: translateY(6px) scale(.97); }
  to   { opacity: 1; transform: none; }
}
.h-lockup:focus-visible { outline: 2px solid var(--green); outline-offset: 6px; border-radius: var(--r-md); }

/* Chrome keeps ONE identity: the mark never restyles on theme change there. */
.h-chrome .h-wordmark, .h-chrome .h-lockup { color: var(--green); }

/* ---------------------------------------------------------------------------
   27. MOTION
   The app should feel built, not static. Transitions are short and purposeful:
   they show WHERE something came from, they never make a member wait.

   Cost note: none of this touches the network. It is GPU compositing of
   transform and opacity only, so a bad rink connection is unaffected. What DOES
   suffer on an old phone is layout-triggering animation, which is why nothing
   here animates width, height, top or left.

   Off switch: `prefers-reduced-motion` is honoured automatically, and a member
   can force it from Settings, which sets data-motion="off" on <html>.
   ------------------------------------------------------------------------ */
@keyframes h-slide-in-right { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: none; } }
@keyframes h-slide-in-left  { from { opacity: 0; transform: translateX(-18px); } to { opacity: 1; transform: none; } }
@keyframes h-slide-out-left { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateX(-18px); } }
@keyframes h-rise           { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes h-pop            { 0% { transform: scale(.9); opacity: 0; } 60% { transform: scale(1.03); } 100% { transform: scale(1); opacity: 1; } }

.h-anim-in     { animation: h-slide-in-right .28s cubic-bezier(.2,.7,.2,1) both; }
.h-anim-in-rev { animation: h-slide-in-left  .28s cubic-bezier(.2,.7,.2,1) both; }
.h-anim-out    { animation: h-slide-out-left .18s cubic-bezier(.4,0,1,1) both; }
.h-anim-rise   { animation: h-rise .3s cubic-bezier(.2,.7,.2,1) both; }
.h-anim-pop    { animation: h-pop .32s cubic-bezier(.2,.8,.3,1) both; }

/* Stagger children so a list assembles instead of appearing. */
.h-stagger > * { animation: h-rise .34s cubic-bezier(.2,.7,.2,1) both; }
.h-stagger > *:nth-child(1) { animation-delay: .02s; }
.h-stagger > *:nth-child(2) { animation-delay: .06s; }
.h-stagger > *:nth-child(3) { animation-delay: .10s; }
.h-stagger > *:nth-child(4) { animation-delay: .14s; }
.h-stagger > *:nth-child(5) { animation-delay: .18s; }
.h-stagger > *:nth-child(n+6) { animation-delay: .22s; }

/* The lockup flip: the mark tips like a puck and lands. */
@keyframes h-lockup-flip {
  0%   { transform: rotateY(0) scale(1); opacity: 1; }
  45%  { transform: rotateY(-88deg) scale(.92); opacity: .25; }
  55%  { transform: rotateY(88deg) scale(.92); opacity: .25; }
  100% { transform: rotateY(0) scale(1); opacity: 1; }
}
.h-lockup { perspective: 900px; }
.h-lockup svg { transition: filter var(--t-fast); }
.h-lockup:hover svg { filter: drop-shadow(0 0 14px rgba(var(--green-rgb), .35)); }
.h-lockup:active svg { transform: scale(.97); }
.h-lockup.is-flip svg { animation: h-lockup-flip .55s cubic-bezier(.4,0,.2,1); }

/* One switch, two sources: the OS preference and the member's own setting. */
@media (prefers-reduced-motion: reduce) {
  .h-anim-in, .h-anim-in-rev, .h-anim-out, .h-anim-rise, .h-anim-pop,
  .h-stagger > *, .h-lockup.is-flip svg, .h-fadein, .sec.on { animation: none !important; }
}
html[data-motion="off"] *, html[data-motion="off"] *::before, html[data-motion="off"] *::after {
  animation-duration: .001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: .001ms !important;
  scroll-behavior: auto !important;
}


/* ---------------------------------------------------------------------------
   28. SELF-PAINTED ICONS ON TINTED SURFACES
   Most icons take currentColor and adapt. A handful carry their own paint
   (jersey, the masks, sun/moon, the Pro badges) and CANNOT. Dropping one into
   an accent-tinted tile gives you a black jersey on dark green — invisible.
   Any container holding one gets .is-selfpaint, which switches it to a neutral
   surface the icon's own colours can sit on.
   ------------------------------------------------------------------------ */
.h-qact__ic.is-selfpaint,
.h-tile__ic.is-selfpaint,
.h-mtool__ic.is-selfpaint,
.h-model__ic.is-selfpaint,
.h-attach__ic.is-selfpaint,
.h-na__ic.is-selfpaint,
.h-opt__ic.is-selfpaint {
  background: var(--panel2);
  border: 1px solid var(--line2);
  color: var(--ink);
}
.h-qact--primary .h-qact__ic.is-selfpaint { background: var(--elev); }

/* ---------------------------------------------------------------------------
   29. TOAST
   A status line, not a dialog: role="status", never takes focus, never blocks.
   Sits above the bottom nav so it does not cover the tab a member just used.
   ------------------------------------------------------------------------ */
.h-toast {
  position: absolute; left: 50%; bottom: calc(78px + env(safe-area-inset-bottom));
  transform: translate(-50%, 12px);
  z-index: 60; max-width: min(92%, 460px);
  background: var(--panel); border: 1px solid var(--line2);
  border-radius: var(--r-lg, 14px); box-shadow: var(--sh-2);
  padding: 11px 16px; font-size: 13px; color: var(--ink);
  opacity: 0; pointer-events: none;
  transition: opacity var(--t-fast), transform var(--t-fast);
}
.h-toast.is-open { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; cursor: pointer; }
.h-toast--act { display: flex; align-items: center; gap: 12px; }
.h-toast__act { margin-left: auto; flex: none; min-height: 32px; padding: 0 6px; border: 0; background: none;
  color: var(--green); font: inherit; font-weight: 800; cursor: pointer; }
.h-toast--bad  { border-color: var(--danger); }
.h-toast--good { border-color: var(--green); }
/* Neither good nor bad: "it worked, and here is a caveat". The dry-run notice
   is the one this exists for — "Test run, nothing was sent" is a SUCCESS the
   organiser must not read as a send. */
.h-toast--warn { border-color: var(--warn); }
@media (prefers-reduced-motion: reduce) { .h-toast { transition: opacity var(--t-fast); } }
html[data-motion="off"] .h-toast { transition: none; }

/* ---------------------------------------------------------------------------
   .h-iconbtn — a square, icon-only button.

   FOUND BY `tools/deadends.mjs` ON ITS FIRST RUN: this class was emitted by
   `gamesheet.js` (Manager tools) and `room.js` (Manage) and had NO RULE
   ANYWHERE. Both are real, visible, shipped surfaces, and both were rendering
   a default browser button — grey, boxy, system font, sitting next to the
   app's own chrome. Nothing errored, no test looked at it, and the only way it
   was ever going to be found was somebody opening that screen and squinting.

   Matched to `.h-room__switch`, which is the same idea with a label on it, so
   the two sit together rather than being two opinions about the same control.
   ------------------------------------------------------------------------ */
.h-iconbtn {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0;
  background: var(--panel2); border: 1px solid var(--line2); color: var(--muted);
  border-radius: var(--r-sm); cursor: pointer; font: inherit;
}
.h-iconbtn:hover { color: var(--ink); border-color: var(--green); }
.h-iconbtn .h-ic { width: 16px; height: 16px; }

/* ---------------------------------------------------------------------------
   8b. GROUP AVATARS

   `.h-gav` is the CONTAINER, and that division is the whole design:
   `h365Hub_GroupAvatar` draws a SQUARE with no rounding on purpose, so the
   caller's own class decides the size and the corner radius. One SVG string is
   a 42px disc in the spaces list, a 54px tile in the hub rail and a 44px
   rounded square in the room hero, and nothing about the picture changes.

   `overflow:hidden` is what does the clipping, so it is not optional — without
   it the gradient corners escape a border-radius and every group avatar in the
   app becomes a square.
   ------------------------------------------------------------------------ */
.h-gav { overflow: hidden; line-height: 0; }
.h-gav > svg { width: 100%; height: 100%; display: block; }

/* The room hero's avatar is a BUTTON for an admin — the picker reachable from
   the thing it changes. It must not look like one: no chrome, no padding, and
   the same footprint as the plain span a member sees, or the header would
   shift depending on who is reading it. */
.h-room__discbtn {
  padding: 0; border: 0; background: none; cursor: pointer; flex: none;
  border-radius: var(--r-md); display: block;
}
.h-room__discbtn:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }

/* THE PICKER.
   A live preview, two gradient stops, an ink row, the emblem grid and the
   initials box — the order Karl's Identity sheet uses, because a member picks a
   colour and then judges every emblem against it. */


/* ============================================================================
   `LKR-10` — ONE COLOUR CONTROL, THREE BUILDERS.

   🗣️ KARL CHOSE OPTION ②: presets lead, the native picker stops being a visible
   control. ⚖️ AND A LITERAL "Done" IS IMPOSSIBLE — that panel is drawn by the
   operating system and a page cannot put a button inside it. What a page CAN do
   is stop sending people there: the swatches answer the phone, the wheel is an
   opt-in for anyone who wants a colour that is not on the row.

   🥇 THE DESIGN IS KARL'S OWN AND IS NOT NEW. It has been shipping in the group
   identity builder since 08-14 (`.h-gid__dot` + `.h-gid__wheel`). This is that
   control promoted to a shared one, because the row says FIX ONCE, BOTH PLACES
   and the avatar studio and the Locker Room emblem row each had their own
   answer — a bare `<input type=color>` and, in the Locker Room, no swatches at
   all.

   🚨 THE WHEEL IS A `<label for>` AND MUST STAY ONE. Karl's Phase 69 note,
   carried in `groupidentity.js`: a proxy BUTTON that calls `.click()` does not
   open the native picker on iOS. It looks identical and silently does nothing
   on an iPhone. `.h-pick__in` is therefore hidden but PRESENT and clickable —
   `opacity: 0`, never `display: none`, which would break the label on Safari.
   ========================================================================== */
/* Sits exactly under the wheel and takes the tap the label forwards. */
/* Drawn as a wheel so it does not read as one more preset. */

/* `.h-gid__sw`, `.h-gid__dot` and `.h-gid__wheel` LIVED HERE and are gone —
   they became `.h-pick__row`, `.h-pick__sw` and `.h-pick__wheel` above, because
   `LKR-10` says FIX ONCE, BOTH PLACES and three builders cannot share a control
   that is named after one of them. `--brand` keeps its own name: it marks
   Karl's two brand colours at the head of the row and is about the COLOURS, not
   about the control. */
/* The two brand colours lead the row and are marked as such — Karl's
   `ghub-idsw-brand`. */
/* AUTO IS A CHOICE, NOT AN ABSENCE. It leads the ink row and writes NULL —
   without it an ink colour, once chosen, could never be given back to the
   auto-contrast rule. */

/* EVERY EMBLEM DRAWN AS ITSELF, in the group's own colours — the same rule the
   Locker Room's "How you appear" follows. A list of names would make somebody
   tap thirteen times to find out what "Ringmono" is. */
/* The cover-photo row: three preset photos, the lit one ringed green. */

/* ---------------------------------------------------------------------------
   TWO WRAPPER SPANS THAT WERE HOLDING A TWO-LINE LAYOUT WITH NO RULE AT ALL.

   FOUND 2026-08-24 by widening `tests/client-contracts.test.mjs` to see BEM
   `__element` names — its token filter was `[a-z0-9-]`, so **every class in
   this codebase containing a double underscore was invisible to it**, which is
   most of them. MEASURED IN A BROWSER RATHER THAN INFERRED FROM THE CSS:

     .h-scard__title  left 67, top 15      .h-scard__meta  left 147, top 17
     .h-mrow__n       left 43, top 89      .h-mrow__s      left 157, top 89

   Same line, side by side. `.h-scard` and `.h-mrow` are both `display: flex`,
   both wrappers are `<span>`s with no rule, and their children are therefore
   INLINE — so the card's title and its "Kanata Rink · 9:00 PM" ran together on
   one line instead of stacking, and so did a member's name and their
   "3 items · $56.50 paid".

   THE TELL WAS ALREADY IN THE STYLESHEET: `.h-scard__meta` sets
   `margin-top: 1px`, which does nothing whatever on an inline element. A
   vertical nudge on a horizontal thing is somebody styling the layout they
   MEANT, next to the one that was rendering.

   `.h-scard__why` was fine throughout, because it has a rule of its own and so
   became a block. One sibling behaving and two not is what an unstyled wrapper
   looks like from the outside, and nothing was ever going to go red for it: the
   markup is correct, the classes are spelled correctly, and every child has its
   own type ramp. It just renders on the wrong number of lines.

   `flex: 1` AND `min-width: 0` TOGETHER — the second is not optional. Without
   it a long programme name refuses to shrink below its content width and pushes
   the date block and the tag off the card.
   ------------------------------------------------------------------------ */
.h-scard__main,
.h-mrow__main {
  display: flex; flex-direction: column; flex: 1; min-width: 0;
}

/* AND THE TWO LINES INSIDE THE MEMBER ROW, which had no rules either — so once
   the wrapper stacked them they stacked IDENTICALLY: same weight, same size,
   same colour, a name and a subtitle indistinguishable from each other.
   `.h-scard__title` / `.h-scard__meta` are the same pair one component over and
   have had a type ramp all along; these are matched to it rather than invented,
   because two components showing the same shape should not disagree about what
   a subtitle looks like.

   HOW THESE TWO GOT MISSED FOR AN EXTRA TEN MINUTES IS WORTH THE LINE: I
   grepped the stylesheets for `h-mrow__n`, got a hit, and nearly closed them as
   already styled. **The hit was the comment directly above this one**, which
   quotes both class names while explaining the measurement. The test
   decomments before it looks; my grep did not. That is this project reading its
   own prose as code AGAIN, and this time it caught the person who has written
   the warning about it into four other files. */
.h-mrow__n { font-weight: 600; font-size: 13.5px; }
.h-mrow__s { color: var(--faint); font-size: 11.5px; margin-top: 1px; }

/* ---------------------------------------------------------------------------
   SHORT VIEWPORTS — the chrome gives height back.

   ⛔ THE DEFECT THIS ANSWERS, and it is a measurement rather than a taste.
   The appbar and the bottom nav are a FIXED 122px (55 + 67) at every viewport,
   because nothing in this file adapts them. Measured with
   `tools/landscape-smoke.mjs`:

     390x844 portrait  -> 122px of 844 = 14.5% chrome, 653px scroller, 8.0 cards
     844x390 landscape -> 122px of 390 = 31.3% chrome, 199px scroller, 3.1 cards

   Add Find's own 69px header and HALF a 390px-tall screen is furniture. The
   scroller shows 3.11 cards — and a fraction is the whole complaint: there is
   ALWAYS a card sliced across the boundary where the list meets the nav. Gus's
   words, after he retracted the stronger claim himself: *"the bottom navigation
   visually cuts across the short landscape viewport and clips cards at the
   scrolling boundary"* — not that anything is unreachable. Find scrolls; it
   looks wrong.

   📏 KEYED ON HEIGHT, NOT ON `orientation`, AND THAT IS DELIBERATE. The report
   arrived as "landscape", but landscape is not the mechanism — SHORTNESS is. A
   short desktop window has the identical problem and an `orientation` query
   would miss it, while a tall landscape tablet has no problem and an
   orientation query would shrink its chrome for nothing. Name the producer, not
   the symptom.

   🛑 AND NOT `position: fixed` ON THE NAV. The note at the top of this file
   warns against exactly that — it breaks safe-area padding and sheet/scrim
   stacking, and it was written after somebody tried. Nothing here changes the
   flex column; it only makes the chrome cost less of it.
   ------------------------------------------------------------------------ */
@media (max-height: 500px) {
  .h-appbar { padding: 5px 15px 4px; }

  .h-bnav { padding: 3px 4px calc(4px + env(safe-area-inset-bottom)); }
  .h-bnav__item { font-size: 9px; gap: 1px; padding: 2px 2px; }
  .h-bnav__item svg { width: 18px; height: 18px; }

  /* THE FAB IS THE PART THAT LITERALLY CUTS ACROSS. `margin-top:-24px` lifts it
     out of the nav and over the scroller; at this height that overhang is worth
     more than the flourish, so it is reduced rather than removed — removing it
     would change the design language on one breakpoint, which is a decision for
     whoever owns the kit, not a bug fix. */
  .h-bnav__fab {
    width: 42px; height: 42px; margin-top: -13px; border-width: 3px;
  }
  .h-bnav__fab svg { width: 21px; height: 21px; }
}

/* ---- V3.30 · THE READABLE DATE BOX (Karl: never 2026-01-01) ----------------
   The real `type="date"` input keeps its box, border and tap target; only its
   locale-drawn text is hidden, under a face written by dates.js. `is-live` is
   set only once dates.js has painted, so a page without it shows the native box. */
.h-datef { position: relative; display: inline-block; vertical-align: middle; max-width: 100%; }
.h-datef:has(> .h-input) { display: block; }
.h-datef.is-live > input[type="date"] { color: transparent; -webkit-text-fill-color: transparent; }
.h-datef.is-live > input[type="date"]::-webkit-datetime-edit { opacity: 0; }
.h-datef__face { display: none; }
.h-datef.is-live .h-datef__face {
  display: flex; align-items: center; position: absolute; inset: 0;
  padding: 0 44px 0 15px; pointer-events: none; color: var(--ink); font-size: 16px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.h-datef.is-live.is-empty .h-datef__face { color: var(--muted); }

/* V3.31 - SF-05. The top bar's Quick Discover compass carries the AI sparkle
   top-left; it twinkles slowly so the door reads as "ask me", and holds still
   for anybody who asked the system for less motion. */
.h-ic__spark { transform-box: fill-box; transform-origin: center; animation: h-spark 3.6s ease-in-out infinite; }
@keyframes h-spark { 0%, 70%, 100% { opacity: 1; transform: scale(1); } 80% { opacity: .45; transform: scale(.7); } 90% { opacity: 1; transform: scale(1.18); } }
@media (prefers-reduced-motion: reduce) { .h-ic__spark { animation: none; } }

/* V3.32 - a full session's door is the WAITLIST, and it must not look like a
   registration: the pair `.pg-pill--drop` already proves for contrast. */
.gs-apply.gs-apply--wait, .h-btn.h-reg__wait { background: var(--purple-solid); color: var(--on-purple); border-color: var(--purple-solid); }

/* ==========================================================================
   🏒 THE GAME ROSTER, REBUILT (Karl, 2026-09-28; Sloane's preview). Heading
   with Sort, the team key, sections with their own +, ringed rows with •••,
   the private Officials strip, the ••• card and the add-players picker.
   ======================================================================== */
.gs-rhead { display: flex; align-items: flex-end; justify-content: space-between; gap: 10px; }
.gs-rhead small { display: block; font-size: 11px; font-weight: 800; letter-spacing: .1em; color: var(--green); }
.gs-rhead h3 { margin: 2px 0 0; font-family: var(--font-display); font-weight: 400; font-size: 24px; letter-spacing: .03em; }
.gs-rsort { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--muted); }
/* RR-01: flat here — the icon alone, no card around it. */
.gs-rsort .h-sortbtn { background: none; border: 0; padding: 0; min-height: 36px; }
.gs-rsort b { color: var(--ink); }
.gs-teamkey { flex-wrap: wrap; }
.gs-teamkey__go {
  margin-left: auto; border: 0; background: none; padding: 4px 2px; cursor: pointer;
  color: var(--green); font: inherit; font-size: 12.5px; font-weight: 800;
}
.gs-rgroup__t { font-family: var(--font-display); font-size: 15px; letter-spacing: .06em; }
.gs-rgroup--wait .gs-rgroup__t { color: var(--active-purple-ink); }
.gs-rgroup__acts { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; }
.gs-plus {
  width: 30px; height: 30px; display: grid; place-items: center; padding: 0; cursor: pointer;
  border: 1px solid var(--green); border-radius: 50%; background: var(--panel); color: var(--green);
}
.gs-plus .h-ic { width: 16px; height: 16px; }
/* RR-02: the waitlist's + wears the waitlist purple. */
.gs-plus--wait { border-color: var(--purple); color: var(--active-purple-ink); }
.gs-rlist { display: flex; flex-direction: column; }
.gs-rrow {
  display: flex; align-items: center; gap: 10px; min-height: 58px; padding: 9px 12px;
  border-top: 1px solid var(--line);
}
.gs-rlist > .gs-rrow:first-child { border-top: 0; }
.gs-rrow.is-you { background: var(--panel); }
.gs-rrow__c { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.gs-rrow__c strong { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gs-rrow__c small { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
.gs-pav {
  --pav: 38px; position: relative; flex: none; width: var(--pav); height: var(--pav);
  display: grid; place-items: center; border-radius: 50%;
  background: var(--panel); border: 2px solid var(--line2); color: var(--ink);
  font-size: 12px; font-weight: 800;
}
.gs-pav.is-ghost { border-style: dashed; color: var(--muted); }
/* C3 (2026-10-05): a face inside the ring. */
.gs-pav.has-face, .gs-av.has-face { background: none; }
.gs-pav.has-face > .h-av, .gs-av.has-face > .h-av { width: 100%; height: 100%; }
.gs-pav i {
  position: absolute; right: -4px; bottom: -3px; width: 16px; height: 16px; display: grid; place-items: center;
  border-radius: 50%; font-style: normal; font-size: 9px; font-weight: 900;
  background: var(--panel2); border: 1.5px solid currentColor;
}
.gs-pav--organizer { border-color: var(--warn); }
.gs-pav--organizer i { color: var(--warn); }
.gs-pav--coach { border-color: var(--purple); }
.gs-pav--coach i { color: var(--active-purple-ink); }
.gs-pav--staff { border-color: var(--green); }
.gs-pav--staff i { color: var(--active-green-ink); }
.gs-pav--friend { border-color: var(--info); }
.gs-pav--friend i { color: var(--info); }
.gs-pav--visitor { border-color: var(--ring-visitor); }
.gs-pav--visitor i { color: var(--ring-visitor); }
.gs-rpill {
  flex: none; min-width: 46px; min-height: 30px; padding: 0 10px; display: inline-grid; place-items: center;
  border: 1px solid var(--line2); border-radius: var(--r-pill); background: var(--panel);
  color: var(--muted); font: inherit; font-size: 11px; font-weight: 900; letter-spacing: .04em;
}
button.gs-rpill { cursor: pointer; }
.gs-rpill.is-yes { border-color: var(--green); color: var(--active-green-ink); }
.gs-rpill.is-no { border-color: var(--danger); color: var(--danger); }
.gs-movein {
  flex: none; min-height: 30px; padding: 0 10px; border: 0; border-radius: var(--r-pill); cursor: pointer;
  background: var(--purple-solid); color: var(--on-purple); font: inherit; font-size: 11px; font-weight: 900; letter-spacing: .04em;
}
.gs-dots {
  flex: none; width: 34px; height: 34px; padding: 0; border: 0; border-radius: 50%; cursor: pointer;
  background: none; color: var(--muted); font: inherit; font-size: 13px; letter-spacing: 1px;
}
.gs-dots:hover, .gs-dots:focus-visible { background: var(--panel); color: var(--ink); }
.gs-refs { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel2); }
.gs-refs summary { display: flex; align-items: center; gap: 10px; padding: 11px 12px; cursor: pointer; list-style: none; }
.gs-refs summary::-webkit-details-marker { display: none; }
.gs-refs summary .h-ic { width: 16px; height: 16px; color: var(--muted); }
.gs-refs__t { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.gs-refs__t small { font-size: 12px; color: var(--muted); }
.gs-refs__body { border-top: 1px solid var(--line); }
.gs-refs__add {
  display: block; width: 100%; padding: 11px 12px; border: 0; border-top: 1px solid var(--line); background: none;
  color: var(--green); font: inherit; font-size: 13px; font-weight: 800; text-align: left; cursor: pointer;
}
.gs-grlink {
  display: flex; align-items: center; gap: 10px; width: 100%; padding: 12px; cursor: pointer; text-align: left;
  border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel2); color: var(--ink); font: inherit;
}
.gs-grlink > span:nth-child(2) { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.gs-grlink small { font-size: 12px; color: var(--muted); }
.gs-grlink .h-ic { width: 18px; height: 18px; color: var(--green); flex: none; }
/* The ••• card */
.gs-pmenu__who { display: flex; align-items: center; gap: 12px; padding: 2px 2px 12px; }
.gs-pmenu__who > span:last-child { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.gs-pmenu__who small { font-size: 12.5px; color: var(--muted); }
.h-more__row.is-danger .h-more__t, .h-more__row.is-danger .h-more__ic { color: var(--danger); }
/* The picker */
.gs-pk { display: flex; flex-direction: column; gap: 10px; }
.gs-pk__seg { margin: 0; }
.gs-pk__q { flex: none; }
.gs-pk__list { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.gs-pk__row {
  display: flex; align-items: center; gap: 10px; width: 100%; padding: 9px 12px; text-align: left; cursor: pointer;
  border: 0; border-top: 1px solid var(--line); background: var(--panel2); color: var(--ink); font: inherit;
}
.gs-pk__list > .gs-pk__row:first-child { border-top: 0; }
.gs-pk__row.is-on { background: var(--active-green-bg); }
.gs-pk__tick { width: 20px; height: 20px; flex: none; color: var(--green); }
.gs-pk__guest {
  display: flex; align-items: center; gap: 10px; width: 100%; padding: 11px 12px; text-align: left; cursor: pointer;
  border: 1px dashed var(--line2); border-radius: var(--r-md); background: none; color: var(--ink); font: inherit;
}
.gs-pk__guest > span:last-child { display: flex; flex-direction: column; gap: 2px; }
.gs-pk__guest small { font-size: 12px; color: var(--muted); }
.gs-pk__guest .h-ic { width: 18px; height: 18px; color: var(--green); flex: none; }
.gs-pk__mismatch { margin: 0; padding: 10px 12px; border: 1px solid var(--warn); border-radius: var(--r-md); background: var(--panel); color: var(--ink); font-size: 12.5px; }
.gs-pk__go { flex: 1.6; }
/* RR-04 (Karl): the Add bar stays on screen however long the list is — it
   sticks to the bottom of the sheet's own scroll, over the list. */
.gs-pk__bar {
  /* Walk v45 (Karl): pinned ON the sheet's bottom edge (the sheet is the
     containing block — `ui.js footSheet`), NO bar behind the buttons. */
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  display: flex; flex-direction: column; gap: 8px;
  margin: 0; padding: 10px 16px calc(12px + env(safe-area-inset-bottom));
  background: transparent; border: 0;
}
.gs-pk__barrow { display: flex; gap: var(--sp-3); }
.gs-pk__barrow .h-btn--ghost { flex: 1; }
.gs-pk__more { text-align: center; }
/* RO-09: the add-someone steps' own Back / Next bar. */
/* v41 (Karl): the roster Add wizard's Back / Next is pinned like every wizard. */
.h-rstep__nav {
  /* Walk v45 (Karl): a wizard's buttons sit at the END of the step, in the
     flow — never pinned, never on a bar (`H365UI.wizFoot`). */
  position: static; display: flex; gap: var(--sp-3); margin: 18px 0 4px; padding: 0;
  background: none; border: 0; box-shadow: none;
}
.h-rstep__nav .h-btn--ghost { flex: 1; }
.h-rstep__nav .h-btn:not(.h-btn--ghost) { flex: 1.6; }

/* ==========================================================================
   👕 THE TEAM BUILDER (`teambuilder.js`, Sloane's preview): three columns side
   by side even on a small phone, F / D / G sections, chips with a padlock,
   press-and-hold drag with a ghost card.
   ======================================================================== */
/* 🖥 B26 / TM-01 (Karl, 2026-09-29): a FULL PAGE over the Game Space and the
   nav (like the roster manager), laid out as Sloane's preview: two teams side
   by side, Free Agents a band across the top only when it is needed. */
.tb-page {
  position: absolute; inset: 0; z-index: 46; overflow-y: auto; background: var(--bg); color: var(--ink);
  opacity: 0; transform: translateY(8px); transition: opacity var(--t-fast), transform var(--t-fast);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.tb-page.is-open { opacity: 1; transform: none; }
.tb-page__hd {
  position: sticky; top: 0; z-index: 3; display: grid; grid-template-columns: 40px minmax(0, 1fr) 40px;
  align-items: center; gap: 10px; padding: calc(12px + env(safe-area-inset-top, 0px)) 16px 12px;
  border-bottom: 1px solid var(--line); background: var(--bg);
}
.tb-page__titles { min-width: 0; text-align: center; }
.tb-page__titles small { display: block; color: var(--green); font-size: 11px; font-weight: 800; letter-spacing: .12em; }
.tb-page__titles h2 { margin: 2px 0 0; font-family: var(--font-display); font-weight: 400; font-size: 26px; letter-spacing: .03em; text-transform: uppercase; line-height: 1.05; }
.tb-page__titles span { display: block; margin-top: 2px; font-size: 12.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tb-page__body { max-width: 900px; margin: 0 auto; padding: 12px 12px 40px; }
.tb { display: flex; flex-direction: column; gap: 12px; }
.tb-hint { margin: 0; font-size: 12.5px; color: var(--muted); text-align: center; }
/* TM-01: Sorting, the standard Sort button and Generate on ONE row. */
.tb-tools { display: flex; align-items: center; gap: 8px; }
.tb-sortw { flex: 1 1 auto; min-width: 0; font-size: 12px; color: var(--muted); }
.tb-sortw b { color: var(--ink); }
.tb-tools .h-sortwrap { flex: none; }
.tb-tools .h-sortbtn { background: none; border: 0; padding: 0; min-height: 40px; }
.tb-gen { flex: none; display: inline-flex; align-items: center; gap: 6px; }
.tb-gen .h-ic { width: 15px; height: 15px; }
.tb-cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.tb-col {
  min-width: 0; display: flex; flex-direction: column; gap: 6px; padding: 8px;
  border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel);
}
.tb-col__hd {
  display: flex; align-items: center; gap: 8px; min-width: 0; padding: 4px 2px 10px;
  border-bottom: 1px solid var(--line);
}
.tb-col__hd > div { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.tb-col__hd strong { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tb-col__hd small { font-size: 11px; color: var(--muted); }
.tb-jersey {
  --jersey: url('../img/spaces/jerseys/jersey-white.png');
  flex: none; width: 38px; height: 38px; padding: 0; border: 0; border-radius: 8px;
  background: var(--jersey) center / contain no-repeat;
}
button.tb-jersey { cursor: pointer; }
/* TM-01: the side's overall skill, the reviews' ring in small. */
.tb-ring { position: relative; flex: none; width: 40px; height: 40px; display: grid; place-items: center; }
.tb-ring svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.tb-ring__bg { fill: none; stroke: var(--line2); stroke-width: 9; }
.tb-ring__fg { fill: none; stroke: var(--green); stroke-width: 9; stroke-linecap: round; }
.tb-ring b { position: relative; font-size: 11.5px; font-weight: 900; }
/* Free Agents: a band across the top (preview), purple — shown only with
   somebody in it, or as the drop target while a player is picked up. */
.tb-free {
  display: flex; flex-direction: column; gap: 6px; padding: 8px; cursor: pointer;
  border: 1px solid var(--purple); border-radius: var(--r-lg); background: var(--active-purple-bg, var(--panel));
}
.tb-free.is-ready { border-style: dashed; }
.tb-free__hd { display: flex; align-items: baseline; justify-content: center; gap: 8px; }
.tb-free__hd strong { font-size: 11px; font-weight: 900; letter-spacing: .1em; text-transform: uppercase; color: var(--active-purple-ink); }
.tb-free__hd small { font-size: 11px; color: var(--muted); }
.tb-free__list { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 6px; }
.tb-sec { display: flex; flex-direction: column; gap: 5px; min-height: 38px; padding: 6px 0 2px; border-radius: 8px; cursor: pointer; }
.tb-sec__t { font-size: 10px; font-weight: 900; letter-spacing: .1em; color: var(--muted); }
.tb-sec--D .tb-sec__t { color: var(--active-purple-ink); }
.tb-sec--G .tb-sec__t { color: var(--goalie); }
.tb-chip {
  position: relative; display: flex; align-items: center; gap: 6px; min-height: 46px;
  padding: 5px 4px 5px 6px; border: 1px solid var(--line2); border-radius: 10px; background: var(--panel2);
  touch-action: manipulation; user-select: none; -webkit-user-select: none;
}
.tb-chip[data-tb-chip] { cursor: grab; }
.tb-chip.is-sel { border-color: var(--green); box-shadow: 0 0 0 1px var(--green); }
.tb-chip.is-locked { opacity: .62; }
.tb-chip.is-dragging { opacity: .35; }
.tb-chip__av {
  flex: none; width: 26px; height: 26px; display: grid; place-items: center; border-radius: 50%;
  background: var(--panel); border: 1px solid var(--line2); font-size: 9.5px; font-weight: 900;
}
.tb-chip__av.has-face { background: none; }
.tb-chip__c { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.tb-chip__n { font-size: 12px; font-weight: 800; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tb-chip__lk { font-size: 8.5px; font-weight: 900; letter-spacing: .08em; color: var(--warn); }
.tb-chip__m { flex: none; display: flex; flex-direction: column; align-items: flex-end; font-style: normal; font-size: 10.5px; font-weight: 800; color: var(--muted); line-height: 1.15; }
.tb-chip__m i { font-style: normal; font-weight: 700; color: var(--ink); }
/* TM-04 (Karl): a padlock you can see and hit — 36 px, a real lock glyph. */
.tb-lock {
  flex: none; width: 36px; height: 36px; margin: -4px -2px -4px 0; padding: 0; display: grid; place-items: center;
  border: 1px solid transparent; border-radius: 9px; background: none; color: var(--muted); cursor: pointer;
}
.tb-lock .h-ic { width: 19px; height: 19px; }
.tb-lock.is-on { color: var(--warn); border-color: transparent; background: var(--panel); }   /* TM-04 (walk v37): the lock alone, no amber outline */
/* TM-01: the unhappy face for a player off their preferred position. */
.tb-unhappy {
  flex: none; width: 26px; height: 26px; padding: 0; display: grid; place-items: center;
  border: 0; border-radius: 50%; background: none; color: var(--warn); cursor: pointer;
}
.tb-unhappy .h-ic { width: 17px; height: 17px; }
.tb-pop {
  position: absolute; right: 8px; bottom: calc(100% + 6px); z-index: 4; padding: 7px 10px; white-space: nowrap;
  border: 1px solid var(--warn); border-radius: var(--r-md); background: var(--elev); color: var(--ink);
  font-size: 12px; font-weight: 600;
}
/* TM-02 (Karl): a two-second shimmer on whoever just moved. */
@keyframes tb-shimmer {
  0%, 100% { box-shadow: 0 0 0 0 transparent; border-color: var(--line2); }
  30%, 70% { box-shadow: 0 0 0 3px var(--green-dim), 0 0 18px var(--green-dim); border-color: var(--green); }
}
@keyframes tb-shimmer-b {
  0%, 100% { box-shadow: 0 0 0 0 transparent; border-color: var(--line2); }
  30%, 70% { box-shadow: 0 0 0 3px var(--purple-dim), 0 0 18px var(--purple-dim); border-color: var(--purple); }
}
/* TM-02 (walk v37): 1.5 s in all; the first pick green, the second purple. */
.tb-chip.is-moved { animation: tb-shimmer .75s ease-in-out 2; }
.tb-chip.is-moved.is-moved--b { animation-name: tb-shimmer-b; }
@media (prefers-reduced-motion: reduce) { .tb-chip.is-moved { animation: none; border-color: var(--green); } }
.tb-ghost {
  position: fixed; left: 0; top: 0; z-index: 400; pointer-events: none; opacity: .92;
  box-shadow: var(--sh-3); border-color: var(--green);
}
/* TM-08: teams not made yet. */
.tb-free.is-used { border-style: solid; }
.tb-chip.is-goalie .tb-chip__av { border-color: var(--goalie); }   /* goalies are blue app-wide (Karl, walk v37) */
/* RO-09: each step of Add someone is its own section (hidden when not current). */
.h-rstep { display: block; }
/* RE-01 (walk v41): "Found a Hockey365 member: Hannah B." on Add's step 2. */
.h-rfound { display: flex; gap: 12px; align-items: flex-start; }
.h-rfound > .h-ic { flex: none; width: 34px; height: 34px; padding: 7px; border-radius: 50%; color: var(--green); background: var(--panel2); border: 1px solid var(--line); }
/* RE-01 v45: the way back on a found member, right above Add. */
.h-rfound__alt {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap;
  margin-top: 12px; padding: 10px 12px; border: 1px solid var(--warn); border-radius: var(--r-md);
  background: var(--panel2); color: var(--ink); font-size: 13px;
}
.h-rfound__retry {
  background: none; border: 0; padding: 4px 0; color: var(--warn); font: inherit; font-weight: 800;
  text-decoration: underline; cursor: pointer;
}
.h-rfound__n { font-family: var(--font-head); font-weight: 800; font-size: 16px; color: var(--ink); margin-bottom: 4px; }
.tb-notyet { padding: 14px; border: 1px dashed var(--line2); border-radius: var(--r-lg); text-align: center; }
.tb-notyet strong { display: block; font-size: 15px; }
.tb-notyet p { margin: 6px 0 0; font-size: 13px; color: var(--muted); }
.tb-link { border: 0; background: none; padding: 0; color: var(--green); font: inherit; font-weight: 800; text-decoration: underline; cursor: pointer; }
.tb-share { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
.tb-share .h-btn { display: inline-flex; align-items: center; gap: 6px; }
.tb-share .h-ic { width: 15px; height: 15px; }
@media (max-width: 380px) {
  .tb-chip { gap: 4px; padding-left: 4px; }
  .tb-chip__av { display: none; }
}

/* 🖨 TM-07: THE TEAM SHEET. A paper preview over the app; printing shows only
   the paper. Printer-friendly: brand ink on white, a faint Hockey365 mark, big
   names sized to fit one page (`--tb-fs`, set from the longer side). */
.tb-sheetview {
  position: fixed; inset: 0; z-index: 300; overflow-y: auto; padding: 12px 12px 40px;
  background: var(--bg);
}
.tb-sheetview__bar { max-width: 820px; margin: 0 auto 12px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.tb-sheetview__bar .h-btn { display: inline-flex; align-items: center; gap: 6px; }
.tb-sheetview__bar p { flex: 1 1 100%; margin: 0; font-size: 12px; color: var(--muted); }
.tb-print {
  --tb-fs: 7mm;
  position: relative; width: 190mm; box-sizing: border-box; margin: 0 auto; padding: 14mm 12mm; overflow: hidden; transform-origin: top left;
  background: var(--brand-neutral); color: var(--brand-black); border-radius: 6px;
  font-family: var(--font-body);
}
.tb-print__mark { position: absolute; left: 50%; top: 52%; width: 70%; transform: translate(-50%, -50%); opacity: .06; pointer-events: none; }
.tb-print__hd { position: relative; text-align: center; margin-bottom: 8mm; }
.tb-print__logo { height: 9mm; width: auto; }
.tb-print__hd h1 { margin: 3mm 0 1mm; font-family: var(--font-display); font-weight: 400; font-size: 11mm; line-height: 1.05; letter-spacing: .02em; text-transform: uppercase; }
.tb-print__hd p { margin: 0; font-size: 4mm; color: var(--brand-charcoal); }
.tb-print__teams { position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: 10mm; }
.tb-print__team { min-width: 0; }
.tb-print__team h2 {
  display: flex; align-items: center; gap: 3mm; margin: 0 0 3mm; padding-bottom: 2mm;
  border-bottom: .6mm solid var(--brand-black); font-size: calc(var(--tb-fs) * 1.05); font-weight: 900;
}
.tb-print__jersey { flex: none; width: 11mm; height: 11mm; background: var(--jersey) center / contain no-repeat; }
.tb-print__team h3 { margin: 3mm 0 1mm; font-size: 3.4mm; font-weight: 900; letter-spacing: .14em; text-transform: uppercase; color: var(--brand-precision); }
.tb-print__team ul { margin: 0; padding: 0; list-style: none; }
.tb-print__team li {
  font-size: var(--tb-fs); font-weight: 800; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tb-print__ft { position: relative; margin-top: 8mm; text-align: center; font-size: 3mm; letter-spacing: .12em; text-transform: uppercase; color: var(--brand-precision); }
@media print {
  @page { size: portrait; margin: 10mm; }
  body.tb-printing > *:not(.tb-sheetview) { display: none !important; }
  body.tb-printing { background: none !important; }
  body.tb-printing .tb-sheetview { position: static; overflow: visible; padding: 0; background: none; }
  body.tb-printing .tb-sheetview__bar { display: none !important; }
  body.tb-printing .tb-print { width: auto; margin: 0 !important; padding: 0; border-radius: 0; background: none; transform: none !important; break-inside: avoid; }
}
.tb-colours { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.tb-swatch {
  display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 8px 6px;
  border: 1px solid var(--line2); border-radius: var(--r-md); cursor: pointer;
  background: var(--panel2); color: var(--ink); font: inherit; font-size: 12px; font-weight: 800;
}
.tb-swatch__art {
  --jersey: url('../img/spaces/jerseys/jersey-white.png');
  width: 56px; height: 58px; background: var(--jersey) center / contain no-repeat;
}
.tb-swatch.is-on { box-shadow: 0 0 0 2px var(--green); }
.tb-genlist { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; font-size: 13px; }

/* 🎚️ THE APP'S OWN SLIDER (walk v37). A green fill up to the value (`--fill`,
   set in the markup and kept current by ui.js), a solid green thumb, and the
   same look in every browser. Unset reads grey. 28px tall for a thumb. */
/* 🎚️ THE ONE SKILL CONTROL (`H365UI.skillControl`, walk v38 LR-03): label,
   a TYPABLE number box, then the `.h-range` slider. Same look on every page. */
.h-skill { margin-bottom: 14px; }
.h-skill__h { display: flex; align-items: center; gap: 8px; min-width: 0; }
.h-skill__l { flex: 1; min-width: 0; font-size: 13.5px; font-weight: 700; }
.h-skill__v {
  flex: none; width: 56px; height: 34px; padding: 0 6px; text-align: center;
  border: 1px solid var(--line2); border-radius: var(--r-sm); background: var(--panel2);
  /* 16px: no AUTO ZOOM ON FIELD ENTRY (Karl's term). */
  font-family: var(--font-head); font-weight: 800; font-size: 16px; color: var(--green);
  font-variant-numeric: tabular-nums; -moz-appearance: textfield; appearance: textfield;
}
.h-skill__v::-webkit-outer-spin-button, .h-skill__v::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.h-skill__v:focus-visible { outline: 2px solid var(--green); outline-offset: 1px; }
.h-skill__v:disabled { border-color: transparent; background: transparent; }
.h-skill.is-unset .h-skill__v { color: var(--muted); }
.h-skill__range { margin-top: 6px; }

.h-range {
  --fill: 50%;   /* the default before `rangeFill` / the inline style sets the real value */
  -webkit-appearance: none; appearance: none; width: 100%; height: 28px;
  background: transparent; cursor: pointer; margin: 4px 0 0;
}
.h-range:focus-visible { outline: 2px solid var(--green); outline-offset: 4px; border-radius: var(--r-pill); }
.h-range::-webkit-slider-runnable-track {
  height: 6px; border-radius: var(--r-pill);
  background: linear-gradient(to right, var(--green) 0 var(--fill), var(--line2) var(--fill) 100%);
}
.h-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 22px; height: 22px; margin-top: -8px;
  border-radius: 50%; background: var(--green); border: 3px solid var(--panel);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .45);
}
.h-range::-moz-range-track { height: 6px; border-radius: var(--r-pill); background: var(--line2); }
.h-range::-moz-range-progress { height: 6px; border-radius: var(--r-pill); background: var(--green); }
.h-range::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%; background: var(--green);
  border: 3px solid var(--panel); box-shadow: 0 1px 4px rgba(0, 0, 0, .45);
}
.h-range:disabled { cursor: default; }
/* NOT SET YET: still a green slider (Karl: never "a floating grey dot"), dimmed,
   with a ringed thumb, so it reads as "not rated" rather than broken. */
.is-unset .h-range::-webkit-slider-runnable-track {
  background: linear-gradient(to right, rgba(var(--green-rgb), .35) 0 var(--fill), var(--line2) var(--fill) 100%);
}
.is-unset .h-range::-webkit-slider-thumb { background: var(--panel); border: 3px solid var(--green); }
.is-unset .h-range::-moz-range-progress { background: rgba(var(--green-rgb), .35); }
.is-unset .h-range::-moz-range-thumb { background: var(--panel); border: 3px solid var(--green); }

/* 🏷 WALK v42 (Karl): the Adult / Youth tip beside a program, session, game
   or group name on every list (`H365UI.ageTip`).
   AY-01 v45 (Karl): "a small filled in chip". Solid fill with its paired ink
   token, so it reads the same on any card or theme; sized to its word
   (`width: max-content`, `align-self: flex-start`) so it never takes a row. */
.h-agetip {
  display: inline-block; vertical-align: 2px; margin-left: 4px; padding: 0 5px;
  width: max-content; align-self: flex-start; flex: none;
  border-radius: var(--r-sm, 4px);
  font-family: var(--font-body, inherit); font-size: 9.5px; font-weight: 800; line-height: 15px;
  letter-spacing: .06em; text-transform: uppercase; white-space: nowrap;
  color: var(--bg); background: var(--muted);
}
.h-agetip--youth { color: var(--on-purple); background: var(--purple-solid); }
.h-agetip--family, .h-agetip--all { color: var(--on-green); background: var(--green); }
.pg-card-pills .h-agetip { margin-left: 0; align-self: flex-end; vertical-align: 0; }

/* 📐 THE WIZARD PROTOCOL (walk v45, Karl): one header and one footer for
   every step-by-step wizard (`H365UI.wizSteps` / `H365UI.wizFoot`). */
.h-wizsteps { display: flex; flex-direction: column; gap: 6px; margin: 0 0 12px; }
.h-wizsteps__bars { display: flex; gap: 5px; }
.h-wizsteps__bars i { flex: 1; height: 4px; border-radius: var(--r-pill); background: var(--line2); }
.h-wizsteps__bars i.is-on { background: var(--green); }
.h-wizsteps__lbl { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
/* ‹ Back a step (Karl, 10-03, second pass): ONE button, chevron + the word
   Step. The tap area covers both; only the chevron looks like a button, and
   Step keeps the label's plain look on hover and focus. */
.h-wizsteps__back { display: inline-flex; align-items: center; gap: 6px; padding: 4px 2px; margin: -4px 0; border: 0; background: none; font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit; cursor: pointer; }
.h-wizsteps__back:focus { outline: none; }
/* The word Step inside the Back control: the label's own plain text, on purpose. */
.h-wizsteps__word { color: inherit; text-decoration: none; }
.h-wizsteps__chev { flex: none; width: 22px; height: 22px; display: grid; place-items: center; border-radius: 50%; border: 1px solid var(--line2); background: var(--panel2); color: var(--ink); }
.h-wizsteps__chev svg { width: 14px; height: 14px; }
.h-wizsteps__back:hover .h-wizsteps__chev, .h-wizsteps__back:focus-visible .h-wizsteps__chev { border-color: var(--green); color: var(--green); }
.h-wizsteps__back:focus-visible .h-wizsteps__chev { box-shadow: 0 0 0 2px var(--green); }
.h-wizfoot { display: flex; gap: var(--sp-3); margin: 18px 0 4px; }
.h-wizfoot .h-btn--ghost { flex: 1; }
.h-wizfoot .h-btn:not(.h-btn--ghost) { flex: 1.6; }
/* Pinned buttons, glass while more is below (Karl, walk v45): NO bar behind
   them — the BUTTONS turn to glass, like the bottom nav; solid at the end. */
.h-pinbtns:not(.is-floor) .h-btn--ghost,
.h-sheet--pin:not(.is-floor) .h-sheet__actions .h-btn--ghost {
  background: linear-gradient(180deg, var(--glass-nav-sheen-top), var(--glass-nav-sheen-low) 60%), var(--glass-btn-tint);
  border-color: var(--glass-nav-edge);
  box-shadow: inset 0 1px 0 var(--glass-nav-hi), 0 6px 18px rgba(0, 0, 0, .45);
  -webkit-backdrop-filter: blur(var(--glass-btn-blur)) saturate(1.4);
  backdrop-filter: blur(var(--glass-btn-blur)) saturate(1.4);
}
.h-pinbtns:not(.is-floor) .h-btn:not(.h-btn--ghost),
.h-sheet--pin:not(.is-floor) .h-sheet__actions .h-btn:not(.h-btn--ghost) {
  background: rgba(var(--green-rgb), .80);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 6px 18px rgba(0, 0, 0, .45);
  -webkit-backdrop-filter: blur(var(--glass-btn-blur)) saturate(1.4);
  backdrop-filter: blur(var(--glass-btn-blur)) saturate(1.4);
}
@media (prefers-reduced-transparency: reduce) {
  .h-pinbtns .h-btn--ghost, .h-sheet--pin .h-sheet__actions .h-btn--ghost { background: var(--panel); -webkit-backdrop-filter: none; backdrop-filter: none; }
  .h-pinbtns .h-btn:not(.h-btn--ghost), .h-sheet--pin .h-sheet__actions .h-btn:not(.h-btn--ghost) { background: var(--action); -webkit-backdrop-filter: none; backdrop-filter: none; }
}

/* 🧱 A SHEET WITH A BUTTON BAR (Karl, walk v42: "fix it for good on all
   screens"). `ui.js footSheet` moves the sheet's content into an inner
   scroller with NO bottom padding, so the wizard bar, the player picker's bar
   and the roster Add bar stick at exactly `bottom: 0` — flush with the
   screen's edge on iOS too (a negative sticky offset left them floating). */
.h-sheet--foot { display: flex; flex-direction: column; overflow: hidden; padding-bottom: 0; --pin-h: 76px; /* ui.js measures the real height */ }
.h-sheet--foot > .h-sheet__grip, .h-sheet--foot > .h-sheet__head, .h-sheet--foot > .h-sheet__actions { flex: none; }
.h-sheet--foot > .h-sheet__scroll {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  margin: 0 -16px; padding: 0 16px var(--pin-h);
}
.h-sheet--foot > .h-sheet__actions:not(:empty) { padding-bottom: calc(14px + env(safe-area-inset-bottom)); }

/* 📌 THE PINNED FOOTER (Karl, walk v37) — `H365UI.open({ pinActions: true })`,
   long edit forms only. The sheet stops scrolling itself; its body scrolls in
   `.h-sheet__scroll` UNDER a footer that sits flush on the bottom edge. Glass
   while more of the form is below (heavier than the bottom nav's), solid at
   the floor (`is-floor`, set by ui.js). */
.h-sheet--pin { display: flex; flex-direction: column; overflow: hidden; padding-bottom: 0; }
.h-sheet--pin .h-sheet__grip, .h-sheet--pin .h-sheet__head { flex: none; }
.h-sheet--pin .h-sheet__scroll {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  margin: 0 -16px; padding: 0 16px calc(84px + env(safe-area-inset-bottom));
}
.h-sheet--pin .h-sheet__actions {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3; margin: 0;
  padding: 12px 16px calc(14px + env(safe-area-inset-bottom));
  /* Walk v45 (Karl): no bar — the buttons carry the glass (below). */
  background: transparent; border: 0;
}
@media (prefers-reduced-transparency: reduce) {
  .h-sheet--pin .h-sheet__actions { background: var(--panel); -webkit-backdrop-filter: none; backdrop-filter: none; }
}

/* ===== SHADED EDGES ON A SIDEWAYS DATE STRIP (Karl, BC-01, 2026-10-01) =====
   `.h-fade-x` on the scroller; `H365UI.fadeX` sets `is-more-l` / `is-more-r`
   for the side that has more to scroll to. A mask, not an overlay, so it fades
   into whatever is behind the strip and takes no taps. */
.h-fade-x {
  --fade-l: 0px; --fade-r: 0px;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-l), #000 calc(100% - var(--fade-r)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-l), #000 calc(100% - var(--fade-r)), transparent 100%);
}
.h-fade-x.is-more-l { --fade-l: 28px; }
.h-fade-x.is-more-r { --fade-r: 28px; }

/* 🏛 THE PUBLIC-ICE CREDIT (ottrec.ca / City of Ottawa), as a small tip
   (Karl, 10-03: "way too big ... the same size as one of our small tips").
   Quiet, never hidden: no display:none, no truncation, a readable token
   colour in both themes (the licence asks for it to be DISPLAYED). Lives here
   because both the member app and ice.html load this file. */
.ice-attrib {
  margin: 14px 0 6px; padding-top: 10px; border-top: 1px solid var(--line);
  font-size: 12px; line-height: 1.5; color: var(--muted);
}
.ice-attrib a { color: var(--green); font-weight: 700; text-decoration: underline; text-underline-offset: 2px; }
