/* ==========================================================================
   DESIGN SYSTEM (ADR-0285): the shared design system of the TMV apps, one
   stylesheet. Tokens, reset, window and shell, buttons, forms, cards, tables,
   modals and sheet, notifications, the gate and the phone frame.

   Linked FIRST on every page, before any page-specific stylesheet. Other TMV
   apps vendor this file byte-identical; the consumer's lock records the
   Members commit it came from. Nothing page-specific belongs here
   (tests/unit/design-system-purity.test.js is the list), and a change here is
   a change to every TMV app. Never edit a vendored copy.
   ========================================================================== */

/* ==========================================================================
   Tokens, reset, utilities, window, shell, title bar, rail nav
   ========================================================================== */
/* ============================================================================
   TMV MEMBERSHIP - REDESIGN
   Clean, modern, professional interface matching Training App / Payroll style
   ============================================================================ */

/* ==========================================================================
   WHAT LIVES HERE — and what deliberately does NOT

   The design tokens moved OUT of this file. `public/css/v3.css` is the sole
   owner of the palette: every --color-*, --space-*, --text-*, --radius-*,
   --shadow-*, --font-*, --tone-* and every derived tint is declared there,
   once. This block used to carry a byte-identical copy of all 95 of them.
   Because v3.css links after style.css and both declare them at equal
   specificity, v3.css always won -- so the copy here was dead weight that
   still read like a source of truth, and it drifted the moment either file
   was edited (ADR-0084).

   What remains below is what v3.css does NOT declare:
     - color-scheme, so form controls and scrollbars follow the theme
     - app-shell metrics (--band-h, --ctl-h, --header-height, --max-content-width)
     - motion tokens (--transition-fast, --anim-*)

   Adding a colour? It goes in v3.css. Never re-declare a v3.css token here.
   ========================================================================== */
:root {
  color-scheme: light;

  /* Motion */
  --transition-fast: 0.15s ease;

  /* Shell bands: the sidebar's day block and the content card's header are
     the same height so the two columns line up across the seam. */
  --band-h: 57px;
  --ctl-h: 39px;
  --header-height: 48px;
  --max-content-width: 1400px;

  /* Breakpoints (for documentation - CSS can't use vars in @media) */
  /* --bp-mobile: 600px;   Primary mobile breakpoint — phones only; the
                            narrowest real iPad (mini, 744px) stays desktop */
  /* --bp-tablet: 1024px;  Tablet/small desktop breakpoint */
  /* --bp-small: 480px;    Small phones */

  /* Animation. app.js writes these per-element via setProperty, which wins. */
  --anim-slide-duration: 0.35s;
  --anim-hold-duration: 400ms;
  --anim-center-duration: 0.7s;
  --anim-center-delay: 0.15s;
  --anim-slide-distance: 12px;
  --anim-center-distance: 8px;
  --anim-easing: ease-out;
  --anim-center-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
}

[data-theme="dark"] {
  color-scheme: dark;
}

/* ==========================================================================
   PERMISSION-BASED VISIBILITY
   Hide elements with data-role or data-permission until JS verifies access
   ========================================================================== */
[data-role],
[data-permission] {
  display: none !important;
}

[data-role].perm-visible,
[data-permission].perm-visible {
  display: flex !important;
  justify-content: flex-start;
  align-items: center;
}

/* ==========================================================================
   BASE RESET
   ========================================================================== */

/* The `hidden` ATTRIBUTE must win over any component rule.
   Component CSS sets `display` on classes (.class-checkin-banner{display:flex},
   .class-header-cancelled-pill{display:inline-block}, ...), and a class selector
   outranks the user-agent's [hidden]{display:none}. Without this, every
   element the app hides via `el.hidden = true` stays visible — which is how
   an empty warning band and a CANCELLED pill on Open Gym reached the screen. */
[hidden] {
  display: none !important;
}

/* Hidden scrollbars are the global default ; matches the schedule sidebar's
   clean look. Specific surfaces that need visible scrolling cues can opt in
   with `scrollbar-width: thin` / a custom ::-webkit-scrollbar block. */
* {
  scrollbar-width: none;
}

*::-webkit-scrollbar {
  display: none;
}

/* THE WINDOW IS A FIXED PANE — every region scrolls inside its own box.

   `height: 100%` on both `html` and `body` caps the document to the viewport:
   the browser page itself never scrolls. Below that, `.app-viewport` and
   `.app-container.shell` are each bounded to exactly that height (see those
   rules below), so the nav rail, the schedule sidebar and the content card
   all resolve to a fixed height too — and each scrolls its own overflow
   internally instead of pushing the window taller. That is production's
   long-standing shape: three independent scroll windows side by side, never
   a page that grows.

   This reverses ADR-0078 ("the card grows and the page scrolls"), which
   pinned the window to a `min-height` floor with no ceiling on the theory
   that the handoff's own SSOT said so. The handoff contradicts itself on
   this point (see ADR-0078 §1); production's actual behavior — and
   `app-window.html`'s own "scrolling happens inside panes, never on the
   body" — is the version to build. See ADR-0079. */
html {
  height: 100%;
}

/* ==========================================================================
   APP WINDOW — V3 (design-system/app-window.html)

   The app is an OBJECT ON A BACKDROP, not a page bled to the browser edge.
   Past --win-max the window stops growing and centres; the backdrop absorbs
   the surplus width, so on a widescreen the rows and the schedule seam stay
   where staff learned them instead of stretching to 2000px.

   The body is the backdrop; .app-container is the window.
   ========================================================================== */
/* The type scale is owned ENTIRELY by v3.css — set no font-size here, on
   `html` or on `body`.
   v3.css pins `html, body { font-size: var(--text-md) }` with rem tokens, which
   puts the document root at 14px, so every rem resolves against 14 rather than
   16 and the app reads at 87.5% of the nominal px in the token comments. That
   is the authored design, not a bug: --text-sm renders 10.5px, --text-base
   11.375px. The side effect is intended too — every rem length in THIS file
   resolves against 14px as well, so surfaces V3 doesn't restyle shrink by the
   same factor.
   An earlier pass read the 87.5% as drift and removed the pin. It isn't drift.
   Do not "correct" it back to a 16px base, and do not convert the tokens to px
   — a px scale lets the host page's root size scale the design, which is the
   thing the rem scale exists to prevent. */
body {
  height: 100%;
  display: flex;
  flex-direction: column;
  color: var(--color-text);
  font-family: var(--font-sans);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* The backdrop the window sits on. v3.css:1487 authors the box (padding,
   background, display:flex, min-height:0); it is silent on sizing because in
   the design's preview the viewport's parent is a fixed-size frame. Here the
   parent is <body>, a flex column pinned to `height: 100%` above, so the
   viewport claims exactly the remaining space: plain `flex: 1` (basis `0`),
   not `flex: 1 0 auto`. An `auto` basis was ADR-0078's growing-page
   mechanism — it let the viewport start from its content and grow past the
   fold. Reverted along with the rest of ADR-0078; see ADR-0079. */
.app-viewport {
  flex: 1;
  /* The title bar stacks above the window (ADR-0158), so the backdrop is a
     column. Its top gutter is dropped in v3.css, the gutter's owner. */
  flex-direction: column;
}

.app-viewport > .app-container.shell {
  flex: 1;
}

/* KEYBOARD FOCUS.

   The design has no global focus rule. It styles focus per component, and each
   one uses the same shape: a ring drawn with box-shadow, so it follows the
   control's border-radius, with a 1.5px gap in the surface colour so the ring
   reads as separate from the control rather than a thickened border.
   `.btn:focus-visible` and `.btn-icon:focus-visible` are that ring verbatim;
   `.form-input:focus`, `.search-bar:focus-within` and `.cc-field:focus-within`
   are the same idea at the field scale.

   Production needs a fallback, because it has far more focusable things than
   the design's specimen page draws. What it had was a hard 2px SQUARE outline
   at 2px offset — which does not follow a radius, and which landed on TOP of
   every component ring above rather than instead of it. Anything with its own
   focus style wore two, one round and one square, and everything else wore a
   rectangle floating off its corners. That is the "focus looks broken
   everywhere" report.

   So: the fallback is now the design's own ring, and it yields to any component
   that defines its own (all of those set `outline: none` and are more specific).

   Inputs get NOTHING here. Every input in this app sits in a wrapper that takes
   the ring on `:focus-within` — `.attendance-search`, `.cc-field` — or is a
   `.form-input`, which rings itself. A rule on the input as well is what put a
   second ring inside the first. */
:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1.5px var(--color-bg-elevated), 0 0 0 4px var(--color-accent-tint);
}

/* The wrapper owns the ring; the input inside it must not draw its own.
   `.search-input-wrapper` and `.search-bar` were here too and are gone
   (ADR-0128): the first had zero emitters, and the second was the universal
   band's parallel copy of `.attendance-search`, which is now the one field. */
.attendance-search input:focus-visible,
.cc-field input:focus-visible {
  box-shadow: none;
}

/* ==========================================================================
   APP LAYOUT
   ========================================================================== */
/* The window's own surface is ELEVATED, matching the reference's .shell.
   Chrome (nav, sidebar, search band, gutters) all sit on this one surface,
   so the only thing separating regions is a 1px edge — never a tonal step.
   Production previously had this at --color-bg with the card at elevated,
   i.e. inverted, which read as a grey channel around a white card. */
/* The app window's frame lives in v3.css `.shell` (ADR-0084). The block that
   used to sit here restated it on `.app-container` — same element, since the
   markup is `class="app-container shell"` and there is only one of them — and
   v3.css links later at equal specificity, so every one of its declarations
   lost. It was inert, not merely redundant. Do not restore it: a second owner
   for the window chrome is what hid the broken 720px frame-drop for so long. */

/* ==========================================================================
   TITLE BAR (ADR-0158)
   The window's top gutter, grown into a 44px strip: the brand on the left,
   the System dot and the account on the right. It has NO surface, border or
   shadow of its own — it sits on the backdrop, so the window below stays the
   one card on the desk (the surface policy's "a boundary gets an edge or a
   shadow" has nothing to draw here). Its content lines up with the window's
   edges because it shares the window's width cap.
   ========================================================================== */
.app-titlebar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  height: 44px;
  flex-shrink: 0;
  width: 100%;
  max-width: var(--win-max);
  margin-inline: auto;
  padding: 0 2px;
  box-sizing: border-box;
}

.app-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

/* The small icon (img/brand/mark.png). The boot cover and the sign-in
   page show the TMV wordmark instead (the gate, ADR-0255). */
.app-brand-mark {
  height: 28px;
  width: auto;
  flex-shrink: 0;
  display: block;
}

/* The app's name, not a label: the largest step of the type scale, bold,
   sized to sit beside the 28px mark. */
.app-brand-name {
  font-size: var(--text-xl);
  font-weight: var(--font-bold);
  letter-spacing: -0.01em;
  color: var(--color-text);
  white-space: nowrap;
}

.app-titlebar-end {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* A ghost button on the backdrop: no fill until hovered or open, so the bar
   stays quiet and the window stays the loudest thing on screen. */
.titlebar-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 10px 0 8px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  font-family: inherit;
  font-size: var(--text-base);
  font-weight: var(--font-medium);
  line-height: 1;
  color: var(--color-text-secondary);
  cursor: pointer;
  white-space: nowrap;
}

.titlebar-btn:hover,
.titlebar-btn[aria-expanded="true"] {
  background: var(--color-bg-muted);
  color: var(--color-text);
}

.account-greeting-name {
  color: var(--color-text);
  font-weight: var(--font-semibold);
}

.titlebar-btn .bi-chevron-down {
  font-size: 0.7143rem;
  color: var(--color-text-muted);
}

.titlebar-sep {
  width: 1px;
  height: 18px;
  background: var(--color-border);
}

@media (prefers-reduced-motion: no-preference) {
  /* The health dot keeps its ADR-0044 attention pulse in its new home. */
  #system-btn .status-dot.is-attention {
      animation: sys-health-pulse 2s ease-in-out infinite;
    }
}

/* The account avatar is v3.css's `.avatar` (the Google photo, else the
   initial), at the title bar's size. */
.avatar {
  overflow: hidden;
}

.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.titlebar-btn .avatar {
  width: 26px;
  height: 26px;
  font-size: 0.7857rem;
}

/* The account menu IS the overflow menu (design-system/overflow-menu.js, the one
   engine): trigger + next-sibling menu, portaled, one-open-at-a-time, Escape
   and focus return, at the menu's own width. Only the identity block and a
   right-aligned value are new. */
.account-id {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px 10px;
}

.account-id .avatar {
  width: 34px;
  height: 34px;
  font-size: 1rem;
}

.account-id-name {
  font-size: var(--text-md);
  font-weight: var(--font-semibold);
  color: var(--color-text);
}

.account-id-meta {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  margin-top: 1px;
}

.menu-item-value {
  margin-left: auto;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* Main content body (scrollable area below search) */
/* The main content is a CARD, not a flat region: elevated surface, radius,
   and a 16px gutter on the right and bottom. The left edge stays flush with
   the seam so the selected sidebar card can bleed into it. */
.main-content-body {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  /* NOT a scroll container. The card's outline is a ::before inset `0 0 0 -1px`
     — one pixel OUTSIDE its own left edge, so that it meets the selected
     sidebar tab across the seam. `overflow-y: auto` here clipped exactly that
     pixel, which is why the card had no left border at all. The design's
     `.page` is `overflow: visible`; the scrolling belongs to the tab inside. */
  margin: 0 16px 16px 0;
  padding: 0;
  background: var(--color-bg-elevated);
  border-radius: var(--radius-md);
}

/* The header is the card's top band, so it carries the card's top corners.
   .class-checkin-header is camp's and the event tab's own pre-V3 header —
   same role, same treatment, so it takes the same rule rather than staying
   square against the rounded card underneath it. */
/* `.hdrD` is the Members card's top band and `.page` does not clip, so it needs
   the card's top corners too. This selector used to be listed twice -- a rename
   pass mapped `.cc-head` onto `.class-checkin-header` and collapsed the pair
   into a duplicate, dropping `.hdrD` on the way. */
/* Only a header that IS the card's top band takes the card's corners.
   `.class-checkin-header` was ALSO used as a repeated LIST ROW by the old
   events page, where a rounded top corner was meaningless — so this
   is scoped to the top-of-card position rather than to the class name. */
.main-content-body > .tab-content > .class-checkin-layout > .class-checkin-header,
.main-content-body > .tab-content > .class-checkin-header,
.main-content-body .hdrD {
  border-radius: var(--radius-md) var(--radius-md) 0 0;
}

/* THE PAGE HEADER: one height for every page's top band, the same set the
   corner rule above names (A1 destination, A2 utility, A3 table). Each used to
   size to its contents, 42 to 52px depending on whether the page had buttons,
   stat tiles or a meta line, so the page under it jumped as you moved between
   pages. `flex: none` because the tab is a scrolling column and a header that
   shrinks is a header that changes height. Desktop only: the phone's headers
   stack and are mobile.css's to size. */
html[data-mode="desktop"] .main-content-body > .tab-content > .class-checkin-layout > .class-checkin-header,
html[data-mode="desktop"] .main-content-body > .tab-content > .class-checkin-header,
html[data-mode="desktop"] .main-content-body .hdrD {
  height: var(--page-head-h);
  box-sizing: border-box;
  flex: none;
}

/* THIS is the card's scroll window — not `.main-content-body` itself (see the
   ::before note above). The card is bounded to a fixed height by the shell's
   grid row, and the active tab (header, body and all — production never
   pinned a header separately) scrolls its own overflow inside that height.
   Only the visible tab is a flex child worth sizing; the hidden ones are
   display:none. */
.main-content-body > .tab-content:not(.hidden) {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* ---------------------------------------------------------------------------
   PAGE ANATOMY — header band + body band, per the design's `.page`.

   `.page-body` is production's name for the design's `.cc-body`: the band under
   the header that carries the page's inset. The crosswalk maps `.cc-body` onto
   `.class-checkin-body`, which collapsed it with a SECOND design component of
   the same name (the dual-list grid) and is check-in-specific besides — so a
   settings page cannot use it without lying. One shared band, not twelve
   per-page paddings. Report the collision to the designer.

   The header is flush because it IS the card's top band; the body is inset.
   --------------------------------------------------------------------------- */
/* The body has no overflow of its own: the scrolling happens one level up, on
   the tab itself. So it grows to its content and never shrinks below it
   (`flex: 1 0 auto`): a shrinking body (the design's `flex:1;min-height:0`)
   was cut to the window's height and its content overflowed past it, which
   left the bottom padding in the middle of a long list. It still fills a short
   page. Camps, Settings, Events and Training each carried this fix on their own;
   it lives here, once, for every page. */
.main-content-body > .tab-content > .page-body {
  padding: var(--page-body-pad);
  flex: 1 0 auto;
}

/* A page that composes this anatomy is a flex column so the body can claim the
   card's height when the content is short. When it is long the tab scrolls. */
/* THE PAGE HEAD (ADR-0197): a page's header, and its tab bar when it has one,
   stay at the top of the page while the body scrolls. The tab is the scroll
   window (above), so both pin to its edge. This block is the ONE declaration of
   that, for every page: nothing opts in, because a page that forgot to opt in is
   the bug this replaced (`.is-pinned` was worn by four of the fifteen pages).

   The header pins at 0 and the tab bar directly beneath it, at `--page-head-h`,
   the header's own height (v3.css; the desktop rule above gives every header
   that height and the phone's single-line headers share it, mobile.css).
   `flex: none` because a flex item that shrinks is a head that changes height as
   the page scrolls. The header is the shell's `--z-chrome` tier, below every
   anchored surface and modal; its own anchored children (the Members filter
   panel, hung from `.hdrD`) paint inside its context, above the body. The tab bar
   sits one step BELOW the header so a menu hanging from the header paints over it.

   Three header homes, all reaching the tab as the scroll window:
     - a child of the tab (A2 / A3 and the table pages),
     - a child of `.class-checkin-layout`, which is `display: contents` (below), so
       it is the tab's child for sticky purposes,
     - a child of Search's `#search-results-content`, a block as tall as its rows. */
.main-content-body > .tab-content > .class-checkin-header,
.main-content-body > .tab-content > .class-checkin-layout > .class-checkin-header,
.main-content-body .hdrD {
  position: sticky;
  top: 0;
  z-index: var(--z-chrome);
  flex: none;
}

.main-content-body > .tab-content > :is(.settings-subnav, .account-subnav) {
  position: sticky;
  top: var(--page-head-h);
  z-index: calc(var(--z-chrome) - 1);
  flex: none;
}

/* How far down the page's own sticky things (a side rail, a toolbar) must start
   so the pinned head never covers them: the header, plus the tab bar when the page
   shows one. The ONE answer, read by every body-level `position: sticky`. */
.main-content-body > .tab-content {
  --page-pin-h: var(--page-head-h);
}

.main-content-body > .tab-content:has(> :is(.settings-subnav, .account-subnav):not([hidden])) {
  --page-pin-h: calc(var(--page-head-h) + var(--page-subnav-h));
}

/* A page composes ONE of the design's three header variants: A1 `.cc-head`
   (destination, category tint) inside `.class-checkin-layout`, A2
   `.cc-head.no-tint` (rollup / utility), or A3 `.hdrD` (table). A2 and A3 sit
   directly on the tab. */
/* DESCENDANT, not child. #tab-search's `.hdrD` is wrapped in
   `#search-results-content` (the Members card's wrapper), so `:has(> .hdrD)`
   never matched it — and the tab fell through to the 16px-inset rule below. The card's top band ended up floating inside a 16px
   frame instead of spanning the card, which is the single most visible way this
   page failed to look like the design. */
/* A1's header is NOT a direct child in production. Class, camp and event wrap
   their bands in `.class-checkin-layout` (the per-tab node production needs and
   the design has no equivalent of), so `:has(> .class-checkin-header)` skipped
   all three — the three destination pages, i.e. the app's main surface. They
   stayed `display: block`, the flex chain broke at the tab, and `.ov-wrap`'s
   `flex: 1` had nothing to resolve against: the whole page collapsed to its
   content height and left the bottom two thirds of the card empty. */
.main-content-body > .tab-content:has(.hdrD),
.main-content-body > .tab-content:has(> .class-checkin-header),
.main-content-body > .tab-content:has(> .class-checkin-layout) {
  display: flex;
  flex-direction: column;
}

/* Controls sit IN FLOW at the end of the field, not absolutely positioned
   over it — that is what let the input reserve 280px of dead padding. */
/* Filter chips: quiet by default; an ACTIVE one lifts to the elevated
   surface with a real border rather than shouting in colour — an applied
   filter is a fact, not an alert. */
.chip {
  cursor: pointer;
  white-space: nowrap;
}

/* ==========================================================================
   MAIN LAYOUT - SIDEBAR + CONTENT
   ========================================================================== */
/* .app-body removed - layout now uses flex-direction: row on .app-container */

/* Sidebar Navigation - top layer, casts shadow onto schedule */
/* No shadow: the V3 surface policy gives a boundary an edge OR a shadow,
   never both, and only L3 overlays get one (design-system/audit.html). */
/* Width is owned by the shell GRID column (.shell / .shell.nav-open in
   v3.css), not by this rule — setting a width here would overflow the
   48px icon-rail column. Everything else about the nav's chrome comes
   from .app-nav in v3.css. */
/* THE SHELL'S ROW IS BOUNDED, so every column stretches to one fixed height
   and scrolls its own overflow instead of pushing the window taller.

   `.shell` is a grid whose row template v3.css never declares, so the
   implicit row is `auto` — sized by its tallest item's CONTENT. On a long
   check-in page that grew the nav column to 2379px, dragging the Settings /
   Sign Out groups 2300px below the fold — the ADR-0078 era answered this by
   pinning only the nav rail (`position: sticky`) and letting the row itself
   stay unbounded. Reverted: the ROW is bounded instead, which fixes the nav
   without a sticky special case and makes the schedule sidebar and content
   card scroll internally too. See ADR-0079. */
.app-container.shell {
  grid-template-rows: minmax(0, 1fr);
  overflow: hidden;
}

.sidebar {
  background: var(--color-bg-elevated);
  display: flex;
  flex-direction: column;
  /* A grid item's automatic minimum is its content size, so without this the
     nav's own height would overflow the bounded row above instead of
     shrinking to it. */
  min-height: 0;
  overflow-y: auto;
  z-index: 102;
}

/* The collapse control is a `.nav-item` now, so it needs no rules of its own.
   `.sidebar-toggle` gave it a boxed, bordered, margined shape that read as a
   different component from the nine nav items beneath it — the design draws it
   as one of them (app-shell.html:180). */
.nav-item {
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  position: relative;
}

.nav-icon {
  font-size: 1.1429rem;
  width: 20px;
  text-align: center;
}

/* No .nav-label rule: the design authors none. The label's width is owned by
   the shell's grid column and its collapsed state by v3.css:1466's
   display:none — the max-width transition here was the pre-grid mechanism,
   still animating against a width it no longer controls. */
.nav-divider {
  height: 1px;
  background: var(--color-border);
  margin: var(--space-2) var(--space-3);
}

/* The spacer pushes the footer group to the bottom. It is a direct child of
   .app-nav (the flex column), exactly as in the design. */
.nav-spacer {
  flex: 1;
}

/* The health dot's attention pulse (ADR-0044) — opacity only. Its button is
 * #system-btn in the title bar (ADR-0158). */
@keyframes sys-health-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

/* Status dot (inline colored circle for worker/connection rows) */
.status-dot {
  margin-right: 6px;
  vertical-align: middle;
}

/* App-wide tooltip / inline-help (design-system/tooltip.js). One layer
   portaled to <body> so it is never clipped by overflow:hidden modals
   and sits above them. Hover + focus + tap; no decorative side stripe. */
.help-tip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  margin-left: 5px;
  padding: 0;
  border: none;
  background: none;
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  line-height: 1;
  cursor: pointer;
  border-radius: 50%;
  opacity: 0.65;
  vertical-align: middle;
  transition: opacity 0.12s ease, color 0.12s ease;
}

.help-tip:hover {
  opacity: 1;
  color: var(--color-accent);
  outline: none;
}

.help-tip:focus-visible {
  opacity: 1;
  color: var(--color-accent);
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb,var(--color-accent) 35%,transparent);
}

.tip-layer {
  position: fixed;
  z-index: 10100; /* above .modal-overlay (10000) */
  max-width: 300px;
  padding: var(--space-3);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--color-text);
  text-align: left;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.12s ease;
}

.tip-layer.is-visible {
  opacity: 1;
  visibility: visible;
}

.tip-layer .tip-lead {
  margin: 0;
  font-weight: var(--font-medium);
}

.tip-layer .tip-list {
  margin: 6px 0 0;
  padding-left: 1.05em;
  color: var(--color-text-muted);
}

.tip-layer .tip-list:empty { display: none; }

.tip-layer .tip-list li {
  margin: 3px 0;
}

/* ==========================================================================
   MAIN CONTENT
   ========================================================================== */
/* Transparent, so the window's one surface runs under the search band and
   through the gutter around the card (reference: .shell .app-main). */
/* No flex sizing: the shell is a GRID, so the column sizes this and the
   design's .app-main declares none. `min-height: 0` IS needed though — a grid
   item's automatic minimum is its content, so without it a long list makes this
   taller than its row, and every `flex:1; min-height:0` below resolves against
   an unbounded parent. Nothing scrolls, the card just grows. */
.main-content {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: transparent;
}

/* ==========================================================================
   CARDS
   ========================================================================== */
.card-body {
  padding: var(--space-4);
}

/* ==========================================================================
   BUTTONS
   ========================================================================== */
/* ==========================================================================
   BUTTONS — V3 (design-reference/design-system/buttons.html)

   One family. Variants change VISUAL WEIGHT only: fill, border and text
   color. Shape, size, radius and type scale are identical across every
   variant, so a new variant is never a new component.
   ========================================================================== */
.btn {
  cursor: pointer;
  /* An explicit list, never `all`: `all` animates layout properties too, so
     a button that changes width on state change slides instead of swapping. */
}

/* 22px — pinned to .pass-badge's height, the smallest fixed element in the
   app, so no button reads shorter than the badges beside it in a row. */
/* .btn-sm lives in v3.css (height 22px, padding 0 9px) — pinned to
   .pass-badge's height so no button reads shorter than the badges beside it in
   a row. A `.btn.btn-sm` override here used to out-specify it with 4px/8px
   padding and break that pin; both copies are gone so v3.css is the one owner. */

/* --- Primary: solid, sharp, no shadow --- */

/* --- Secondary / success / warning / danger: soft tint chips.
       Color-forward but quiet, so a row of them does not shout. Semantic
       fills are reserved for the one action that IS the row's purpose. --- */

/* Not in the handoff, but the family rule says a variant differs only in
   weight — so warning takes the same soft-tint shape as its siblings
   rather than staying a lone solid fill. */
.btn-warning {
  background: var(--color-warning-tint);
  color: color-mix(in srgb, var(--color-warning) 75%, var(--color-text));
}

.btn-warning:hover {
  background: color-mix(in srgb, var(--color-warning-tint) 76%, var(--color-warning));
}

.btn-warning:active {
  background: color-mix(in srgb, var(--color-warning-tint) 60%, var(--color-warning));
}

/* --- Outline & ghost: flat tertiary, fill on hover --- */

/* --- Icon button: square, bordered, matches outline --- */
.btn-icon {
  cursor: pointer;
  flex-shrink: 0;
}

/* ==========================================================================
   Sub-tabs and filter pills
   ========================================================================== */
/* ==========================================================================
   SETTINGS
   ========================================================================== */

/* Settings Sub-Navigation */
/* The shared sub-tab nav component (ADR — see docs/adr/, "one sub-tab
   pattern"). Base is the page's OWN top-level band (Settings / History /
   Waivers / Training) — a sibling of `.page-body`, not nested in its
   padding, exactly like Edit Account's Band B (`.account-subnav` /
   `.subnav-item`, v3.css): full-bleed, flush under the header, own
   horizontal inset. On a narrow screen every band scrolls sideways
   (50-history-admin-camps.css).
   One modifier is a NESTED strip inside a subtab's own padded content, so
   it cancels the band's full-bleed inset rather than inheriting it:
   - .tray — a flat, borderless, equal-width SEGMENTED control (pill fill on
     the active item) for a small, fixed set of tabs (waiver date tabs,
     event-manual required/optional) — a different control, so it keeps its
     own active/hover treatment below rather than inheriting the underline. */
.settings-subnav {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0 var(--space-1);
  padding: 0 var(--space-4);
  background: var(--color-bg-subtle);
  border-bottom: 1px solid var(--color-border);
  /* The band's height, as the token the pinned page head and every body-level
     sticky element read (`--page-subnav-h`, v3.css). The items already make it
     this tall; naming it here is what stops the two numbers drifting. */
  min-height: var(--page-subnav-h);
  box-sizing: border-box;
}

.settings-subnav.tray {
  border-bottom: none;
  padding: var(--space-1);
  margin-bottom: var(--space-4);
}

.settings-subnav-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 38px;
  padding: 0 var(--space-3);
  border: none;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
  font-weight: var(--font-medium);
  cursor: pointer;
  white-space: nowrap;
  transition: color .15s ease, border-color .15s ease;
}

.settings-subnav.tray .settings-subnav-item {
  flex: 1;
  justify-content: center;
  border-radius: var(--radius-md);
  border-bottom: none;
}

.settings-subnav-item:hover {
  color: var(--color-text);
}

.settings-subnav.tray .settings-subnav-item:hover {
  background: var(--color-bg-elevated);
}

.settings-subnav-item.active {
  color: var(--color-accent);
  border-bottom-color: var(--color-accent);
  font-weight: var(--font-semibold);
}

.settings-subnav.tray .settings-subnav-item.active {
  background: var(--color-accent);
  color: #fff;
  border-bottom-color: transparent;
}

/* `.mapping-row` and its three children are deleted (ADR-0130): a Square item
   mapping is a Settings list row, so it is `.type-item` like the other five.
   The `.mapping-section` grouping above stays — a mapping list is read by what
   it maps TO, so those bands are real. */
.text-xs {
  font-size: var(--text-xs, 0.75rem);
}

/* ==========================================================================
   Buttons, forms, settings frame
   ========================================================================== */
/* Toggle button group */
/* A segmented control sizes to its segments. `display: flex` made it a block
   that stretched to the container width, leaving a dead empty segment after the
   last button — invisible in the two original usages because both sit inside
   flex rows (which shrink an item to content anyway), obvious the moment one is
   used in a plain form column. */
.btn-group-toggle {
  display: inline-flex;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.btn-toggle {
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-sm);
  cursor: pointer;
  background: var(--color-bg);
  border-right: 1px solid var(--color-border);
  transition: all 0.15s ease;
}

.btn-toggle:last-child {
  border-right: none;
}

.btn-toggle input {
  display: none;
}

/* Button-element variant of the same family (data-action rows, e.g. the Member
   Logins claim-staging role toggle): the UA button border is reset to the
   family's. */
button.btn-toggle { appearance: none; border: 0; border-right: 1px solid var(--color-border); }

button.btn-toggle:last-child { border-right: none; }

/* ONE marker for "this segment is on" (ADR-0129), and it is a state a screen
   reader can read. `.is-active` was here and is deleted along with the six
   other spellings the census found (`.active` on the History presets, view
   picker and balance pair; Analytics swapping `.btn-ghost` for `.btn-primary`;
   `.selected`; `.is-on`; `.on`) — every one of them was invisible to assistive
   tech, so a four-button view picker announced four unlabelled buttons with
   nothing saying which view you were on.
   The `:has(input:checked)` half is not a second marker: a label wrapping a
   real radio (the Square environment picker) already carries the state
   natively, and `aria-pressed` on a label would be a lie about the element. */
.btn-toggle[aria-pressed="true"],
.btn-toggle:has(input:checked) {
  background: var(--color-accent);
  color: white;
  font-weight: var(--font-semibold, 600);
}

/* An icon-only segment is square: the family's horizontal padding is sized for
   a word, and four 16px glyphs in it drift apart. */
.btn-toggle-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-1) var(--space-2);
  color: var(--color-text-muted);
}

.btn-toggle-icon[aria-pressed="true"] { color: white; }

.btn-toggle-icon svg { width: 16px; height: 16px; }

/* A segmented control in a toolbar band sits beside `.btn-sm` buttons. */
.attendance-toolbar .btn-group-toggle { align-self: center; }

/* ==========================================================================
   PRICE LIST TAB - Settings
   ========================================================================== */

/* A labelled switch row: `.toggle-switch` + a stacked label/description.
   Named for the SHAPE, not the screen — the camp form and the Check-in prompts
   cards both use it, and a screen-specific prefix on a control another screen
   renders would be a lie about where it belongs. */
.toggle-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
}

/* The switch is a FIXED-size control (44x24) — it must never absorb the row's
   shrink. Settings' descriptions are one short line so it never showed there;
   the camp form's three-line explanation squeezed it to a sliver. */
.toggle-row .toggle-switch {
  flex-shrink: 0;
}

.toggle-row .toggle-label-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* A disabled switch means the row's precondition isn't met (the camp form's
   online-registration toggle needs a Square item name first) — the whole row
   dims so the reason line reads as the explanation, not as a separate note. */
.toggle-row:has(.toggle-switch input:disabled) {
  opacity: 0.55;
}

.toggle-row:has(.toggle-switch input:disabled) .toggle-slider {
  cursor: not-allowed;
}

/* A switch mid-save (`runToggleSave` in foundation/save-model.js disables the
   input, so the dim above already applies). This only swaps the cursor, so
   "the write is in flight" reads differently from "this row isn't available". */
.toggle-row[aria-busy="true"] .toggle-slider {
  cursor: progress;
}

/* ==========================================================================
   SETTINGS FRAME (ADR-0165) — 2026-10-12
   --------------------------------------------------------------------------
   The map, the one header, the sibling band, the intro line,
   the Needs-a-look card and the header's Save Changes. Everything every Settings page
   shares; nothing a single page owns. Components (.state-chip, .btn,
   .empty-state, .attendance-search) stay in v3.css and are only USED here.
   The phone rules live in mobile.css under html[data-mode="mobile"].
   No one-sided accent borders, no gradients, no pill radii.
   ========================================================================== */

/* ── The one header: one line for the map and every page ──
   Its height is every page header's (`--page-head-h`, 00-shell-layout-members.css)
   and its icon gap is `.class-header-name > .bi` (v3.css). Only the ellipsis is
   restyled here. Never justify-content or flex-wrap: those are the page-header
   owner's (ADR-0122 §5). */
.settings-header .class-header-name-row {
  min-width: 0;
  align-items: baseline;
}

.settings-header .class-header-name {
  overflow: hidden;
  text-overflow: ellipsis;
}

.settings-title-group {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  white-space: nowrap;
}

.settings-title-group:empty {
  display: none;
}

/* The header's actions are `.class-header-actions` (v3.css), Edit Account's own
   slot: it carries `margin-left: auto`, so they sit at the right edge without
   touching justify-content (ADR-0122 §5). It must not shrink under a long title
   (the name row ellipsizes instead). The page's link rides in its own span,
   which paints nothing while empty. */
.settings-header .class-header-actions {
  flex: none;
}

/* ── The map ──────────────────────────────────────────────────────────────
   Two columns, both starting level at the top: Find then the five main groups
   in ONE column on the left (quick-glance lists read best as a single
   column); the Needs-a-look card then the Admin group on the right. */
.settings-home-cols {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  gap: var(--space-4) var(--space-6);
  align-items: start;
}

.settings-home-main,
.settings-home-side {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

/* Find sits in its own wrapper: `.attendance-search` is `flex: 1` for a toolbar
   ROW, and as a flex item of this column it would collapse to its border. */
.settings-find {
  flex: none;
}

.settings-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.settings-group-name {
  margin: 0;
  padding: 0 2px;
  display: flex;
  gap: 6px;
  align-items: center;
  font-size: var(--text-xs);
  font-weight: var(--font-semibold);
  color: var(--color-text-secondary);
  text-transform: uppercase;
  letter-spacing: .05em;
}

.settings-group.is-admin .settings-group-name {
  color: var(--color-text-muted);
}

.settings-group-blurb {
  margin: 0;
  padding: 0 2px;
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

.settings-index {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.settings-index > li + li {
  border-top: 1px solid var(--color-border-subtle);
}

/* ── Needs a look: things that need taking care of, so each gets its full
   sentence on its own row. Not compacted. Full border, never a one-sided stripe. */
.settings-attention {
  padding: 10px 12px;
  border: 1px solid color-mix(in srgb, var(--color-warning) 35%, transparent);
  border-radius: var(--radius-md);
  background: var(--color-warning-tint);
}

.settings-attention-title {
  display: flex;
  gap: 6px;
  align-items: center;
  margin-bottom: 4px;
  font-weight: var(--font-semibold);
  color: var(--color-warning-ink);
}

.settings-attention ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.settings-attention-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 6px 4px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  font: inherit;
  color: var(--color-text);
  text-align: left;
  cursor: pointer;
}

.settings-attention-row:hover {
  background: color-mix(in srgb, var(--color-warning) 12%, transparent);
}

/* A row led by its own icon (the stock check's rules): icon and sentence side by side, from the left. */
.settings-attention-row.has-icon { justify-content: flex-start; align-items: flex-start; gap: 8px; }
.settings-attention-row.has-icon > .bi { flex: none; line-height: 1.5; }

/* An app's own icon at the size it has on a phone's home screen, so staff know it on sight (the stock check's "open the
   app on the gym phone"); the words beside it sit at its middle. */
.app-icon { flex: none; width: 52px; height: 52px; border-radius: 22%; box-shadow: var(--shadow-sm); }
.settings-attention-row.has-app-icon { align-items: center; }

.settings-attention-where {
  display: inline-flex;
  gap: 4px;
  align-items: center;
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  white-space: nowrap;
}

/* ==========================================================================
   Modals and the sheet
   ========================================================================== */
/* ==========================================================================
   MODAL SYSTEM - Single Source of Truth
   All modals use modal-utils.js factory. CSS defines consistent styling.
   ========================================================================== */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10000;
}

/* A modal holds its top once open (showModal, ADR-0232): it grows downward instead of re-centring, and once the
   person is working in it `.holds-foot` stops it at the viewport's foot so its body scrolls. A phone's modal is a
   bottom sheet (v3.css, MODAL AS A SHEET) that showModal never holds, so these rules need no width guard. */
.modal-overlay.holds-top { align-items: flex-start; }

.modal-overlay.holds-top > .modal { margin-top: var(--modal-top); }

.modal-overlay.holds-foot > .modal { max-height: var(--modal-room); }

/* Base modal container. `.modal` is the ONLY name for it: createModal emits
   it, and since ADR-0097 nothing else constructs a modal. The `.modal-content`
   / `.modal-container` synonyms that existed to cover hand-rolled overlays are
   deleted along with the last of those overlays. */
.modal {
  background: var(--color-bg-elevated);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  width: 100%;
  max-width: 520px;
  max-height: 90vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* Modal sizes */
.modal-sm { max-width: 320px; }

.modal-lg { max-width: 550px; }

.modal-wide { max-width: 700px; }

/* Two-column forms (.form-columns). At `wide` each column lands near 310px,
   which is narrower than a single-column modal and makes the layout feel
   cramped rather than roomier — the point of splitting into columns is to give
   each field MORE room, not less. */
.modal-xl { max-width: 880px; }

/* A form built around a drawing that has to be tapped precisely (the injury
   report's floor plan and body figure, side by side at the same height): at
   xl the figure is 183px wide, too small to hit an arm or a knee. */
.modal-xxl { max-width: 1040px; }

/* Header */
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}

.modal-header h3,
.modal-title {
  font-size: var(--text-md);
  font-weight: var(--font-semibold);
  color: var(--color-text);
  margin: 0;
}

/* Header with subtitle */

/* Warning header variant */
.modal-header-warning {
  background: var(--color-warning-tint);
  border-bottom-color: var(--color-warning);
}

.modal-header-warning .modal-title {
  color: var(--color-warning);
}

/* Close button */
.modal-close {
  width: 28px;
  height: 28px;
  border: none;
  background: none;
  font-size: 1.4286rem;
  color: var(--color-text-muted);
  cursor: pointer;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.modal-close:hover {
  background: var(--color-bg-subtle);
  color: var(--color-text);
}

/* Body */
.modal-body {
  padding: var(--space-4);
  overflow-y: auto;
  flex: 1;
}

.modal-body .form-group {
  margin-bottom: var(--space-3);
}

.modal-body .form-group:last-child {
  margin-bottom: 0;
}

/* Inside a .form-row the ROW owns the bottom margin (it's a grid with its own
   gap + margin), so the per-field margin would double the gap between rows. */
.modal-body .form-row .form-group {
  margin-bottom: 0;
}

/* A dialog's prose. `confirmModal` emits it alone, so it needs no spacing of
   its own there — `.modal-body`'s padding is the whole box. `promptForText`
   (ADR-0125) puts a field UNDER it, and without this the field's label sits
   flush against the last line of the sentence explaining it. Scoped with
   :not(:last-child) so a confirmation does not gain a bottom gap it has
   nothing to separate from. This is the first and only rule on the class. */
.modal-body .confirm-message:not(:last-child) {
  margin-bottom: var(--space-3);
}

/* The typed confirm under a confirmModal message (`typeToConfirm`): what to
   type, then the field. The one typed confirm in the app. */
.modal-body .confirm-type {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

/* Footer */
.modal-footer {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--color-border);
  gap: var(--space-2);
  flex-shrink: 0;
}

.modal-footer-left {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* `.modal-footer-right` was the hand-rolled twin of this; the log-entry edit
   modal was its last emitter and now goes through the factory's
   `.modal-footer-actions` (ADR-0097). Deleted with the other synonyms. */
.modal-footer-actions {
  display: flex;
  gap: var(--space-2);
  margin-left: auto;
}

/* Split body layout (two columns) */
.modal-split-body {
  display: flex;
  gap: var(--space-4);
}

.modal-column {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.modal-column-header {
  font-size: var(--text-sm);
  font-weight: var(--font-semibold);
  color: var(--color-text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border);
}

.modal-section-content {
  padding: var(--space-3);
}

/* `.modal-member-name`, `.checkin-class-indicator`, `.checkin-class-name`,
   `.checkin-options` / `.checkin-option` (+ its `.membership` / `.classes` /
   `.opengym` / `:hover` / `.shared-pass` variants and its `.option-label` /
   `.option-detail` children) were here and are DELETED (ADR-0132, ADR-0014).
   Every one of them was emitted by exactly one element: `#checkin-modal`, the
   static pick-a-pass modal in index.html that nothing could open — no markup
   anywhere carried `data-action="openCheckinModal"` or
   `data-action="showCheckinOptionsForPanel"`, the only two routes to it. The
   live pick-a-pass surface is the merged check-in popover
   (`showMiniSchedulePopup`) and, for the already-used-a-pass case,
   `showPassCheckinModal` in core/checkin-ui.js. */

/* ==========================================================================
   FILTER PILLS
   ========================================================================== */
/* `.filter-pills` / `.filter-pill` and its three `.active` tints were here and
   are DELETED (ADR-0129). A second of the census's "7 names for selected" with
   ZERO emitters — and the whitest-on-transparent of them, authored for a dark
   header bar this app no longer has. The live access-category filter is
   `.chip` (ADR-0128); the `@media` copy in 50-history goes with it. */

/* ==========================================================================
   UTILITY CLASSES
   ========================================================================== */
.hidden {
  display: none !important;
}

.font-semibold {
  font-weight: var(--font-semibold);
}

.mt-2 {
  margin-top: var(--space-2);
}

.mt-4 {
  margin-top: var(--space-4);
}

.mb-3 {
  margin-bottom: var(--space-3);
}

.mb-4 {
  margin-bottom: var(--space-4);
}

.gap-2 {
  gap: var(--space-2);
}

.gap-4 {
  gap: var(--space-4);
}

/* ==========================================================================
   CLASS CHECK-IN PAGE
   ========================================================================== */

/* No max-width: the outer .main-content-body card is now the width
   boundary (a 16px gutter to the seam), so a second cap here only left
   blank space the card's own edge already owns. */
/* No gap. The design stacks header / notice / toolbar / body FLUSH as direct
   children of `.page`; this wrapper exists only because production needs a
   per-tab node, so it must be transparent to layout. Each band already carries
   its own border-bottom, so a gap both doubled the separation and orphaned the
   border it sat under. */
/* So it is transparent to layout in the literal sense: `display: contents`
   removes its box and makes the header, notice, toolbar and body flex items of
   the TAB, which is the card's scroll window and a column that gives the body the
   remaining height (`flex: 1; min-height: 0`), exactly as the design's `.page`
   does for its direct children. A box of its own was bounded to the tab's height,
   so the header's `position: sticky` (ADR-0197) had a containing block only one
   screen tall and let go of the page after one screen of scroll. The wrapper
   survives as a node for the lock classes (`.is-locked`, `.is-camp-locked`) the
   descendant selectors read. */
.class-checkin-layout {
  display: contents;
}

/* Header. This used to be its OWN bordered, rounded card, because there was
   no outer card to sit inside of. Now that .main-content-body supplies that
   chrome (border, radius, the class-coloured edge), a second full border
   here read as a card nested inside a card. The header keeps its tinted
   wash — visually the same role .class-checkin-header's tint plays — but the boundary
   is gone and the top corners are carved to match the outer card's, via
   the shared rule below. */
.class-checkin-header {
  /* `position: relative` stays — the overflow menu and the cancelled pill are
     positioned against this header.
     NO justify-content and NO flex-wrap: v3.css composes this header as a plain
     row where `.class-header-actions` carries `margin-left: auto`, so the coach
     chip sits immediately after the class name and the controls push right.
     `space-between` leaked from here (v3.css never declares it) and shunted the
     coach into the right-hand cluster — measured at left=859 against the
     design's 401. */
  position: relative;
  border: 0;
  border-radius: 0;
  transition: background 0.2s ease, border-color 0.2s ease;
}

/* No tint overlay. v3.css puts the class colour in the header's OWN background
   as a 9% color-mix; this pre-port ::before laid a second 15% wash on top
   (~22.7% composite, about 2.5x the authored saturation) and, being a
   positioned descendant, painted over the header's text as well as its
   background -- the class name, meta and coach chip were all washed. */
.class-header-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.class-header-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* The class check-in header's meta runs INLINE ("6:00 PM · Kids"), because that
   header is a single line by design.

   It needs saying explicitly because of a name collision the V3 crosswalk
   introduced: production already owned `.class-header-meta` (the stacked block
   directly above, used by the camp and event headers), and the design system's
   inline `.cc-hmeta` was renamed onto that same name. v3.css declares only
   font-size/colour/white-space for it and no direction, so the stacked rule
   above won by default and the header grew to three lines (67px).
   Load order cannot resolve this — the two rules never conflict on the same
   property. Scoping is the fix.

   The camp header keeps the column direction via its own higher-specificity
   rule further down. */
.class-checkin-header .class-header-meta {
  flex-direction: row;
  align-items: baseline;
  gap: 0;
}

.class-header-time,
.class-header-age {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* .class-header-icon-btn and its :hover are owned by v3.css:1138-1139. This
   file's older copy declared the same box in different terms plus an
   opacity:.5 resting state and a transition the design never had — and since
   the V3 rule is silent on both, they survived it. */

/* Overflow menu (3-dots) */
.class-header-overflow {
  position: relative;
  display: none;
  margin-left: var(--space-1);
}

.class-header-overflow-menu {
  top: 100%;
  right: 0;
}

/* Portaled-to-body open state: positioned in viewport coords via inline
   styles set by setupClassHeaderActions. z-index sits above the
   schedule sidebar (~200) and below modal-overlay (10000). */
.class-header-overflow-menu.portaled {
  z-index: 5000;
}

.class-header-overflow-menu.visible {
  display: flex;
  flex-direction: column;
}

/* Permission gating ; shown via JS */
.class-header-overflow.perm-visible {
  display: block;
}

/* The header's STAT TILES — a horizontal cluster of tiles (editable name and
   age on the event header, counts on the camp header). Deliberately NOT
   `.class-header-stats`: the crosswalk gives that name to the design's
   `.cc-menu-stats`, the vertical muted list inside the overflow MENU, and
   v3.css declares it later at equal specificity — so sharing the name forced
   these tiles into a narrow muted column with menu padding. */
.class-header-stat-tiles {
  display: flex;
  gap: var(--space-3);
}

/* A page's numbers as one quiet line in its header (Camps, Events, Training, Duplicates): what used to be a band
   with no search in it. The facts' own markup (factsLine) carries the weight and the warn/ok colour.
   The header is one line, so when it is narrower than its parts the facts give way FIRST: they shrink eight times
   faster than the title row and clip with an ellipsis, instead of squeezing the title to nothing. The phone drops
   them (mobile.css). */
.class-header-stat-tiles.header-facts {
  align-items: center;
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  white-space: nowrap;
  flex: 0 8 auto;
  min-width: 0;
}

.class-header-stat-tiles.header-facts > * {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.stat-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--space-2) var(--space-3);
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  min-width: 60px;
  position: relative;
  z-index: 1;
}

.stat-item .stat-value {
  font-size: var(--text-lg);
  font-weight: var(--font-semibold);
  color: var(--class-color, var(--color-text));
}

.stat-item .stat-label {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

.stat-item.trend .stat-value.up {
  color: var(--color-success);
}

.stat-item.trend .stat-value.down {
  color: var(--color-danger);
}

/* THE HERO FIGURE: the one number a page is about (Vault's Summary: the month's profit), read before anything else on the
   page. Its label above in the card title's uppercase, the number at display size, the lines under it in the secondary
   colour. `data-tone` colours the number: ok green (a profit, a target met), error red (a loss). On the phone the number
   is the phone's large size (28), the biggest on its scale. */
.hero-figure { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }

.hero-figure-label { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.03em; }

.hero-figure-value { font-size: 2.8571rem; font-weight: var(--font-bold); line-height: 1.05; letter-spacing: -.02em; font-variant-numeric: tabular-nums; color: var(--color-text); }

.hero-figure[data-tone="ok"] .hero-figure-value { color: var(--color-success); }

.hero-figure[data-tone="error"] .hero-figure-value { color: var(--color-danger); }

.hero-figure-sub { font-size: var(--text-sm); color: var(--color-text-secondary); }

html[data-mode="mobile"] .hero-figure-value { font: var(--ph-t-large); letter-spacing: -.02em; font-variant-numeric: tabular-nums; }

/* A card's own lead figure under the page's hero (Vault Summary's costs): the same label, number and lines, the number a
   step smaller (28, the phone's title size on the phone) so the page's hero stays the first thing read. */
.hero-figure.is-secondary .hero-figure-value { font-size: 2rem; }

html[data-mode="mobile"] .hero-figure.is-secondary .hero-figure-value { font: var(--ph-t-title); font-weight: var(--font-bold); }

/* ==========================================================================
   Touch and hover
   ========================================================================== */
@media (hover: hover) and (pointer: fine) {
  /* ---------------------------------------------
     Touch Device Interactions
     --------------------------------------------- */

  /* Only apply hover effects on devices that support hover */
  .btn:hover,
    .nav-item:hover,
    .roster-item:hover,
    .attendance-item:hover {
      /* Existing hover styles apply */
    }
}

@media (hover: none) {
  /* Touch devices: active state instead of hover */
  .btn:active {
      transform: scale(0.97);
      opacity: 0.9;
    }

  .nav-item:active,
    .roster-item:active,
    .attendance-item:active {
      background: var(--color-bg-muted);
      transform: scale(0.99);
    }

  /* Remove hover backgrounds on touch */
  .btn:hover,
    .nav-item:hover {
      background: inherit;
    }

  .btn-primary:active {
      background: var(--color-accent-hover);
    }

  .btn-success:active {
      filter: brightness(0.95);
    }

  .btn-outline:active {
      background: var(--color-bg-subtle);
    }

  .btn-ghost:active {
      background: var(--color-bg-subtle);
    }

  /* --- FORM INPUTS: Prevent iOS zoom on focus ---
       Gated on touch capability, not viewport width. A landscape iPad is
       wide enough to sit outside the mobile breakpoint but is still a
       touchscreen — width-only gating missed it: a <16px input holds focus
       through the rotation's layout switch and iOS latches a zoomed-in
       scale. Element-type selector (not .form-input) so it also
       covers one-off fields that skip those classes (e.g. the main search bar)
       instead of chasing each one by name. !important
       because several scoped selectors elsewhere (#overview-search,
       etc.) set a smaller font at
       higher specificity and would otherwise win back the zoom on touch.
       Checkbox/radio/range/color/file are unaffected — they're sized via
       width/height, not font-size. */
  input,
    select,
    textarea {
      font-size: 1.1429rem !important;
    }
}

/* ==========================================================================
   Notifications, update banner, switch
   ========================================================================== */
.modal-header-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.modal-subtitle {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  font-weight: var(--font-normal);
}

.btn-block {
  width: 100%;
}

/* Toggle Switch Styles */
.toggle-switch {
  position: relative;
  display: inline-block;
  width: 44px;
  height: 24px;
}

.toggle-switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.toggle-slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  /* The OFF track was a hardcoded #ccc — invisible as a bug until the camp
     form put a switch whose OFF state MEANS something on a dark panel, where
     a light-grey pill was the brightest thing in the section and read as lit.
     Budgeted by job (ADR-0084): an off control still has to be findable, so
     it sits a container-edge step off the surface it lies on, in both themes. */
  background-color: color-mix(in srgb, var(--color-text-muted) 38%, var(--color-bg-muted));
  transition: 0.3s;
  border-radius: 24px;
}

.toggle-slider:before {
  position: absolute;
  content: "";
  height: 18px;
  width: 18px;
  left: 3px;
  bottom: 3px;
  background-color: white;
  transition: 0.3s;
  border-radius: 50%;
}

.toggle-switch input:checked + .toggle-slider {
  background-color: var(--color-accent);
}

.toggle-switch input:checked + .toggle-slider:before {
  transform: translateX(20px);
}

/* ==========================================================================
   UPDATE BANNER
   ========================================================================== */
.update-banner {
  position: fixed;
  bottom: var(--space-4);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  z-index: var(--z-notice);
  animation: slideUp 0.3s ease-out;
}

.update-banner span {
  font-size: var(--text-sm);
  font-weight: var(--font-medium);
  color: var(--color-text);
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}

/* ==========================================================================
   NOTIFICATION SYSTEM ; single source of truth for all toasts, banners,
   connection indicators, and update prompts. See notification-service.js.
   ========================================================================== */

/* Stack container ; owned and managed by notification-service.js */
#notification-stack {
  position: fixed;
  /* Mobile-first: above bottom nav, the screen's width less its gutters, each card centred in it and as wide as it
     needs. (It was `left: 50%` + `translateX(-50%)`, which let a card grow to only HALF the screen: a fixed box's
     shrink-to-fit width is measured from its left edge, so a card with two buttons squeezed its sentence to a
     word a line.) */
  bottom: calc(var(--mobile-nav-h, 0px) + var(--space-4) + env(safe-area-inset-bottom, 0px));
  left: var(--space-4);
  right: var(--space-4);
  display: flex;
  flex-direction: column-reverse; /* newest on top */
  align-items: center;
  gap: var(--space-2);
  z-index: 9999;
  pointer-events: none;
}

@media (min-width: 601px) {
  /* Desktop: pin to the bottom-right corner */
  #notification-stack {
      left: auto;
      right: var(--space-4);
      bottom: var(--space-4);
      align-items: flex-end;
      max-width: 420px;
    }
}

/* ---- Base notification card ------------------------------------------- */
.notification {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  pointer-events: auto;
  animation: notification-slide-in 0.3s ease-out;
  min-width: 0;
  max-width: 100%;
}

.notification__icon {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--color-accent);
}

.notification__text {
  font-size: var(--text-sm);
  font-weight: var(--font-medium);
  color: var(--color-text);
  flex: 1 1 auto;
  min-width: 0;
  word-wrap: break-word;
}

/* Two-line card: a headline plus a quieter detail line (buildNotificationCard's
   `detail`). It carries a sentence of explanation rather than a one-line
   status, so it reads a step larger than a toast, and its action sits on its
   own row under the text so a long label ("Clear Cache & Reload") can't squeeze
   the sentence into a narrow column. Rows: dot | text | ×, then the action. */
.notification--detailed {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--space-2) var(--space-3);
  align-items: start;
}

.notification--detailed .notification__icon {
  grid-area: 1 / 1;
  margin-top: 5px;
}

.notification--detailed .notification__text {
  grid-area: 1 / 2;
  font-size: var(--text-md);
  font-weight: var(--font-semibold);
}

.notification--detailed .notification__dismiss {
  grid-area: 1 / 3;
}

.notification--detailed .notification__action {
  grid-area: 2 / 2;
  justify-self: start;
}

.notification__detail {
  display: block;
  margin-top: 3px;
  font-size: var(--text-md);
  font-weight: var(--font-normal);
  color: var(--color-text-secondary);
}

.notification__action {
  flex-shrink: 0;
  padding: var(--space-1) var(--space-3);
  background: transparent;
  border: 1px solid currentColor;
  border-radius: var(--radius-md);
  color: var(--color-accent);
  font-size: var(--text-xs);
  font-weight: var(--font-semibold);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  cursor: pointer;
  transition: background 0.15s ease;
}

.notification__action:hover {
  background: color-mix(in srgb, currentColor 10%, transparent);
}

.notification__dismiss {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: transparent;
  border: none;
  color: var(--color-text-muted);
  font-size: 1.2857rem;
  line-height: 1;
  cursor: pointer;
  border-radius: var(--radius-sm);
}

.notification__dismiss:hover {
  color: var(--color-text);
  background: color-mix(in srgb, var(--color-text) 10%, transparent);
}

/* ---- Semantic tiers --------------------------------------------------- */
.notification--info {
  border-color: var(--color-accent);
}

.notification--info .notification__icon {
  background: var(--color-accent);
}

.notification--info .notification__action {
  color: var(--color-accent);
}

.notification--success {
  border-color: var(--color-success);
}

.notification--success .notification__icon {
  background: var(--color-success);
}

.notification--success .notification__action {
  color: var(--color-success);
}

.notification--warning {
  border-color: var(--color-warning);
}

.notification--warning .notification__icon {
  background: var(--color-warning);
}

.notification--warning .notification__action {
  color: var(--color-warning);
}

.notification--error {
  border-color: var(--color-danger);
}

.notification--error .notification__icon {
  background: var(--color-danger);
}

.notification--error .notification__action {
  color: var(--color-danger);
}

/* A card's second button (buildNotificationCard's `link`, the update banner's "What's new"): the same
   `.notification__action`, in the secondary colour, so the tier-coloured action stays the one to press. */
.notification .notification__action.is-quiet {
  color: var(--color-text-secondary);
}

/* ---- Pulse variant (for in-progress states like "Reconnecting…") ------ */
.notification--pulse .notification__icon {
  animation: notification-pulse 1.2s ease-in-out infinite;
}

/* ---- Dismiss animation ------------------------------------------------ */
.notification--dismissing {
  animation: notification-slide-out 0.25s ease-in forwards;
  pointer-events: none;
}

/* ---- Animations ------------------------------------------------------- */
@keyframes notification-slide-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes notification-slide-out {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(8px);
  }
}

@keyframes notification-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
  /* Respect reduced motion preference */
  .notification {
      animation: notification-fade-in 0.2s ease-out;
    }

  .notification--dismissing {
      animation: notification-fade-out 0.15s ease-in forwards;
    }

  .notification--pulse .notification__icon {
      animation: none;
      opacity: 0.7;
    }

  @keyframes notification-fade-in {
      from { opacity: 0; } to { opacity: 1; }
    }

  @keyframes notification-fade-out {
      from { opacity: 1; } to { opacity: 0; }
    }
}

.member-overflow-trigger .bi,
.class-header-icon-btn .bi,
.card-icon-btn .bi,
.btn-icon .bi,
.search-clear .bi {
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

.member-overflow-trigger .bi::before,
.class-header-icon-btn .bi::before,
.card-icon-btn .bi::before,
.btn-icon .bi::before,
.search-clear .bi::before {
  vertical-align: 0;
  line-height: 1;
}

/* ==========================================================================
   THE APP'S ONE SPIN KEYFRAME
   ==========================================================================

   What is left of the former ANALYTICS TAB block. Everything else in it —
   `.analytics-page` / `-date-range` / `-toggles` / `-summary-cards` /
   `-chart-card` / `-chart-container` / `-table*` / `-backfill-actions` /
   `-data-mgmt-card`, the whole `.summary-card*` family, every `.data-mgmt-*`
   and `.backfill-log-*` rule and their `max-width: 600px` block — is DELETED
   with the Reports page and `tab/analytics-ui.js` (ADR-0134). History →
   Reports draws its bars and tables from `.report-*` in
   50-history-admin-camps.css, on `.card` and v3.css's `.data-table`, so none
   of it had an emitter left. (The `.data-mgmt-*` / `.backfill-log-*` half had
   already had none for some time — it styled a backfill console deleted
   before this.)

   `.text-success` / `.text-danger` stay: they are the app-wide semantic text
   colours, used across app.js, camp, square, balance and notes.
   ========================================================================== */

/* Referenced by the boot splash (00-shell) and the two standalone stylesheets
   (visit.css, waiver.css); the staff app's own spinners use badge-pending-spin. */
@keyframes spin {
  to { transform: rotate(360deg); }
}

.text-success {
  color: var(--color-success);
}

.text-danger {
  color: var(--color-danger);
}

/* ==========================================================================
   Reduced motion
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  /* ==========================================================================
     REDUCED MOTION
     ========================================================================== */
  *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
}

/* ==========================================================================
   Components
   ========================================================================== */
/* ═══════════════════════════════════════════════════════════════
   TMV Members — V3 stylesheet
   SEEDED from design-system/base.css, 2026-08-21 (rebuild 6); 56
   design-system-only rules stripped. The handoff was the STARTING POINT,
   not a live upstream -- this file is now the app's own design system and
   evolves here. Re-porting a future drop means MERGING into these values,
   never overwriting them. Deliberate departures from the handoff are
   recorded in ADRs; the neutral contrast ramp is ADR-0084.
   Self-contained: every var() it references is defined in this file
   (except --badge-color / --badge-count-color / --class-color / --tc /
   --arrow-top, which are set inline at runtime).
   Link AFTER style.css; do not merge.
   ═══════════════════════════════════════════════════════════════ */
:root {
  /* Spacing — 4px base */
  --space-1: 4px;   --space-2: 8px;   --space-3: 12px;
  --space-4: 16px;  --space-5: 20px;  --space-6: 24px;
  --space-8: 32px;  --space-10: 40px; --space-12: 48px;

  /* App window geometry — the shell is one card on a backdrop:
     identical gutter on all four sides, capped width, centred. */
  --win-gutter: 16px;
  --win-max:    1600px;

  /* Type scale */
  --text-xs:   0.6875rem;  /* 11px */
  --text-sm:   0.75rem;    /* 12px */
  --text-base: 0.8125rem;  /* 13px */
  --text-md:   0.875rem;   /* 14px */
  --text-lg:   1rem;       /* 16px */
  --text-xl:   1.125rem;   /* 18px */
  --text-2xl:  1.5rem;     /* 24px */

  /* Per-device text-size preference (ADR-0081, GH #39). Every font-size in
     the app is rem (tokens above + exact-converted literals), and rem resolves
     against the html element ONLY — so this one multiplier scales all type
     while every px box metric (badge/button/row heights) stays put. The one
     deliberate exception is the schedule column's TEXT SHARE (--sched-col,
     ADR-0085): that column exists to hold one line of prose, so the part of
     its width that is text scales with this knob while its chrome does not.
     Applied on html alone (below): a multiplier on `html, body` would compound
     twice on body-inherited text. Driven pre-paint by js/design-system/ui-scale.js
     from localStorage; 1 / 1.1 / 1.2 are the only supported steps. */
  --ui-font-scale: 1;

  /* Weights */
  --font-normal: 400; --font-medium: 500;
  --font-semibold: 600; --font-bold: 700;

  /* Radius */
  --radius-sm: 4px; --radius-md: 6px;
  --radius-lg: 8px; --radius-xl: 16px; --radius-full: 9999px;

  /* ============================================================
     ELEVATION — the four layers, DECLARED (ADR-0120).
     This tier existed only as prose in a comment beside
     `.mini-schedule-popup` ("above chrome, below modals and toasts"), and
     nine anchored surfaces were written against it by memory: 20, 40, 100,
     320, 330, 1000, 5000, 10100 for one job. A number nobody can name is a
     number that drifts, so the tier is tokens and every anchored surface
     spells one of them.
       --z-chrome      the shell's own rails, panels and sticky headers
       --z-anchored    EVERY anchored surface: the ... menu, every popover,
                       every picker. They are one family and one layer; two
                       of them are never open at once (dismiss-layer.js), so
                       they never need to out-rank each other.
       --z-modal       the modal overlay
       --z-above-modal an anchored surface that must clear its OWN host
                       modal (the account search popover, ADR-0094), and
                       toasts and tooltips, which must clear everything.
       --z-gate        the full-screen sign-in gate: it covers the whole
                       shell, and every modal and notice clears it.
       --z-notice      the update banner: it clears EVERY overlay, the gate
                       and the modal included, because Reload now must stay
                       in reach on whatever screen a release finds the person.
     ============================================================ */
  --z-chrome: 100;
  --z-anchored: 1000;
  --z-gate: 9000;
  --z-modal: 10000;
  --z-above-modal: 10100;
  --z-notice: 10200;

  /* Every page's header is this tall: the destination header, the utility
     header and the table header alike, so moving between pages never shifts
     the page under it. One owner applies it (00-shell-layout-members.css,
     "THE PAGE HEADER"); the phone's single-line headers take it too
     (mobile.css sets it to 48px there). It is also where the pinned tab bar
     starts: the page head (ADR-0197) is the header at 0 and the tab bar at this
     offset. */
  --page-head-h: 52px;
  /* The `.settings-subnav` tab bar's height (38px items + its 1px rule): the
     floor `.settings-subnav` declares, and what a body-level sticky element adds
     to `--page-head-h` to start below the pinned head (`--page-pin-h`). */
  --page-subnav-h: 39px;
  /* The height of a control in a list toolbar band (ADR-0205): the search field
     and every control beside it (buttons of any size, the segmented toggles,
     selects, icon buttons, and a chip that sits in a row with a
     search field) are exactly this tall. One number, read by `.attendance-search`
     and by the control rules inside `.attendance-toolbar`; no page sets a height
     on a toolbar control of its own. */
  --toolbar-control-h: 36px;
  /* A page HEADER's controls are a small button's height: its buttons, a
     segmented toggle, a chip and a stat tile all stand 22px tall beside the
     title (owner 2026-10-16: "the same size as the buttons since it's in the
     header"). `.btn-sm` reads it too, so the header and the button are one number. */
  --header-control-h: 22px;

  /* The page body's inset (`.page-body`) and the search band's own vertical
     padding. A band's side inset IS the body's, so its separator can run the
     card's full width while its controls stay on the page content's edge (one
     rule below, `.page-body .attendance-toolbar`); its height is a field plus
     twice the vertical padding, for every band, with or without a field. */
  --page-body-pad: var(--space-4);
  --page-band-pad-y: 10px;

  /* Shadows. --shadow-sm is a card's: every card already draws its edge with a 1px border, so it is the soft lift alone (a
     second 1px ring outside the border drew the edge twice, 2px and blurred, ADR-0258). md and lg float over the page
     (popovers, sheets) with no border of their own, so the ring is their edge. --float-edge is that ring's colour, and
     an anchored card's pointer outlines itself in it, so the pointer and the card share one edge (ADR-0287). */
  --float-edge: rgba(28,25,23,.10);
  --shadow-sm: 0 1px 2px rgba(28,25,23,.06);
  --shadow-md: 0 2px 8px rgba(28,25,23,.07), 0 0 0 1px var(--float-edge);
  --shadow-lg: 0 8px 24px rgba(28,25,23,.09), 0 0 0 1px var(--float-edge);

  /* Font stacks */
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', system-ui, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* ============================================================
     TONE ENGINE — every neutral derives from TWO numbers.
       --tone-hue    : the hue shared by all greys (deg)
       --tone-chroma : saturation multiplier, 0 (true grey) .. 1 (full)
     tone-engine.js animates these from cool (morning) through neutral
     (midday) to warm graphite (evening). With no JS they rest at the
     values below and the whole system still looks correct.
     ============================================================ */
  --tone-hue: 250; /* @kind other */
  --tone-chroma: 0.7; /* @kind other */

  /* ============================================================
     THE EDGE BUDGET (ADR-0084). The handoff's neutrals were authored on
     a colour-accurate display and tuned for quiet. On the gym's check-in
     screens the structure disappeared: every container edge, divider and
     surface step measured between 1.05 and 1.83 contrast, against WCAG's
     3.0 non-text floor, while the shadow that used to back those edges up
     had been cut to a third of its old alpha. Type was never the problem.

     So an edge's contrast is now BUDGETED BY ITS JOB, stated in CIE L*
     against the surface it is drawn on. Three tiers, and nothing else:

       --color-border-subtle   divider INSIDE a container (row to row)  ~9
       --color-border          the edge OF a container                 ~14
       --color-border-strong   edges you must find without hunting     ~21
                               (inputs, popovers, menus)

     Surfaces keep >= 2.5 L* between any two tokens, so no two ever read as
     the same colour -- the old ramp had --color-border and --color-bg-muted
     0.2 apart, i.e. the same grey doing two different jobs.

     Only the LIGHTNESS terms moved. The two-number derivation (ADR-0072) and
     every chroma term are untouched, so the tone engine still drives the whole
     palette and the ramp holds its contrast at every hour of the day.
     ============================================================ */
  /* ---- Surfaces (light) — derived from the two numbers ---- */
  --color-bg:            oklch(0.975 calc(0.010 * var(--tone-chroma)) var(--tone-hue));
  --color-bg-elevated:   oklch(0.999 calc(0.005 * var(--tone-chroma)) var(--tone-hue));
  --color-bg-subtle:     oklch(0.936 calc(0.013 * var(--tone-chroma)) var(--tone-hue));
  --color-bg-muted:      oklch(0.908 calc(0.016 * var(--tone-chroma)) var(--tone-hue));
  --color-border:        oklch(0.875 calc(0.014 * var(--tone-chroma)) var(--tone-hue));
  --color-border-strong: oklch(0.820 calc(0.018 * var(--tone-chroma)) var(--tone-hue));
  /* Universal row-hover tint (audit follow-up 2026-08-19): a tint OFF the row's
     own surface, not the darker --color-bg-subtle swap, which read heavy in
     dark mode. Single source for every row hover in the app.
     Declared per THEME, because one percentage cannot serve both: --color-text
     flips from near-black to near-white, and the same 4% mix measured ΔL* 3.1
     in light against 4.0 in dark — the light hover was the weaker of the two
     and sat near the threshold of visible. 7% / 5% lands both at ~5, which is
     comfortably under the row divider it must not swallow (ΔL* 9.1 / 7.9).
     A hover is a transient pointer cue: it must be unmistakable but must stay
     quieter than the structure around it. */
  --hover-row: color-mix(in srgb, var(--color-text) 7%, transparent);
  /* Row divider — one step lighter than --color-border. Long lists (attendance,
     roster, search results) need a rule that separates without ruling. */
  --color-border-subtle: oklch(0.920 calc(0.012 * var(--tone-chroma)) var(--tone-hue));

  /* Chip ink. The count chip's fill is always a light mix of an arbitrary
     configured class color, in BOTH themes, so its digit and its edge are
     keyed to a fixed near-black rather than to --color-text: in dark mode
     --color-text is near-white and would lighten an already-light fill. */
  --color-chip-ink: #16130d;
  /* The INK that reads on a --color-warning tint. --color-warning (#f59e0b) is
     the fill hue and is far too light to be text on its own 22% wash, so three
     rules had each hardcoded the same #92400e separately (ADR-0127). */
  --color-warning-ink: #92400e;

  /* Text (light) */
  --color-text:           oklch(0.250 calc(0.016 * var(--tone-chroma)) var(--tone-hue));
  --color-text-secondary: oklch(0.450 calc(0.015 * var(--tone-chroma)) var(--tone-hue));
  --color-text-muted:     oklch(0.565 calc(0.014 * var(--tone-chroma)) var(--tone-hue));

  /* Accent — locked blue (approved) */
  --color-accent:      oklch(0.55 0.16 250);
  --color-accent-hover:oklch(0.50 0.17 250);
  --color-accent-fg:   #ffffff;
  --color-accent-tint: oklch(0.955 0.035 250);

  /* Semantic */
  --color-success:       #16a34a;
  --color-success-tint:  #f0fdf4;
  --color-warning:       #f59e0b;
  --color-warning-tint:  #fffbeb;
  --color-danger:        #b91c1c;
  --color-danger-tint:   #fef2f2;
  --color-info:          #0ea5e9;
  --color-info-tint:     #f0f9ff;

  /* Pass / class category colors — DO NOT CHANGE — staff muscle memory */
  --color-opengym:  #4caf50;
  --color-classes:  #2196f3;
  --color-athletic: #ffb82a;
  --color-private:  #f44336;
  /* The fifth access category. It had no token for as long as the other four
     did, so its one CSS consumer (`.badge-aerial`) hardcoded the same hex a
     second time, next to four siblings that read tokens. The value is pinned
     against `ACCESS_CATEGORY_COLORS.aerial` by status-label-drift.test.js —
     the constant is the source of truth, this is the stylesheet's copy. */
  --color-aerial:   #8c52ff;
  --color-camp:     #795548;
  --color-paused:   #e27902;
  --color-expired:  #8b0000;
  --color-camp-light: #a1887f;
  --color-camp-dark:  #5d4037;
  /* Synthetic / tag colors — previously hardcoded per page (audit 2026-08-18) */
  --color-addon:     #7c3aed;
  --color-staff:     #6b46c1;
  --color-sponsored: #0d9488;

  /* Category surface tints */
  --tint-opengym:  color-mix(in srgb, var(--color-opengym)  14%, var(--color-bg-elevated));
  --tint-classes:  color-mix(in srgb, var(--color-classes)  14%, var(--color-bg-elevated));
  --tint-athletic: color-mix(in srgb, var(--color-athletic) 22%, var(--color-bg-elevated));
  --tint-private:  color-mix(in srgb, var(--color-private)  14%, var(--color-bg-elevated));
}

[data-theme="dark"] {
  /* ---- Surfaces (dark) — same two numbers, dark lightness ramp ---- */
  --color-bg:            oklch(0.168 calc(0.012 * var(--tone-chroma)) var(--tone-hue));
  --color-bg-elevated:   oklch(0.232 calc(0.014 * var(--tone-chroma)) var(--tone-hue));
  --color-bg-subtle:     oklch(0.196 calc(0.013 * var(--tone-chroma)) var(--tone-hue));
  --color-bg-muted:      oklch(0.300 calc(0.016 * var(--tone-chroma)) var(--tone-hue));
  --color-border:        oklch(0.355 calc(0.016 * var(--tone-chroma)) var(--tone-hue));
  --color-border-strong: oklch(0.430 calc(0.020 * var(--tone-chroma)) var(--tone-hue));
  --color-border-subtle: oklch(0.300 calc(0.015 * var(--tone-chroma)) var(--tone-hue));
  --color-text:           oklch(0.945 calc(0.010 * var(--tone-chroma)) var(--tone-hue));
  --color-text-secondary: oklch(0.720 calc(0.012 * var(--tone-chroma)) var(--tone-hue));
  --color-text-muted:     oklch(0.625 calc(0.012 * var(--tone-chroma)) var(--tone-hue));
  /* Accent — locked blue, lifted for contrast on dark */
  --color-accent:       oklch(0.62 0.15 250);
  --color-accent-hover: oklch(0.68 0.15 250);
  --color-accent-fg:    #ffffff;
  --color-accent-tint:  oklch(0.300 0.055 250);
  --color-success-tint: oklch(0.300 0.05 145);
  --color-danger-tint:  oklch(0.300 0.06 25);
  /* The handoff's dark block declared only the success and danger tints, so
     info and warning fell through to their LIGHT near-white values in dark
     mode (verified in the browser: #f0f9ff / #fffbeb). style.css HAD the right
     dark values, but :root and [data-theme="dark"] have equal specificity and
     v3.css links later, so the light ones won. Ported here, where the dark
     block that actually wins can hold all four. Hues match the semantic
     colours: info #0ea5e9 is h237, warning #f59e0b is h70. */
  --hover-row: color-mix(in srgb, var(--color-text) 5%, transparent);
  --color-info-tint:    oklch(0.300 0.05 230);
  --color-warning-tint: oklch(0.300 0.05 75);
  /* The ink for text ON the warning tint. Declared once for the light theme
     (#92400e, dark brown) and never given a dark value, so it stayed dark brown
     on the dark tint (Important-note flag, the Settings Needs-a-look card). */
  --color-warning-ink: oklch(0.83 0.12 75);
  /* The "gone" tone (a Serious injury, an expired pass, a withdrawn record). Declared once for the light theme
     (#8b0000, dark red) and never given a dark value, so its dot and word sat at 1.7:1 on the dark card and could
     barely be seen. Lifted to the lightness its siblings (--color-paused, --color-success) already have on dark. */
  --color-expired: oklch(0.66 0.19 25);
  --shadow-sm: 0 1px 2px rgba(0,0,0,.45);
  --float-edge: rgba(255,255,255,.07);
  --shadow-md: 0 2px 8px rgba(0,0,0,.50), 0 0 0 1px var(--float-edge);
  --shadow-lg: 0 8px 24px rgba(0,0,0,.55), 0 0 0 1px var(--float-edge);
}

/* ================================================================
   BASE RESET
   ================================================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--text-md);
  font-weight: var(--font-normal);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* The text-size multiplier lives on html ONLY — rem resolves against html, so
   this scales every rem font-size once; body keeps the plain token above. */
html { font-size: calc(var(--text-md) * var(--ui-font-scale, 1)); }

button { font-family: inherit; cursor: pointer; }

/* ================================================================
   SHARED LAYOUT UTILITIES
   ================================================================ */
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }

@media (max-width: 640px) {
  .two-col { grid-template-columns: 1fr; }
}

/* ================================================================
   BUTTONS (buttons.html)
   ================================================================ */
.btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  font-weight: var(--font-semibold);
  letter-spacing: -0.003em;
  white-space: nowrap;
  border: 1px solid transparent;
  line-height: 1.2;
  -webkit-user-select: none; user-select: none;
  transition: background .15s ease, border-color .15s ease,
              box-shadow .15s ease;
}

.btn .bi { font-size: 1.08em; line-height: 0; }

.btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1.5px var(--color-bg-elevated), 0 0 0 4px var(--color-accent-tint);
}

.btn:disabled, .btn[aria-disabled="true"] {
  opacity: .45; pointer-events: none; box-shadow: none; transform: none;
}

/* 22px — pinned to .pass-badge's height, the smallest fixed element in the app,
   so no button reads shorter than the badges it sits beside in a row. */
.btn-sm { height: var(--header-control-h); padding: 0 9px;  font-size: var(--text-sm);  gap: 4px; }

.btn-xs { padding: 2px 7px;  font-size: var(--text-xs);  gap: 3px; }

.btn-lg { padding: 8px 16px; font-size: var(--text-md);  gap: 7px; }

/* --- Primary: solid, sharp, no border --- */
.btn-primary {
  background: var(--color-accent); color: var(--color-accent-fg);
  border-color: var(--color-accent);
}

.btn-primary:hover { background: var(--color-accent-hover); border-color: var(--color-accent-hover); }

.btn-primary:active { background: color-mix(in srgb, var(--color-accent-hover) 88%, #000); border-color: color-mix(in srgb, var(--color-accent-hover) 88%, #000); }

/* --- Secondary / success / danger: soft tint chips — color-forward, no shadow --- */
.btn-secondary {
  background: var(--color-accent-tint); color: var(--color-accent);
}

.btn-secondary:hover { background: color-mix(in srgb, var(--color-accent-tint) 78%, var(--color-accent)); }

.btn-secondary:active { background: color-mix(in srgb, var(--color-accent-tint) 62%, var(--color-accent)); }

.btn-success {
  background: var(--color-success-tint); color: var(--color-success);
}

.btn-success:hover { background: color-mix(in srgb, var(--color-success-tint) 76%, var(--color-success)); }

.btn-success:active { background: color-mix(in srgb, var(--color-success-tint) 60%, var(--color-success)); }

.btn-danger {
  background: var(--color-danger-tint); color: var(--color-danger);
}

.btn-danger:hover { background: color-mix(in srgb, var(--color-danger-tint) 78%, var(--color-danger)); }

.btn-danger:active { background: color-mix(in srgb, var(--color-danger-tint) 62%, var(--color-danger)); }

/* --- Outline & ghost: flat tertiary, fill on hover --- */
.btn-outline { background: transparent; color: var(--color-text); border-color: var(--color-border-strong); }

.btn-outline:hover { background: var(--color-bg-subtle); }

.btn-outline:active { background: var(--color-bg-muted); }

.btn-ghost { background: transparent; color: var(--color-text-secondary); border-color: transparent; }

.btn-ghost:hover { background: var(--color-bg-subtle); color: var(--color-text); }

.btn-ghost:active { background: var(--color-bg-muted); }

/* --- Icon button: matches outline, square, sharp --- */
.btn-icon {
  width: 28px; height: 28px; padding: 0;
  background: transparent; border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text-secondary); font-size: 1rem;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background .15s ease, border-color .15s ease;
}

.btn-icon:hover { background: var(--color-accent-tint); color: var(--color-accent); border-color: var(--color-border-strong); }

.btn-icon:active { background: color-mix(in srgb, var(--color-accent-tint) 78%, var(--color-accent)); }

.btn-icon:focus-visible { outline: none; box-shadow: 0 0 0 1.5px var(--color-bg-elevated), 0 0 0 4px var(--color-accent-tint); }

/* --- Button group: sharp, bordered, compact segmented row (View / Edit / ⋯) ---
     Stays neutral/bordered rather than tinted or bulky — built for dense rows. */
.btn-group { display: inline-flex; }

.btn-group .btn {
  border-radius: 0;
  margin-left: -1px;
  background: var(--color-bg-elevated);
  color: var(--color-text-secondary);
  border-color: var(--color-border-strong);
  font-weight: var(--font-medium);
}

.btn-group .btn:hover { background: var(--color-bg-subtle); color: var(--color-text); z-index: 2; }

/* Per-corner longhand, not the `border-radius` shorthand — regardless of how
   many buttons end up in the group. A button that is BOTH :first-child AND
   :last-of-type (the group has exactly one button) needs BOTH pairs of
   corners rounded; the shorthand version let whichever rule came second
   overwrite the first entirely, so a solo button — or a button that's the
   only one VISIBLE, e.g. the overflow trigger once a responsive rule hides
   its sibling — ended up rounded on only one side instead of all four. */
.btn-group .btn:first-child {
  margin-left: 0;
  border-top-left-radius: var(--radius-sm);
  border-bottom-left-radius: var(--radius-sm);
}

/* `:last-of-type`, not `:last-child`: production's row group holds the overflow
   MENU after its trigger (the toggle finds it as nextElementSibling), so the
   last CHILD is a div and the last button lost its right radius entirely — a
   segmented control square on both ends. The rule always meant "the last
   button". */
.btn-group .btn:last-of-type {
  border-top-right-radius: var(--radius-sm);
  border-bottom-right-radius: var(--radius-sm);
}

.btn-group .btn:focus-visible { z-index: 3; }

/* --- More below: the round arrow at the bottom centre of a scroll window ---
     design-system/scroll-hint.js inserts it after every surface it lists and
     writes left/top at the scroller's bottom centre; it is absolutely placed,
     so showing it moves nothing. Same skin as .btn-icon (elevated surface,
     strong border, accent on hover), round because it floats over content.
     ADR-0226. */
.scroll-hint {
  --scroll-hint-inset: var(--space-3);
  position: absolute;
  z-index: var(--z-chrome);
  width: 32px; height: 32px; padding: 0;
  border-radius: 50%;
  background: var(--color-bg-elevated); border: 1px solid var(--color-border-strong);
  box-shadow: var(--shadow-md);
  color: var(--color-text-secondary); font-size: 1rem;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  opacity: 0; pointer-events: none;
  transform: translate(-50%, calc(-100% - var(--scroll-hint-inset) + 6px));
  transition: opacity .15s ease, transform .15s ease, background .15s ease, color .15s ease;
}

.scroll-hint.is-shown {
  opacity: 1; pointer-events: auto;
  transform: translate(-50%, calc(-100% - var(--scroll-hint-inset)));
}

.scroll-hint:hover { background: var(--color-accent-tint); color: var(--color-accent); }

.scroll-hint:active { background: color-mix(in srgb, var(--color-accent-tint) 78%, var(--color-accent)); }

@media (prefers-reduced-motion: reduce) {
  /* On a phone the hint takes a finger: its 44px band is the field clear's rule (below, beside .search-clear). */
  .scroll-hint { transition: none; }
}

a { color: var(--color-accent); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }

a:hover { color: var(--color-accent-hover); }

/* ================================================================
   NAV (schedule-cards.html also uses)
   ================================================================ */
.nav-list { display: flex; flex-direction: column; gap: 2px; }

.nav-item {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 8px 10px;
  background: transparent; border: 0; border-radius: var(--radius-md);
  color: var(--color-text-secondary);
  font-size: var(--text-base); font-weight: var(--font-medium);
  text-align: left; transition: background .15s;
}

.nav-item .nav-icon { font-size: 1.1429rem; width: 20px; text-align: center; }

.nav-item:hover { background: var(--color-bg-subtle); color: var(--color-text); }

.nav-item.active,
.nav-item[aria-expanded="true"] { background: var(--color-bg-subtle); color: var(--color-text); font-weight: var(--font-semibold); }

.nav-badge { margin-left: auto; background: var(--color-bg-muted); color: var(--color-text-secondary); font-size: 0.7143rem; font-weight: var(--font-semibold); padding: 1px 7px; border-radius: var(--radius-full); }

/* Status dots */
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }

.status-dot-ok { background: var(--color-success); }

.status-dot-warn { background: var(--color-warning); }

.status-dot-error { background: var(--color-danger); }

.status-dot-neutral { background: var(--color-text-muted); }

/* pinned to the 28px control tier (Filter/⋯) — 32px read too big for this bar */
.chip { display: inline-flex; align-items: center; height: 28px; box-sizing: border-box; background: var(--color-bg-subtle); border: 1px solid transparent; border-radius: var(--radius-sm); padding: 0 10px; font-family: inherit; font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--color-text-secondary); transition: background .15s; }

.chip:hover { background: var(--color-bg-muted); color: var(--color-text); }

/* A chip used as a toggle says so in the accessibility tree, not in a class
   (ADR-0129) — same marker as the segmented control, different skin. */
.chip[aria-pressed="true"] { background: var(--color-bg-elevated); border-color: var(--color-border-strong); color: var(--color-text); }

/* A chip carrying its own live count — the number and the control that opens
   it are one object, never a figure parked elsewhere in the header. Tabular
   figures so the chip does not jitter as the count ticks 99 -> 100. */
.chip-count { margin-left: 6px; font-weight: var(--font-semibold); color: var(--color-text); font-variant-numeric: tabular-nums; }

.chip[aria-pressed="true"] .chip-count { color: inherit; }

/* An icon-only chip: same family, square instead of labelled. */
/* Number keys (design-system/number-keys.js): 0 to 9 as equal keys, five to a row, under the box they fill. */
.chip-keys { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--space-1); margin-top: var(--space-2); }
.chip-keys > .chip { justify-content: center; font-variant-numeric: tabular-nums; }

.chip-icon { width: 28px; padding: 0; justify-content: center; font-size: 15px; }

/* The family's ghost weight — same box, no fill. Used for an ACTION sitting in
   a row of state chips (Clear), so it cannot be mistaken for a filter. */
.chip-ghost, .chip-clear { background: transparent; color: var(--color-text-muted); }

.chip-ghost:hover, .chip-clear:hover { background: transparent; color: var(--color-text); text-decoration: underline; }

/* The range menu (foundation/range-menu.js): which days a page shows. A .chip that says the range, and under it the
   named ranges and a From and To for a range of your own. History and Injuries. */
.range-menu { position: relative; }

.range-menu-list { position: absolute; top: calc(100% + 4px); right: 0; display: flex; flex-direction: column; }

.range-menu-list .overflow-menu-item.is-on { font-weight: var(--font-semibold); }

.range-menu-custom { display: grid; grid-template-columns: 44px 1fr; gap: 6px 8px; align-items: center; padding: 6px 8px; }

.range-menu-custom .form-label { margin: 0; font-size: var(--text-xs); }

/* ================================================================
   MEMBER TABLE (universal-search-results.html)
   ================================================================ */
/* --- The card, and the Universal Search Results header/subheader ---
   Chrome is production's (live style.css ~1229-1256): a 50px header on --color-bg-subtle whose
   title is small, uppercase and secondary. These replace earlier DS values (14px 18px padding,
   no header fill, text-md primary title) that had drifted from the live app.
   The card holds three rows and no nested pane: header, subheader, flush table. */
.card { background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow-sm); }

.card-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--space-4); background: var(--color-bg-subtle); border-bottom: 1px solid var(--color-border); height: 50px; box-sizing: border-box; }

.card-title { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.03em; margin: 0; }

.card-head-left { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }

.card-head-right { display: flex; align-items: center; gap: var(--space-2); }

/* the count is the card's one number: matches over the set being searched, tabular so it does
   not reflow as you type. It appears here and never also in the subheader. */
.card-count { font-size: var(--text-sm); color: var(--color-text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; }

.card-count strong { font-weight: var(--font-semibold); color: var(--color-text); }

/* card-LEVEL actions only (export, overflow); row actions never appear in the header */
.card-icon-btn { width: 28px; height: 28px; display: grid; place-items: center; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-bg-elevated); color: var(--color-text-muted); cursor: pointer; font-size: 0.9286rem; }

.card-icon-btn:hover { background: var(--color-bg-subtle); color: var(--color-text); }

/* Subheader: the readout of what you are looking at. The search band lives OUTSIDE the card,
   so with a query and two filters applied nothing inside the card said what it was showing.
   Not a second toolbar. Chips wrap to a second line and the row grows; it never scrolls
   sideways and never truncates a filter. min-height keeps the resting state the same height
   as the filtered one, so the table below does not move under the cursor. */
.card-subheader { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; min-height: 40px; padding: 7px var(--space-4); background: var(--color-bg-elevated); border-bottom: 1px solid var(--color-border); }

.sub-label { font-size: var(--text-sm); color: var(--color-text-muted); white-space: nowrap; }

/* an applied filter reads as a fact with a way out: key, value, and the x that drops it */
.sub-chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 4px 2px 9px; font-size: var(--text-sm); color: var(--color-text); background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: var(--radius-sm); white-space: nowrap; }

.sub-chip .k { color: var(--color-text-muted); }

.sub-chip .v { font-weight: var(--font-medium); }

.sub-chip button { width: 18px; height: 18px; display: grid; place-items: center; border: 0; border-radius: 3px; background: transparent; color: var(--color-text-muted); cursor: pointer; font-size: 0.7143rem; font-family: inherit; }

.sub-chip button:hover { background: var(--color-bg-muted); color: var(--color-text); }

.sub-clear { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; font-family: inherit; font-size: var(--text-sm); color: var(--color-text-secondary); background: transparent; border: 1px solid var(--color-border); border-radius: var(--radius-sm); cursor: pointer; white-space: nowrap; }

.sub-clear:hover { background: var(--color-bg-subtle); color: var(--color-text); }

.sub-sort { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; font-family: inherit; font-size: var(--text-sm); color: var(--color-text-secondary); background: transparent; border: 1px solid var(--color-border); border-radius: var(--radius-sm); cursor: pointer; white-space: nowrap; }

.sub-sort strong { font-weight: var(--font-semibold); color: var(--color-text); }

.sub-sort:hover { background: var(--color-bg-subtle); }

.sub-sort .bi { font-size: 0.7143rem; color: var(--color-text-muted); }

/* The neutral table skin, lifted out of .member-table 2026-09-12 so a second
   table cannot draw its own header band. A header row in the uppercase muted
   register over a bg-subtle band, a padded body cell with a hairline under it,
   and a hover highlight. Nothing else: a column width, a frame, a reflow and a
   min-width all belong to the table that needs them.
   Composed by MARKUP (`class="data-table member-table"`), never by adding a
   second name to these selectors — that would make the base two owners again,
   which is how .staff-table came to carry its own header typography.
   A row inside the body that is a heading rather than a record opts out of the
   hover with .table-section-row; without it the section break lights up under
   the cursor as though it could be clicked. `.member-table`'s own group
   header row predates this base and carries its own name (.group-header-row,
   app.js) rather than .table-section-row — listed here too so the base's
   hover doesn't outrank the group header's steady-state tint
   (80-groups-notifications-orders.css) the moment the cursor is over it;
   before this base existed, .member-table's own hover rule excluded it
   the same way (cleanup after admin-ui PR2, 2026-09-13). */
.data-table { width: 100%; border-collapse: collapse; font-size: var(--text-base); }

.data-table th { text-align: left; font-size: 0.7143rem; font-weight: var(--font-semibold); color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.08em; padding: 8px 14px; border-bottom: 1px solid var(--color-border); background: var(--color-bg-subtle); }

.data-table td { padding: 8px 14px; border-bottom: 1px solid var(--color-border); vertical-align: middle; }

/* A table's total: one row in <tfoot>, a rule above it, no rule under it, set in the text colour and semibold. (0,1,2):
   it outranks `.data-table td` (0,1,1) and its border-bottom. History's Months and Vault's money tables draw it. */
.data-table tfoot td { border-top: 1px solid var(--color-border); border-bottom: none; font-weight: var(--font-semibold); color: var(--color-text); }

.data-table tr:not(.table-section-row):not(.group-header-row):hover td { background: var(--hover-row); }

.avatar { width: 32px; height: 32px; border-radius: var(--radius-full); background: var(--color-bg-muted); color: var(--color-text-secondary); display: inline-flex; align-items: center; justify-content: center; font-size: 0.8571rem; font-weight: var(--font-semibold); flex-shrink: 0; }

.member-overflow { position: relative; }

/* `:not(.btn)` — production reuses this class as the BEHAVIOUR hook for the
   member row's segmented `...`, which is a real .btn.btn-sm. In the design the
   row's overflow is a plain button (data-row-more) and this class only ever
   means the quiet 28px icon, so the two never collide there. Without the guard
   the 28px box wins over .btn-sm's 22px and the segments go uneven. */
.member-overflow-trigger:not(.btn) { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: none; border-radius: var(--radius-sm); background: transparent; color: var(--color-text-muted); cursor: pointer; font-size: 1rem; opacity: .5; transition: opacity .15s, background .15s; }

.member-overflow-trigger:not(.btn):hover { opacity: 1; background: var(--color-bg-subtle); }

.member-overflow-menu { display: none; position: fixed; }

.member-overflow-menu.visible { display: flex; flex-direction: column; }

/* ================================================================
   FORMS — the single owner of the form control family.
   Consolidated here 2026-09-08 (ADR-0014: delete the loser, never
   out-specify it). Previous owners, all deleted in the same commit:
     10-forms-account-settings.css  .form-label, .form-input/.form-select
                                    base + :focus + field-state modifiers
                                    + :disabled + .form-input-sm
     50-history-admin-camps.css     .form-hint, .form-error, .form-group label
     20-settings-design-system.css  .form-field (dead — zero consumers)
   Two deliberate value changes, measured rather than slipped in:
     - the control steps to --text-md. It was --text-sm on .form-input
       (v3 won) and --text-base on .form-select (only 10-forms declared
       it), so inputs and selects on one row never matched. --text-sm is
       chip size; this is a field staff type a phone number into.
     - the label is --color-text-secondary. `.form-group label` (0,1,1)
       beat `.form-label` (0,1,0) everywhere it applied, so the label
       colour was wrapper-dependent. The vocabulary's value wins; the
       label is chrome, the value it labels is the data.
   `.form-group > label` rides the same rule because 97 labels in the app
   carry no class — one declaration, not a second owner.
   TWO MORE NAMES DELETED 2026-09-14 (ADR-0129), both synonyms rather than
   rivals: `.form-select` never once appeared in a rule without `.form-input`
   beside it — it was a second spelling of this declaration with 12 emitters —
   and `.form-control` (50-history) was the Bootstrap-era skin, a real second
   base at --text-sm on --color-bg with a 2px focus ring. Every emitter of
   both is `.form-input` now; a `<select>` carries the same class as the input
   beside it because it is the same control.
   ================================================================ */
.form-label,
.form-group > label:not(.form-checkbox) { display: block; margin-bottom: var(--space-1); font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--color-text-secondary); }

.form-input { width: 100%; box-sizing: border-box; padding: 7px 10px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-bg-elevated); font: inherit; font-size: var(--text-md); color: var(--color-text); }

.form-input::placeholder { color: var(--color-text-muted); }

.form-input:focus { outline: 0; border-color: var(--color-accent); box-shadow: 0 0 0 3px var(--color-accent-tint); }

.form-input-sm { padding: var(--space-1) var(--space-2); font-size: var(--text-sm); }

/* A read-only value in a field's control slot (ADR-0129) — the camp form's
   late-fee line, the pass bundle's Purchased and Remaining. It takes the
   control's vertical box so it lines up with the input beside it in a
   `.form-row`, and NOT its border or background, because a box that looks
   like an input but refuses the caret is the worse lie. The three surfaces
   that had spelled this as a disabled `<input>` also put it in the tab order
   for a number nobody may change. It had never had a base declaration at all:
   the camp form emitted `.form-static` against a rule scoped to a DIFFERENT
   modal, so it rendered as bare body text under a field label. */
.form-static { padding: 7px 0; font-size: var(--text-md); color: var(--color-text); }

/* Field states — COMPLETION, derived by design-system/field-state.js from the
   control's value. A template declares intent (`data-field-required`) and
   never a state class.

   The ramp used to be backwards: required-and-empty was danger red, so every
   untouched form screamed, optional-and-empty was amber, and something that
   was actually WRONG had no colour at all. That is why the border was written
   off in-code as "cosmetic only" — it was crying wolf on every empty field.

   Now it is monotonic. Nothing is quieter than a prompt, and a prompt is
   quieter than an error:
     optional + empty  -> nothing at all
     required + empty  -> a firmer edge. A prompt, not an error.
     required + filled -> nothing. The prompt's ABSENCE is the feedback;
                          a green border on every completed field is noise.
     aria-invalid      -> the only red, and only after a save attempt. */
.form-input.field-required { border-color: var(--color-border-strong); }

/* .field-complete and .field-optional deliberately have NO rule. They are
   still applied, because they are the state a test and a screen reader can
   read, but "done" and "optional and empty" are exactly the cases that
   should look like nothing at all. */

/* Validation. The ONE red, and the reason travels with it. */
.form-input[aria-invalid="true"] { border-color: var(--color-danger); background: color-mix(in srgb, var(--color-danger) 5%, var(--color-bg-elevated)); }

.form-input[aria-invalid="true"]:focus { border-color: var(--color-danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-danger) 18%, transparent); }

.form-input:disabled { border-color: var(--color-border) !important; background: var(--color-bg-muted) !important; box-shadow: none !important; }

/* The hint slot. One element, two states: it carries the help text, and
   it carries the reason when the control is invalid. */
.form-hint { display: block; margin-top: var(--space-1); font-size: var(--text-xs); color: var(--color-text-muted); }

[aria-invalid="true"] ~ .form-hint { color: var(--color-danger); }

/* Form-level error box (a whole form failed, not one field). */
.form-error { display: none; padding: var(--space-2) var(--space-3); margin-top: var(--space-3); font-size: var(--text-sm); color: var(--color-danger); background: color-mix(in srgb, var(--color-danger) 10%, transparent); border-radius: var(--radius-md); }

/* Autosave readout, for a control that saves itself with no button to press
   (`foundation/save-model.js`'s createAutosave). Hint scale and hint colour by
   default, because that is what it is: a line of text beside the controls. Not
   a `.state-chip` — a chip labels a RECORD's lifecycle state, and this reports
   the outcome of one write. Not a toast either: at a 500ms debounce, a toast
   per pause in typing is noise. Tone by text colour only; nothing moves in the
   row when it appears (it is laid out inline, never inserted mid-row). */
.save-indicator { display: inline-flex; align-items: center; gap: 5px; font-size: var(--text-xs); color: var(--color-text-muted); white-space: nowrap; }

.save-indicator[hidden] { display: none; }

.save-indicator.is-saved { color: var(--color-success); }

.save-indicator.is-failed { color: var(--color-danger); }

/* ONE checkbox — the `.form-checkbox` that `formField({type:'checkbox'})` has
   emitted all along with no stylesheet defining it, so the two type-modal
   checkboxes ("Grants all categories", "Identity tag") drew at browser
   defaults beside V3 controls. This is the single-checkbox control only; a
   pick-several list is a different component (foundation/multi-select.js).
   The label IS the control: the input sits inside it, so the row is a flex
   line rather than the block label chrome above — which is why the base
   `.form-group > label` rule excludes this class rather than being
   out-specified here. The text beside the box therefore takes the CONTROL's
   size and colour, not the label's.
   A native input with `accent-color`, not a drawn box: that is what every
   checkbox in this app was already, under six names. As of ADR-0129 it is the
   ONLY one — `.checkbox-label`, `.site-config-toggle`, `.toggle-label`,
   `.modal-checkbox-item`, `.recommendation-checkbox` and the zero-emitter
   `.access-checkbox` are all deleted and their emitters are here. What is NOT
   folded in is `.toggle-switch`: a sliding 44x24 switch whose change writes
   immediately is a different control from a box you tick before pressing
   Save, and the two read differently on purpose. The 2px nudge centres the 16px box on the first
   text line, so a wrapping label runs under itself and not under the box.
   The ring is the form vocabulary's own (.form-input:focus), so a keyboard
   user sees one ring shape across every control in a modal. */
.form-checkbox { display: flex; align-items: flex-start; gap: var(--space-2); font-size: var(--text-md); color: var(--color-text); cursor: pointer; }

.form-checkbox input[type="checkbox"] { flex: none; width: 16px; height: 16px; margin: 2px 0 0; accent-color: var(--color-accent); cursor: pointer; }

.form-checkbox input[type="checkbox"]:focus-visible { outline: 0; border-radius: 3px; box-shadow: 0 0 0 3px var(--color-accent-tint); }

.form-checkbox:has(input:disabled) { cursor: default; }

.form-checkbox input[type="checkbox"]:disabled { cursor: default; }

.form-checkbox input[type="checkbox"]:disabled ~ span { color: var(--color-text-muted); }

.form-checkbox > span { min-width: 0; }

/* The field. A base of min-width:0 only — a grid child without it refuses to
   shrink below its content and blows the track out. The label's own
   margin-bottom stays the single owner of the label-to-control rhythm; giving
   the wrapper a flex gap as well would double it on all 106 fields. */
.form-group { min-width: 0; }

/* Legacy two-column row, migrated verbatim from 10-forms so it has one owner.
   Deliberately NOT converted to the six-track grid below: `#schedule-modal
   .form-row` overrides its grid-template-columns, and a `grid-column: span 3`
   child inside a 1- or 2-track grid creates IMPLICIT columns rather than
   clamping, so it would silently break. `.form-row` is on the migration
   ledger in the ADR; new markup uses .form-grid. */
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); margin-bottom: var(--space-3); }

/* The field grid: SIX tracks, because six divides into halves (span 3) and
   thirds (span 2) and two columns cannot. A child defaults to a third, which
   is the density this form wants at full card width. */
.form-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 14px var(--space-4); align-items: start; }

.form-grid > * { grid-column: span 2; }

.form-grid > .span-3 { grid-column: span 3; }

.form-grid > .span-4 { grid-column: span 4; }

.form-grid > .span-6 { grid-column: span 6; }

/* A titled region inside a page (ADR-0117) — fields today (Edit Account),
   any content tomorrow: Settings and Admin adopt it for non-form sections
   (Analytics cards, System status, training cards) without a second name for
   the same job. Renamed from `.form-fieldset`/`.form-legend`, which only ever
   described Edit Account's own content, not the component's actual job.
   NOT `.form-section` — that name belongs to the waiver page's vocabulary
   (waiver-sheet.css, rendered live by the waiver template editor) and to a
   scoped rule in the edit-registration modal. Claiming it unscoped is the
   crosswalk collision that broke camp and event during the V3 port. */
.titled-section { display: flex; flex-direction: column; gap: var(--space-3); border: 0; padding: 0; margin: 0; }

/* :not(.hidden) on BOTH sides: the adjacent-sibling combinator matches DOM
   position, not visibility, so a hidden section (Passes' household-bundles
   block on a non-family account, e.g.) still counted as "the previous
   section" and added this gap in front of the next VISIBLE one, which then
   sat 20px further down than every other tab's first section. */
.titled-section:not(.hidden) + .titled-section:not(.hidden) { margin-top: var(--space-5); }

.section-legend { display: flex; align-items: center; gap: var(--space-3); width: 100%; padding: 0; }

.section-legend > .t { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }

/* The rule fills the gap between the title and any action cluster, so the
   section reads as one band without drawing a full-width divider. */
.section-legend > .r { flex: 1; height: 1px; background: var(--color-border-subtle); }

.section-legend > .a { display: flex; align-items: center; gap: var(--space-2); }

/* A statement of fact in the action cluster, for a section whose header has
   something to say as well as something to do (the household line on an
   account that belongs to one). */
.section-legend > .a > .legend-fact { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-md); color: var(--color-text-secondary); }

.section-legend > .a > .legend-fact strong { color: var(--color-text); font-weight: var(--font-semibold); }

/* A facts line (header-facts.js `factsLine`): "12 expected · 9 signed", the numbers in the text colour and tabular, an
   ok or warn tone on a number that needs it. The header's facts slot and the rows that say their numbers draw it. */
.facts-line { min-width: 0; }

.fact strong { font-variant-numeric: tabular-nums; color: var(--color-text); }

.fact.is-ok strong { color: var(--color-success); }

.fact.is-warn strong { color: var(--color-warning-ink); }

.fact-dot { margin: 0 8px; color: var(--color-text-muted); }

/* THE BAR: a track and its fill, the share riding `--bar-pct`. Under a number it is a 4px footnote; in a bar row
   (label, bar, number) it is the row's main object at 10px. The row's two outer columns are fixed widths
   (`--bar-label-w`, `--bar-n-w`) so every bar in a list starts and ends on the same line. History Reports and the
   Injuries page draw it. */
.bar { display: block; width: 100%; height: 4px; margin-top: 3px; border-radius: var(--radius-sm); background: var(--color-bg-muted); overflow: hidden; }

.bar-fill { display: block; height: 100%; width: var(--bar-pct, 0%); border-radius: inherit; background: var(--color-accent); }

/* A bar split in parts: one track, each `.bar-fill` a part of the whole side by side, its share riding `--bar-pct` and its
   colour `--bar-tone` (the colour its key wears elsewhere), at the bar row's 10px so it reads as the card's main object.
   Vault Summary's costs split into expenses and payroll draw it. A 2px gap of track between parts keeps each boundary crisp. */
.bar.is-split { display: flex; gap: 2px; height: 10px; }

.bar.is-split > .bar-fill { flex: none; border-radius: 0; background: var(--bar-tone, var(--color-accent)); }

.bar-list { display: flex; flex-direction: column; gap: var(--space-2); }

.bar-row { display: grid; grid-template-columns: var(--bar-label-w, 96px) minmax(0, 1fr) var(--bar-n-w, 32px); align-items: center; gap: var(--space-3); }

.bar-row-label { min-width: 0; font-size: var(--text-sm); color: var(--color-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.bar-row .bar { height: 10px; margin-top: 0; }

.bar-row-n { text-align: right; font-size: var(--text-sm); color: var(--color-text-secondary); font-variant-numeric: tabular-nums; }

/* A bar row that filters what it counts (the Injuries side card): the row is the button, pressed while its filter is on. */
button.bar-row { width: 100%; margin: 0 -6px; padding: 3px 6px; box-sizing: content-box; border: 1px solid transparent; border-radius: var(--radius-sm); background: none; font: inherit; text-align: left; cursor: pointer; }

button.bar-row:hover { background: var(--color-bg-subtle); }

button.bar-row[aria-pressed="true"] { background: var(--color-bg-elevated); border-color: var(--color-border-strong); }

button.bar-row[aria-pressed="true"] .bar-row-label { color: var(--color-text); font-weight: var(--font-medium); }

/* A CHOICE (ADR-0231, design-system/choice.js): one option in a list the person picks from (a pass bundle, the account
   that holds a pass, a person to tick). A bordered row the list's width: `.choice-main` holds the title and the muted
   line under it, `.choice-end` what sits at the right. A `<button>` picks one (aria-pressed) or acts, a `<label>` with a radio picks one in a form, a `<label>` that is also
   `.form-checkbox` ticks several, a `<div>` is an item already picked with its own controls. The picked state changes
   colours only and the text never wraps, so picking moves nothing. Below the width the end needs, the end drops under
   the main text rather than squeezing it. Declared after `.form-checkbox` on purpose: the row centres its content,
   where a lone checkbox aligns to its label's first line. */
.choice-list { display: flex; flex-direction: column; gap: var(--space-1); }

.choice-list.scroll { max-height: 264px; overflow-y: auto; }

.choice { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-3); width: 100%; min-height: 44px; box-sizing: border-box; padding: var(--space-2) var(--space-3); margin: 0; text-align: left; background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: var(--radius-md); font: inherit; color: var(--color-text); }

button.choice, label.choice { cursor: pointer; }

button.choice:hover, label.choice:hover { background: var(--color-bg-subtle); }

button.choice[aria-pressed="true"], label.choice:has(> input:checked) { border-color: var(--color-accent); background: var(--color-accent-tint); }

.choice > input[type="checkbox"] { margin: 0; }

/* An option that cannot be picked right now (a login that already has its own account): muted, no hover. */
label.choice:has(> input:disabled), label.choice:has(> input:disabled):hover { cursor: default; background: var(--color-bg-elevated); }

label.choice:has(> input:disabled) .choice-main { color: var(--color-text-muted); }

.choice > input[type="radio"] { flex: none; width: 16px; height: 16px; margin: 0; accent-color: var(--color-accent); cursor: pointer; }

.choice > input[type="radio"]:focus-visible { outline: 0; border-radius: 50%; box-shadow: 0 0 0 3px var(--color-accent-tint); }

.choice-main { flex: 1 1 0; min-width: min(9rem, 100%); display: flex; flex-direction: column; gap: 1px; }

.choice-title, .choice-meta { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.choice-title { display: flex; align-items: center; gap: var(--space-2); min-width: 0; font-weight: var(--font-medium); }

.choice-meta { font-size: var(--text-sm); color: var(--color-text-muted); }

.choice-end { display: inline-flex; align-items: center; justify-content: flex-end; gap: var(--space-2); margin-left: auto; font-size: var(--text-sm); color: var(--color-text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; }

.choice-end .form-input { width: auto; max-width: 220px; }

/* A count typed at a row's end (Stock's unopened boxes): wide enough for three digits, the digits right-aligned. */
.choice-end .form-input.is-count { width: 5.5em; text-align: right; }

/* A card with a description (a role, a severity): the text wraps, the input sits on the title's line. */
.choice.wraps { align-items: flex-start; }

/* ...while its end (a tag, a number, a count box, a button) stays on the card's centre line, as on a card that does
   not wrap, so the ends of a list make a straight column whatever wraps beside them. */
.choice.wraps > .choice-end { align-self: center; }

.choice.wraps > input { margin-top: 2px; }

.choice.wraps .choice-title, .choice.wraps .choice-meta { white-space: normal; }

/* A row whose action is irreversible (danger) or loud (warning): the ⋮ menu's items keep their tone as a sheet's rows.
   The tone is the text and the icon; the row keeps the family's shape (choiceHtml's `tone`). */
.choice.is-danger, .choice.is-danger .choice-title .bi { color: var(--color-danger); }

.choice.is-warning, .choice.is-warning .choice-title .bi { color: var(--color-warning-ink); }

/* The gap a menu's divider becomes between rows of a list. */
.choice-gap { height: var(--space-2); flex: none; }

/* ----------------------------------------------------------------
   MODAL AS A SHEET (ADR-0248) — createModal on a phone

   One construction, two presentations. Under 600px (`html[data-mode="mobile"]`) showModal / confirmModal add `.is-sheet`
   to the overlay and a `.modal-grab` bar to the modal, and this block draws what the desktop draws as a centred card as a
   bottom sheet instead: full width, rounded on top, the height of its content (`.modal-tall`, createModal's `tall`, holds a
   filter-as-you-type list at full height so the sheet does not resize with every keystroke), over a scrim that fades with
   it. Sheets stack: each is its own overlay in document order, and a tap on the scrim or a drag down closes the top one.
   The steps (`--ph-*`) are the phone's, declared once in mobile.css. Nothing here applies at any other width, so the
   desktop modal and the compact shell are exactly what they were.
   ---------------------------------------------------------------- */
/* The overlay covers the part of the screen the phone shows (keyboard-room.js stamps it on <html>), not the layout
   viewport, so a sheet sits on the keyboard rather than under it and its max-height shrinks to the room left. Before the
   first stamp, and while pinch-zoomed, the variables are unset and the overlay is the whole viewport as before. */
html[data-mode="mobile"] .modal-overlay.is-sheet {
  top: var(--vv-top, 0px); height: var(--vv-height, auto);
  align-items: flex-end; justify-content: stretch; padding: 0;
  background: transparent;
  transition: background var(--ph-t-slide) var(--ph-ease);
}

html[data-mode="mobile"] .modal-overlay.is-sheet.is-open { background: var(--ph-scrim); }

/* A window that must be answered before anything else (createModal's `urgent`): the page behind goes out of focus, on a
   desk and behind a phone's sheet once it is open. */
.modal-overlay.is-urgent, html[data-mode="mobile"] .modal-overlay.is-sheet.is-urgent.is-open { background: rgba(0, 0, 0, 0.55); -webkit-backdrop-filter: blur(10px) saturate(120%); backdrop-filter: blur(10px) saturate(120%); }

html[data-mode="mobile"] .modal-overlay.is-sheet.is-closing { pointer-events: none; }

/* The sheet is the containing block of what it holds (the scroll hint above all): its slide transform makes it one while it
   moves, and a box that is one only mid-slide puts an absolutely placed child in two places. */
html[data-mode="mobile"] .modal-overlay.is-sheet > .modal {
  position: relative;
  width: 100%; max-width: none; margin: 0;
  max-height: calc(100% - var(--ph-safe-t) - 28px);
  border-radius: var(--ph-r-sheet) var(--ph-r-sheet) 0 0;
  box-shadow: 0 -8px 32px rgba(0, 0, 0, .18);
  transform: translateY(100%);
  transition: transform var(--ph-t-slide) var(--ph-ease);
}

html[data-mode="mobile"] .modal-overlay.is-sheet.is-open > .modal { transform: none; }

html[data-mode="mobile"] .modal-overlay.is-sheet > .modal.modal-tall { height: calc(100% - var(--ph-safe-t) - 28px); }

html[data-mode="mobile"] .modal-overlay.is-sheet .modal-grab {
  flex: none; width: 36px; height: 5px; margin: 8px auto 0; border-radius: var(--radius-full); background: var(--color-border-strong);
  touch-action: none;
}

html[data-mode="mobile"] .modal-overlay.is-sheet .modal-header { padding: 8px 8px 10px 20px; border-bottom: 0; touch-action: none; }

/* A long name breaks inside the title rather than pushing the close past the edge (a 320px phone). */
html[data-mode="mobile"] .modal-overlay.is-sheet .modal-title { min-width: 0; font: var(--ph-t-title); margin-top: 10px; overflow-wrap: anywhere; }

html[data-mode="mobile"] .modal-overlay.is-sheet .modal-close { flex: none; width: var(--ph-ctl); height: var(--ph-ctl); }

html[data-mode="mobile"] .modal-overlay.is-sheet .modal-body { padding: 0 var(--ph-inset) 16px; overscroll-behavior: contain; }

html[data-mode="mobile"] .modal-overlay.is-sheet .modal-footer { padding: 12px var(--ph-inset) calc(12px + var(--ph-safe-b)); }

/* On the keyboard the footer is clear of the home indicator, so it keeps no room for it. */
html[data-mode="mobile"][data-keyboard="open"] .modal-overlay.is-sheet .modal-footer { padding-bottom: 12px; }

@media (prefers-reduced-motion: reduce) {
  html[data-mode="mobile"] .modal-overlay.is-sheet, html[data-mode="mobile"] .modal-overlay.is-sheet > .modal { transition: none; }
}

/* Cards side by side, read left to right, one column when the row is narrower than two. */
.choice-list.across { flex-direction: row; flex-wrap: wrap; gap: var(--space-2); }

.choice-list.across > .choice { flex: 1 1 200px; width: auto; }

/* Tiles: square cards in a grid, the title (an icon, drawn large) over its name (a style's icon, ADR-0243). */
.choice-list.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); gap: var(--space-2); }

.choice-list.tiles > .choice { flex-direction: column; flex-wrap: nowrap; justify-content: center; gap: var(--space-1); min-height: 64px; padding: var(--space-2) var(--space-1); text-align: center; }

.choice-list.tiles .choice-main { flex: none; width: 100%; min-width: 0; align-items: center; }

.choice-list.tiles .choice-title { justify-content: center; font-size: 1.75rem; line-height: 1; }

.choice-list.tiles .choice-meta { font-size: var(--text-xs); }

/* Joined: one box, a line between rows (a list of people to tick). Picked rows keep the tint; the box keeps the border. */
.choice-list.joined { gap: 0; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-bg-elevated); overflow: hidden; }

.choice-list.joined.scroll { overflow-y: auto; }

.choice-list.joined > .choice { border: 0; border-radius: 0; border-bottom: 1px solid var(--color-border-subtle); }

.choice-list.joined > .choice:last-child { border-bottom: 0; }

.choice-list.joined > button.choice[aria-pressed="true"], .choice-list.joined > label.choice:has(> input:checked) { border-color: var(--color-border-subtle); }

/* A page made of sections. Owns the rhythm between them so the markup needs
   no .mt-3 / .mb-4 utilities. */
.form-page { display: flex; flex-direction: column; gap: var(--space-5); }

/* One rhythm between sections on a page (ADR-0216). `.form-page` has a gap AND the next `.titled-section` adds the
   margin above, so Personal read 40px between sections. The container's gap is the one owner: the margin is dropped
   wherever a gap already does the job. Written with the same `:not(.hidden)` pair so it wins. (Personal's individual and
   household halves are `.form-page`s of their own inside the tab's: each owns the gap between its own sections.) */
.form-page > .titled-section:not(.hidden) + .titled-section:not(.hidden) { margin-top: 0; }

/* A section that is shown but not available yet: muted, no pointer, no focus. */
.titled-section.is-disabled { opacity: .5; pointer-events: none; user-select: none; }

/* MAIN BESIDE A SIDE CARD (ADR-0221): a page's list on the left and a narrow card of secondary facts on the right
   (Injuries' breakdowns, Staff's roles card, Training's progress and help cards). One column, the side card under the
   main, below 1100px: the desktop shell spends ~330px of the window on the nav rail and the day's schedule, so 1100px
   is where the content column stops holding a list beside a card (the same line .record-grid collapses on). `--side-w`
   is the side card's width. Owned here; a page draws it with its own children and never re-declares the grid. */
.side-layout { display: grid; grid-template-columns: minmax(0, 1fr) var(--side-w, 260px); gap: var(--space-4); align-items: start; }

.side-layout > * { min-width: 0; }

@media (max-width: 1100px) {
  .side-layout { grid-template-columns: minmax(0, 1fr); }
}

/* ONE RECORD, READ-ONLY (ADR-0218): a saved record shown in full, as cards of key-value rows, the key in the
   secondary colour in a fixed column so a label never reads as a value. Main column and side column; one column
   below 1100px and on the phone. Owned here; the waiver, the Square order and the injury report all draw it. */
.record-grid { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: var(--space-4); align-items: start; }

.record-col { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }

.record-card { background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 10px 14px; display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.record-card-title { margin: 0 0 4px; font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .05em; color: var(--color-text-secondary); font-weight: var(--font-semibold); }

.kv-row { display: grid; grid-template-columns: var(--kv-k-w, 110px) minmax(0, 1fr) auto; gap: var(--space-3); align-items: center; padding: 5px 0; border-top: 1px solid var(--color-border-subtle); font-size: var(--text-md); }

.record-card-title + .kv-row, .record-card > .kv-row:first-child, .card-body > .kv-row:first-child { border-top: 0; }

.kv-k { color: var(--color-text-secondary); }

.kv-v { min-width: 0; overflow-wrap: anywhere; }

a.kv-v { color: var(--color-accent); text-decoration: none; }

a.kv-v:hover { text-decoration: underline; }

.kv-end { justify-self: end; }

@media (max-width: 1100px) {
  .record-grid { grid-template-columns: minmax(0, 1fr); }
}

html[data-mode="mobile"] .kv-row { grid-template-columns: 92px minmax(0, 1fr); }

/* A row whose key is a name of any length and whose value is a short figure (an income stream and its amount and share, a
   ratio and its aim): on the phone the key takes the row's free width and the value and the note after it keep their own
   widths on the same line, instead of the 92px key column and a note on a line of its own. The desk is unchanged. */
html[data-mode="mobile"] .kv-row.is-wide-key { grid-template-columns: minmax(0, 1fr) auto auto; }

html[data-mode="mobile"] .kv-end { grid-column: 1 / -1; justify-self: start; }

/* The required marker. aria-hidden in the markup — the asterisk is a visual
   convention, and a screen reader gets `required` on the control itself. */
.form-req { color: var(--color-danger); font-weight: var(--font-bold); }

/* A sub-tab holding a problem says so, so the summary is never the only place
   the error exists. */
.subnav-item .errdot { width: 6px; height: 6px; border-radius: 50%; background: var(--color-danger); display: inline-block; flex: none; }

/* Band B: card chrome under the header, not a strip floating in the body.
   38px, matching the A3 filter row's rhythm rather than the 57px shell band. */
.account-subnav { display: flex; align-items: stretch; height: 38px; padding: 0 var(--space-4); background: var(--color-bg-subtle); border-bottom: 1px solid var(--color-border); }

.subnav-item { display: inline-flex; align-items: center; gap: 6px; padding: 0 var(--space-3); font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--color-text-secondary); background: none; border: 0; border-bottom: 2px solid transparent; cursor: pointer; transition: color .15s ease, border-color .15s ease; }

.subnav-item:hover { color: var(--color-text); }

.subnav-item.active { color: var(--color-accent); border-bottom-color: var(--color-accent); font-weight: var(--font-semibold); }

/* Per-tab count, so you can see what is on a tab before opening it. The
   numbers already existed — updateMobileAccountBadges computed all five and
   painted them on the phone only. */
.subnav-n { min-width: 16px; height: 15px; padding: 0 4px; border-radius: var(--radius-sm); background: var(--color-bg-muted); color: var(--color-text-secondary); font-size: var(--text-xs); font-weight: var(--font-semibold); font-variant-numeric: tabular-nums; display: inline-flex; align-items: center; justify-content: center; }

.subnav-item.active .subnav-n { background: var(--color-accent-tint); color: var(--color-accent); }

/* A sub-tab that does not apply to this account: present, quiet, and
   explaining itself on hover rather than disappearing. */
.subnav-item.is-unavailable { color: var(--color-text-muted); opacity: .55; cursor: not-allowed; }

.subnav-item.is-unavailable:hover { color: var(--color-text-muted); }

/* A collapsible SECTION (ADR-0123). One owner — foundation/section-fold.js —
   and one state: .collapsed on the body, never an inline style.display, which
   no stylesheet can see or override. One chevron, .collapse-icon, rotated when
   the fold is open; a fold never swaps one glyph for another.
   `.collapsible` on the legend means "this legend folds its section". It used
   to be `[data-action]` — any action at all — which was true of the three
   archived legends by accident and would have been wrong for the first legend
   whose action was something else. */
.fold-body.collapsed { display: none; }

.section-legend.collapsible { cursor: pointer; }

.section-legend.collapsible:hover > .t { color: var(--color-text); }

.collapse-icon { transition: transform 0.2s ease; cursor: pointer; color: var(--color-text-muted); font-size: var(--text-xs); }

.collapse-icon.rotated { transform: rotate(180deg); }

/* Row state chip. A dot plus one word, sitting beside the row's badge.
   State used to be crammed into the pass badge's COUNT segment — the same
   slot that otherwise reads "62d" or "5" — so a badge said "Exp" where a
   number belonged, and there was no way to show state and count together. */
/* Status chip: a status dot and a word on a tint of the dot's tone. The
   title bar's Local chip and System button, and the System info window's
   overall status (window and phone More sheet), all wear it; `data-tone`
   (ok | warn | error | neutral) picks the tone. Not a `.state-chip`: that
   labels a record's lifecycle state, this reports the system's. No pill, and
   the text stays --color-text: amber or green text on its own tint fails
   contrast, so the tone rides the dot and the tint. */
.status-chip { --chip-tone: var(--color-text-muted); display: inline-flex; align-items: center; gap: 6px; padding: 3px 7px; border: 0; border-radius: var(--radius-sm); background: color-mix(in srgb, var(--chip-tone) 18%, transparent); font-family: inherit; font-size: var(--text-xs); font-weight: var(--font-semibold); line-height: 1.5; color: var(--color-text); white-space: nowrap; }

.status-chip[data-tone="ok"] { --chip-tone: var(--color-success); }

.status-chip[data-tone="warn"] { --chip-tone: var(--color-warning); }

.status-chip[data-tone="error"] { --chip-tone: var(--color-danger); }

button.status-chip { cursor: pointer; }

button.status-chip:hover { background: color-mix(in srgb, var(--chip-tone) 30%, transparent); }

.state-chip { display: inline-flex; align-items: center; gap: 5px; height: 20px; padding: 0 8px; border-radius: var(--radius-sm); font-size: var(--text-sm); font-weight: var(--font-semibold); white-space: nowrap; flex: none; }

.state-chip .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }

.state-chip.on { color: var(--color-success); background: color-mix(in srgb, var(--color-success) 12%, transparent); }

.state-chip.hold { color: var(--color-paused);  background: color-mix(in srgb, var(--color-paused) 14%, transparent); }

.state-chip.off { color: var(--color-text-muted); background: color-mix(in srgb, var(--color-text-muted) 14%, transparent); }

.state-chip.gone { color: var(--color-expired); background: color-mix(in srgb, var(--color-expired) 12%, transparent); }

.state-chip.new { color: var(--color-accent); background: var(--color-accent-tint); }

/* The fold's summary line. It was a `.badge` pill holding a bare count; it
   now holds a sentence, and a pill around a sentence is a lozenge. */
.fold-summary { font-size: var(--text-sm); font-weight: var(--font-normal); color: var(--color-text-muted); text-transform: none; letter-spacing: 0; }

.subnav-n[hidden] { display: none; }

.row-main { flex: 1; display: flex; align-items: center; gap: var(--space-2); }

.row-name { font-weight: var(--font-normal); }

.row-time { color: var(--color-text-muted); font-size: var(--text-sm); }

/* THE ACTION SLOT — one class for every row family in the app (ADR-0130).
   Nine names declared this, six of them with the same three properties.
   `is-quiet` is the only variant: dimmed at rest for the Settings lists, the
   notes list and the balance ledger, full on row hover AND on :focus-within —
   the focus half being the correctness half, since two of the old copies were
   `opacity: 0` and put keyboard focus on a button nobody could see. Quiet is
   .72, never 0, for the same reason. A row whose action is the reason the row
   is on screen (a camp check-out button) is never quiet. */
.row-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }

.row-actions.is-quiet { opacity: .72; transition: opacity .15s ease; }

.row-actions.is-quiet:focus-within,
*:hover > .row-actions.is-quiet { opacity: 1; }

/* ================================================================
   PAGE CARD COMPONENTS (cc-*) — CANONICAL
   Promoted 2026-08-18 from per-page copies in app-shell.html,
   class-checkin.html and universal-search-results.html; the conflict
   log and each resolution is in design-system/audit.html.
   RULE: component appearance is defined HERE and only here. A page's
   own <style> may override PLACEMENT (position, margins, grid areas,
   page-specific insets like the 26px bleed gutter) — never appearance.
   Insets: card-level bands (header, field bar, notices) 16px;
   pane internals (pane head, rows) 14px.
   --class-color fallback is NEUTRAL (--color-border-strong): absence
   of color is the signal that nothing is scoped.
   ================================================================ */
.class-checkin-header {display:flex;align-items:center;gap:var(--space-3);padding:12px 16px;background:color-mix(in srgb,var(--class-color,var(--color-border-strong)) 9%,var(--color-bg-elevated));border-bottom:1px solid color-mix(in srgb,var(--class-color,var(--color-border-strong)) 22%,var(--color-border))}

.class-checkin-header.no-tint {background:var(--color-bg-elevated);border-bottom:1px solid var(--color-border)}

.class-header-name-row {display:inline-flex;align-items:baseline;gap:var(--space-3)}

.class-header-name {font-size:var(--text-lg);font-weight:var(--font-semibold);letter-spacing:-.015em;color:var(--color-text);margin:0;white-space:nowrap}

/* A page title's icon sits one spacing step from its words, on every page. The
   gap is this margin, never a typed space in the markup (a space is a font's
   width, and it vanished wherever the title was written without one). The
   table header's title (.hdrD h2) is the same title. */
.class-header-name > .bi,.hdrD h2 > .bi {margin-right:var(--space-2)}

.class-header-meta {font-size:var(--text-md);color:var(--color-text-secondary);white-space:nowrap}

.class-header-meta .sep {color:var(--color-text-muted);margin:0 5px}

.class-header-actions {display:flex;align-items:center;gap:var(--space-3);margin-left:auto;flex:none}

.class-header-icon-btn {width:26px;height:26px;flex:none;display:grid;place-items:center;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text-muted);cursor:pointer;font-size: 0.9286rem}

.class-header-icon-btn:hover {background:var(--color-bg-subtle);color:var(--color-text)}

/* Camp's header overflow button carries BOTH .member-overflow-trigger (for
   the generic dropdown JS) and .class-header-icon-btn (for header sizing) —
   .member-overflow-trigger:not(.btn) is 28px at equal (0,2,0) specificity to
   this plain .class-header-icon-btn rule, and it sits earlier in this same
   file, so specificity ties go to source order and 28px was winning. That
   2px pushed the whole camp header to 53px against every other header's
   locked 51px. This combined selector matches the same (0,2,0) specificity
   but comes later, so it wins the tie correctly. */
.member-overflow-trigger.class-header-icon-btn {width:26px;height:26px}

/* THE LIST TOOLBAR (ADR-0124). One band for "a row of controls that acts on
   what is below it", on every page that has one. It is flush with the surface
   it tops, and its bottom rule is the only thing that separates it from that
   surface — the background is the card's own, so there is nothing else to see.

   Two slots, `.toolbar-left` (grows) and `.toolbar-right` (pushed to the end),
   both optional: a band holding one field needs neither. `.stacked` +
   `.toolbar-row` is the ONE extension, for a page whose controls do not fit on
   one line (History) — one band, one rule, two rows, rather than two bands.

   Scoped check-in field. The label names the action, not the mechanism. */
.attendance-toolbar {display:flex;align-items:center;gap:var(--space-2);padding:var(--page-band-pad-y) var(--page-body-pad);min-height:calc(var(--toolbar-control-h) + 2 * var(--page-band-pad-y) + 1px);flex-wrap:wrap;row-gap:var(--space-2);background:var(--color-bg-elevated);border-bottom:1px solid var(--color-border);position:relative}

.attendance-toolbar .toolbar-left {display:flex;align-items:center;gap:6px;flex:1;min-width:200px}

.attendance-toolbar .toolbar-right {display:flex;align-items:center;gap:6px;flex-shrink:0;margin-left:auto}

.attendance-toolbar.stacked {flex-direction:column;align-items:stretch;gap:var(--space-2)}

/* .modal-title-row: createModal's titleChips, the title and its state chips on one wrapping line. */
.attendance-toolbar>.toolbar-row,.modal-title-row {display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;min-width:0}

/* ONE HEADER CONTROL HEIGHT (--header-control-h): whatever control a page
   header holds stands as tall as its buttons. Not on the phone, whose header
   keeps its own touch sizes (mobile.css). */
html:not([data-mode="mobile"]) .class-checkin-header :is(.btn,.btn-group-toggle,.chip,.stat-item) {height:var(--header-control-h);box-sizing:border-box}

html:not([data-mode="mobile"]) .class-checkin-header .btn-group-toggle .btn-toggle {display:inline-flex;align-items:center;padding-top:0;padding-bottom:0}

html:not([data-mode="mobile"]) .class-checkin-header .stat-item {align-items:center;padding-top:0;padding-bottom:0}

/* ONE CONTROL HEIGHT (ADR-0205). The search field and every control beside it
   in a band are `--toolbar-control-h` tall: buttons of every size (a `.btn-sm`
   here is a band control, not the 22px that is pinned to `.pass-badge`
   elsewhere), icon buttons (square), the segmented toggles, selects and inputs,
   a chip that is a plain control (List / Calendar) and a
   chip of any kind that shares a band with a search field. A FILTER count chip
   that has no search field beside it, and every chip outside a band (History's
   Essential / Everything row, the camp's day and age chips), keeps its own 28px. Declared here, once:
   a page that needs a control another height is not a band control. */
.attendance-search {min-height:var(--toolbar-control-h)}

.attendance-toolbar .attendance-search {height:var(--toolbar-control-h);box-sizing:border-box}

.attendance-toolbar :is(.btn,.btn-icon,.btn-group-toggle,.form-input) {height:var(--toolbar-control-h);box-sizing:border-box}

.attendance-toolbar .btn-icon {width:var(--toolbar-control-h)}

.attendance-toolbar .btn-group-toggle .btn-toggle {display:inline-flex;align-items:center;justify-content:center}

.attendance-toolbar .chip:not(:has(.chip-count)),.attendance-toolbar:has(.attendance-search) .chip {height:var(--toolbar-control-h)}

/* A band that tops the CARD supplies the card's own 16px inset (above). One
   inside `.page-body` sits in a column that already carries that inset, so it
   drops its own and spans the column — which is what puts its controls on the
   same left edge as the list underneath. */
/* A page's band bleeds to the card's edges (a negative margin of the body's
   inset, the inset restored as its padding), so its separator is as wide as the
   card's, like the check-in pages'. The gap under a band is the CONTAINER's (a flex
   column with `gap: var(--space-3)`), never the band's margin: one owner per gap. A band that is the page's first thing also
   closes the body's top inset, so it sits under the page head like theirs. The
   bands that sit INSIDE a card carry `.band-in-card` (`searchBandHtml({inCard})`) and are not page bands. The
   phone composes its own chrome (ADR-0083): there the band keeps the column's width. */
html:not([data-mode="mobile"]) .page-body .attendance-toolbar:not(.band-in-card) {margin:0 calc(-1 * var(--page-body-pad))}

html:not([data-mode="mobile"]) .page-body .attendance-toolbar:not(.band-in-card):first-child {margin-top:calc(-1 * var(--page-body-pad))}

/* A page's search band (its first band, wherever it sits) stays under the page head while the list scrolls, on
   every page. History holds its band inside its own pinned block, `.hx-pin`, which is already sticky. */
html:not([data-mode="mobile"]) .page-body .attendance-toolbar:not(.band-in-card):not(.attendance-toolbar ~ .attendance-toolbar):not(.hx-pin > *) {position:sticky;top:var(--page-pin-h);z-index:calc(var(--z-chrome) - 2)}

html[data-mode="mobile"] .page-body .attendance-toolbar {padding-left:0;padding-right:0;flex-direction:row;align-items:center}

html[data-mode="mobile"] .page-body .attendance-toolbar>.attendance-search {flex:1 1 100%;width:100%}

html[data-mode="mobile"] .page-body .attendance-toolbar .toolbar-right {margin-left:0}

.attendance-search {flex:1;display:flex;align-items:stretch;background:var(--color-bg-elevated);border:1px solid var(--color-border-strong);border-radius:var(--radius-md);overflow:hidden;transition:border-color .15s,box-shadow .15s}

.attendance-search.is-focus,.attendance-search:focus-within {border-color:var(--color-accent);box-shadow:0 0 0 3px var(--color-accent-tint)}

.attendance-search-scope {display:inline-flex;align-items:center;gap:6px;padding:0 11px;font-size:var(--text-sm);font-weight:var(--font-semibold);color:var(--color-text);background:color-mix(in srgb,var(--class-color,var(--color-border-strong)) 14%,var(--color-bg-elevated));border-right:1px solid color-mix(in srgb,var(--class-color,var(--color-border-strong)) 22%,var(--color-border));white-space:nowrap}

.attendance-search>i.bi {align-self:center;margin:0 0 0 11px;color:var(--color-text-muted);font-size: 1rem}

.attendance-search input {flex:1;min-width:0;border:0;background:transparent;font-family:inherit;font-size:var(--text-md);color:var(--color-text);padding:8px 11px;outline:0}

.attendance-search input::placeholder {color:var(--color-text-muted)}

/* A phone keeps the 16px floor: under it, iOS zooms the page when the field takes focus. */
html[data-mode="mobile"] .attendance-search input {font-size:1.1429rem}

/* The field's clear button (ADR-0128). It lives HERE, beside the field it sits
   in, and nowhere else — the sole base declaration in the app.

   It is QUIET: a 28px transparent icon button that borrows the field's own
   ground on hover. The design's `.cc-clear` (a solid accent-blue action button)
   lost that argument twice — once when the V3 crosswalk matched it to this
   class on the word "clear", and again when two scoped copies
   (`.search-wrapper .search-clear`, `.history-search-box .search-clear`) drew
   that solid blue block ON TOP of a live field. A clear button is furniture
   inside a field, not the field's primary action.

   `.visible` is the contract, and `display` is what carries it: the button is
   absent from the layout while the field is empty rather than transparent and
   still occupying its box. `design-system/search-field.js` is the only writer.

   The comment that used to sit here named `style.css` as the owner. That file
   stopped existing when ADR-0095 split it into the ten numbered files, and the
   declaration ended up in `90-waivers-misc-v3-ports.css` with four scoped
   copies fighting it from three other files. */
.search-clear {position:relative;display:none;align-items:center;justify-content:center;width:28px;flex:none;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text-muted);font-family:inherit;font-size:var(--text-sm);font-weight:var(--font-medium);cursor:pointer}

.search-clear.visible {display:inline-flex}

.search-clear:hover {background:var(--color-bg-subtle);color:var(--color-text)}

/* On a phone the clear (drawn 28px) and the scroll hint (32px) take a finger: the invisible 44px band a phone .btn carries
   (mobile.css), centred on the drawn control. */
html[data-mode="mobile"] .search-clear.visible::after,html[data-mode="mobile"] .scroll-hint::after {content:'';position:absolute;left:50%;top:50%;width:var(--ph-hit);height:var(--ph-hit);transform:translate(-50%,-50%)}

/* Panes + rows (14px internal inset). Flush by default — a single list has no
   2nd-level card AND runs full-width, edge to edge, exactly like Universal
   Search Results / Recent Accounts (H1: "rows sit flush in the main card, no
   inner card"). The 14px gutter and the bordered/bg card only appear when a
   pane sits beside another one in a dual list (H2: "two adjacent lists need a
   boundary a single list doesn't"). */
.class-checkin-body {padding:0;flex:1;min-height:0}

.class-checkin-panel {background:transparent;border:0;border-radius:0;overflow:hidden}

/* NOT applied. In base.css this is `.class-checkin-body .cc-pane`, and there
   `.class-checkin-body` is the DUAL-LIST body specifically — so the pane card
   fires only when there are two lists side by side. The crosswalk renamed
   `.cc-body` onto that same name, merging the two, and the card started firing
   for EVERY pane: a single list got a bordered, 8px-radius card nested inside
   the content card, its border landing on top of the card's own outline.
   The correctly scoped rule is `#camp-checkin-body .class-checkin-panel`
   in 30-modals-checkin.css.
   Report to the designer. */
.checkin-section-header {display:flex;align-items:center;gap:var(--space-2);padding:9px 14px;background:var(--color-bg-subtle);border-bottom:1px solid var(--color-border)}

.checkin-section-title {font-size: 0.7857rem;font-weight:var(--font-semibold);letter-spacing:.08em;text-transform:uppercase;color:var(--color-text-muted)}

.checkin-section-count {font-size:var(--text-sm);font-weight:var(--font-semibold);color:var(--color-text-secondary);font-variant-numeric:tabular-nums}

.checkin-section-hint {margin-left:auto;font-size:var(--text-sm);color:var(--color-text-muted)}

.checkin-section-action {margin-left:auto;display:flex;align-items:center;gap:6px}

.attendance-list {display:flex;flex-direction:column}

/* The RECORD row (ADR-0130): name | badges | time | actions, four columns and
   four children, always, so a list of rows lines up. `core/attendance-row.js`
   is the one builder that knows that. The fourth track was `26px` — sized for
   exactly one 28px `…` trigger, which overflowed it by 2px — and is now the
   `.row-actions` slot every row family in the app ends with. */
.attendance-row {display:grid;grid-template-columns:minmax(90px,1fr) auto auto auto;align-items:center;gap:var(--space-2);min-height:40px;padding:5px 14px;border-bottom:1px solid var(--color-border-subtle);transition:background .12s}

.attendance-row:hover {background:var(--hover-row)}

.attendance-name {font-size:var(--text-md);color:var(--color-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

.attendance-name .age {color:var(--color-text-muted);font-size:.92em}

.attendance-badges {display:flex;align-items:center;gap:6px;justify-content:flex-end;flex-shrink:0}

.attendance-row.more .attendance-name {color:var(--color-text-muted);font-size:var(--text-sm)}

.attendance-actions {display:flex;gap:6px}

/* Empty pane state — the ONE empty state (ADR-0098, docs/patterns/empty-state.md).
   Base = a centered column; `.fill` / `.panel` are the layout modifiers and
   `.phone-*` (mobile.css) the phone size steps. Icon + action are optional
   children. No surface declares its own empty-state class. */
.empty-state {padding:26px 14px;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center}

.empty-state-title {font-size:var(--text-md);color:var(--color-text-secondary)}

.empty-state-sub {font-size:var(--text-sm);color:var(--color-text-muted);margin-top:3px}

.empty-state-icon {display:block;margin-bottom:var(--space-2);font-size:2rem;opacity:.6}

.empty-state-action {margin-top:var(--space-3)}

/* Fills a parent that already has a definite height (a modal column). */
.empty-state.fill {height:100%;gap:var(--space-3);padding:var(--space-4)}

/* Stands alone AS the pane — reserves its own height so the page doesn't jump.
   It is the whole view, so its icon carries more of the weight. */
.empty-state.panel {min-height:300px;gap:var(--space-3);padding:var(--space-8)}

.empty-state.panel .empty-state-icon {font-size:3rem;margin-bottom:0;color:var(--color-text-muted)}

/* `.error` — the pane is saying "this failed", not "there is nothing". A tint on
   the icon and nothing else: the copy and the warning glyph carry the meaning,
   so the pane keeps the same shape as its sibling states. Declared after
   `.panel` so a panel-sized error still gets the danger tint. */
.empty-state.error .empty-state-icon {color:var(--color-danger)}

/* Loading. `badge-pending-spin` (00-shell) is the app's one spin keyframe. */
.empty-state-spinner {display:block;width:26px;height:26px;margin-bottom:var(--space-2);border:3px solid var(--color-border);border-top-color:var(--color-accent);border-radius:50%;animation:badge-pending-spin .8s linear infinite}

/* Notices: cancelled class, past date, locked, viewer (band inset 16px;
   pages with a bleed gutter override the LEFT inset only) */
.class-checkin-banner {display:flex;align-items:center;gap:8px;padding:8px 16px;font-size:var(--text-sm);border-bottom:1px solid var(--color-border)}

.class-checkin-banner .bi {flex-shrink:0}

.class-checkin-banner.warn {background:color-mix(in srgb,var(--color-warning) 12%,var(--color-bg-elevated));border-bottom-color:color-mix(in srgb,var(--color-warning) 35%,var(--color-border));color:var(--color-text)}

/* `>` so the tint reaches only the notice's own leading icon, never the icon
   inside a button the notice carries (Complete camp, Send the waiver). */
.class-checkin-banner.warn>.bi {color:var(--color-warning)}

.class-checkin-banner .spacer {flex:1}

/* Inside a page body the notice is a box, not a strip: the full border replaces the strip's bottom rule. */
.class-checkin-banner.is-inset {border:1px solid var(--color-border);border-radius:var(--radius-md);padding:8px 12px}

.class-checkin-banner.warn.is-inset {border-color:color-mix(in srgb,var(--color-warning) 35%,var(--color-border))}

/* Floating surface (L3): ONE look for everything that floats over the page on
   desktop and tablet (ADR-0287). The menus, the check-in popover, the camp
   quick-info, the notes preview, the Members filter panel, the tooltip, the
   date picker and the multi-select list carry `.floating` beside their own
   name. The edge is --shadow-lg's ring and the surface draws no border
   (ADR-0258 §2): a border under the ring drew the edge twice, 2px and blurred.
   The corner is a modal's. A surface's own rule says where it sits and how big
   it is, never this look; tests/unit/floating-surface-drift.test.js holds it. */
.floating {background:var(--color-bg-elevated);border:0;border-radius:var(--radius-lg);box-shadow:var(--shadow-lg)}

/* Menus (L3) — ONE look for every desktop and tablet menu.
   The row ⋮ (.member-overflow-menu), the page-header ⋮ and the title bar's
   account menu (.class-header-overflow-menu), and the range menu
   (.range-menu-list) are one panel with one item; the panel's look is `.floating`. design-system/overflow-menu.js
   places the first two; the class names are its behaviour hooks, not sizes.
   The panel is a floating surface, so its edge is --shadow-lg's ring and it
   draws no border of its own (ADR-0258 §2): a border under that ring drew the
   edge twice, 2px and blurred. Its corner is a modal's (--radius-lg). An item is
   body text (--text-md) in a 36px row, 44px under a finger (iPad, Fold), the
   tap size a .choice row has. Phone menus are sheets of .choice rows and never
   reach these rules (ADR-0248). ADR-0281. */
.member-overflow-menu,.class-header-overflow-menu,.range-menu-list {z-index:var(--z-anchored);box-sizing:border-box;min-width:180px;max-width:320px;padding:var(--space-1)}

.class-header-overflow-menu {position:absolute;display:none;flex-direction:column}

.overflow-menu-item {display:flex;align-items:center;gap:10px;width:100%;min-height:36px;box-sizing:border-box;padding:0 var(--space-3);border:0;border-radius:var(--radius-md);background:transparent;font-family:inherit;font-size:var(--text-md);font-weight:var(--font-normal);line-height:1.25;color:var(--color-text);text-align:left;white-space:nowrap;cursor:pointer}

.overflow-menu-item:hover,.overflow-menu-item:focus-visible {background:var(--hover-row);outline:0}

.overflow-menu-item .bi,.overflow-menu-row>.bi {flex:none;width:16px;text-align:center;font-size:1rem;color:var(--color-text-secondary)}

@media (pointer:coarse) {
  .overflow-menu-item {min-height:44px}
}

/* Tone. Two tones, one per reversibility: danger is the irreversible (red),
   warning is loud but undoable (amber — Cancel today's class, which Restore
   puts back). Restore itself takes no modifier at all (owner decision, plan
   §7). Written once, after the base, at its specificity, so no menu can
   render a danger item in plain text again (census §A1). */
.overflow-menu-item-danger,.overflow-menu-item-danger .bi {color:var(--color-danger)}

.overflow-menu-item-danger:hover,.overflow-menu-item-danger:focus-visible {background:color-mix(in srgb, var(--color-danger) 10%, transparent);color:var(--color-danger)}

.overflow-menu-item-warning,.overflow-menu-item-warning .bi {color:var(--color-warning)}

.overflow-menu-item-warning:hover,.overflow-menu-item-warning:focus-visible {background:color-mix(in srgb, var(--color-warning) 10%, transparent);color:var(--color-warning)}

/* A row that holds its own controls rather than being one command (the account
   menu's Text size, ADR-0282): an item's line and inset, the controls at its end.
   Clicking inside it leaves the menu open (only an .overflow-menu-item closes). */
.overflow-menu-row {display:flex;align-items:center;gap:10px;min-height:36px;box-sizing:border-box;padding:2px var(--space-1) 2px var(--space-3);font-size:var(--text-md);color:var(--color-text);white-space:nowrap}

.overflow-menu-row .menu-row-end {margin-left:auto;display:inline-flex;align-items:center;gap:var(--space-1)}

.overflow-menu-row .menu-row-end .menu-item-value {margin:0;min-width:3.2em;text-align:center;font-variant-numeric:tabular-nums}

.overflow-menu-row .btn-icon:disabled {opacity:.4;cursor:default;background:transparent;color:var(--color-text-secondary);border-color:var(--color-border)}

@media (pointer:coarse) {
  .overflow-menu-row {min-height:44px}

  .overflow-menu-row .btn-icon {width:36px;height:36px}
}

/* The universal band's scope label is `.attendance-search-scope`, the field's
   own (ADR-0128). `.search-bar-scope` was the same chip under a second name,
   unfilled where the shared one is tinted — so the band's "Look up" and the
   check-in pages' "Check in" read as two different controls. */

/* ================================================================
   MEMBERS HEADER (hdrD) — CANONICAL
   The Locked Universal Search Results header: title, count, filter
   readout chips, filter panel, overflow. Was duplicated verbatim in
   app-shell.html and universal-search-results.html.
   ================================================================ */
.hdrD {display:flex;align-items:center;gap:var(--space-3);padding:7px var(--space-4);background:var(--color-bg-elevated);border-bottom:1px solid var(--color-border);min-height:42px;box-sizing:border-box;position:relative}

.hdrD h2 {margin:0;font-size:var(--text-lg);font-weight:var(--font-semibold);letter-spacing:-.015em;color:var(--color-text);white-space:nowrap}

.hdrD .acts {display:flex;align-items:center;gap:var(--space-2);margin-left:auto}

.hdrD .btn-group .btn {display:inline-flex;align-items:center;gap:5px;white-space:nowrap;height:28px;box-sizing:border-box;padding:0 9px;font-size:var(--text-sm)}

.hdrD .btn-group .btn .bi {font-size: 0.8571rem;color:var(--color-text-muted)}

.hdrD .btn-group [data-clear-hdr] {padding-left:8px;padding-right:8px}

.hdrD .btn-group [data-clear-hdr] .bi {font-size: 0.7143rem}

.hdrD .btn-group [data-clear-hdr][hidden] {display:none}

.hdrD .btn-group:not(.has-clear) [data-filter-btn] {border-radius:var(--radius-sm)}

.hdrD .btn-group .btn.on,.hdrD .btn-group .btn.on + [data-clear-hdr] {border-color:var(--color-accent);color:var(--color-text);font-weight:var(--font-semibold);z-index:1}

.hdrD .btn-group .btn.on .bi,.hdrD .btn-group .btn.on + [data-clear-hdr] .bi {color:var(--color-accent)}

.hdrD .btn-group .btn[aria-expanded="true"] {background:var(--color-bg-subtle);color:var(--color-text)}

.hdrD .btn-group .btn .n {height:20px;display:inline-flex;align-items:center;font-size:var(--text-sm);line-height:1;font-weight:var(--font-semibold);font-variant-numeric:tabular-nums;padding:0 7px;border-radius:var(--radius-sm);background:var(--color-accent-tint);color:var(--color-accent)}

.hdr-count {font-size:var(--text-md);color:var(--color-text-secondary);font-variant-numeric:tabular-nums;white-space:nowrap}

.hdr-count strong {font-weight:var(--font-semibold);color:var(--color-text)}

.readout {display:flex;align-items:center;gap:5px;align-self:center;min-width:0;flex-wrap:nowrap;overflow:hidden;padding-right:var(--space-3);margin-right:var(--space-1);border-right:1px solid var(--color-border)}

.readout[hidden] {display:none}

.filter-chip {min-width:0;display:inline-flex;align-items:center;gap:6px;height:28px;box-sizing:border-box;padding:0 4px 0 10px;font-size:var(--text-base);background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-sm);color:var(--color-text);white-space:nowrap}

.filter-chip .dot {width:8px;height:8px;border-radius:2px;background:var(--tc,var(--color-text-muted));flex:none}

.filter-chip button {width:18px;height:18px;display:grid;place-items:center;border:0;border-radius:3px;background:transparent;color:var(--color-text-muted);cursor:pointer;font-size: 0.7143rem}

.filter-chip button:hover {background:var(--color-bg-muted);color:var(--color-text)}

/* Hangs from the header's bottom edge, its own top border on the header's
   1px rule (100% is the padding box, which ends where that rule starts), at
   whatever height the header is (--page-head-h on desktop). */
.filter-body {position:absolute;top:100%;right:var(--space-4);z-index:var(--z-anchored);display:grid;grid-template-columns:repeat(2,minmax(168px,1fr));gap:0 var(--space-5);padding:var(--space-3) var(--space-4);}

.filter-body[hidden] {display:none}

.filter-group {display:flex;flex-direction:column;gap:2px;min-width:0}

.filter-group h4 {margin:0 0 4px;font-size: 0.7857rem;font-weight:var(--font-semibold);text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-muted)}

.filter-option {display:flex;align-items:center;gap:8px;height:28px;padding:0 6px;border:0;border-radius:var(--radius-sm);background:transparent;font-family:inherit;font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-text-secondary);cursor:pointer;text-align:left;white-space:nowrap}

.filter-option:hover:not([disabled]) {background:var(--color-bg-subtle);color:var(--color-text)}

.filter-option .box {width:14px;height:14px;flex:none;display:grid;place-items:center;border:1px solid var(--color-border-strong);border-radius:3px;background:var(--color-bg-elevated);font-size: 0.6429rem;color:transparent}

.filter-option[aria-pressed="true"] {color:var(--color-text);font-weight:var(--font-semibold)}

.filter-option[aria-pressed="true"] .box {background:var(--tc,var(--color-accent));border-color:var(--tc,var(--color-accent));color:#fff}

.filter-option .dot {width:8px;height:8px;border-radius:2px;background:var(--tc,var(--color-text-muted));flex:none}

.filter-option .nm {flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}

.filter-option .n {font-size: 0.7857rem;font-variant-numeric:tabular-nums;color:var(--color-text-muted)}

.filter-option[disabled] {opacity:.45;cursor:default}

.filter-body .foot {grid-column:1/-1;display:flex;align-items:center;gap:var(--space-2);margin-top:var(--space-2);padding-top:var(--space-2);border-top:1px solid var(--color-border)}

.filter-body .foot .spacer {margin-left:auto}

/* ================================================================
   ANCHORED POPOVER — the ONE pointer (ADR-0120)
   ================================================================
   The census (§A2) counted FOUR pointer treatments for one job: the
   check-in popover's `.anchored.arrow-*` pair, the camp quick-info's
   near-copy of it (same two triangles, different offsets and a
   translateY instead of a margin), the notes preview's top-edge one
   with its own custom property name, and the health callout's rotated
   square. The first three are this family now; the fourth stays its
   own, because a callout that inherits its tier colour needs a shape
   that can inherit a border (ADR-0114).

   `.anchored` is the beside-the-trigger mode, not a position: the card
   is placed in VIEWPORT coordinates by design-system/anchored-popover.js
   (which is why every anchored card is `position: fixed` — a card that
   resolved top/left in DOCUMENT coordinates detached from its anchor by
   exactly scrollY the moment the page scrolled). All this class does is
   let the pointer out of the box, which is also why the card's own
   corners move to its first and last child.

   `arrow-*` names the card's OWN edge the pointer sits on, which is the
   opposite of the side the card is on: a card to the LEFT of its anchor
   carries `arrow-right`. The two offsets are set inline —
   `--arrow-left` on the vertical pair, `--arrow-top` on the horizontal
   one — and clamped away from the corners by the placement owner. */
.anchored {overflow:visible}

.anchored>:first-child {border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}

.anchored>:last-child {border-bottom-left-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}

/* Two triangles: the ::before is the card's border colour, the ::after the
   same shape one pixel inside it in the card's own surface colour. A card
   whose anchored edge is not `--color-bg-elevated` (the notes preview's
   tinted header) overrides `--anchor-arrow-fill` and nothing else. */
.anchored.arrow-top::before,.anchored.arrow-top::after,
.anchored.arrow-right::before,.anchored.arrow-right::after,
.anchored.arrow-bottom::before,.anchored.arrow-bottom::after,
.anchored.arrow-left::before,.anchored.arrow-left::after {content:'';position:absolute;width:0;height:0;border:8px solid transparent;pointer-events:none}

.anchored.arrow-left::before,.anchored.arrow-left::after,
.anchored.arrow-right::before,.anchored.arrow-right::after {top:var(--arrow-top,50%);margin-top:-8px}

.anchored.arrow-top::before,.anchored.arrow-top::after,
.anchored.arrow-bottom::before,.anchored.arrow-bottom::after {left:var(--arrow-left,50%);margin-left:-8px}

.anchored.arrow-left::before {right:100%;border-right-color:var(--anchor-arrow-border,var(--float-edge))}

.anchored.arrow-left::after {right:100%;margin-right:-1px;border-right-color:var(--anchor-arrow-fill,var(--color-bg-elevated))}

.anchored.arrow-right::before {left:100%;border-left-color:var(--anchor-arrow-border,var(--float-edge))}

.anchored.arrow-right::after {left:100%;margin-left:-1px;border-left-color:var(--anchor-arrow-fill,var(--color-bg-elevated))}

.anchored.arrow-top::before {bottom:100%;border-bottom-color:var(--anchor-arrow-border,var(--float-edge))}

.anchored.arrow-top::after {bottom:100%;margin-bottom:-1px;border-bottom-color:var(--anchor-arrow-fill,var(--color-bg-elevated))}

.anchored.arrow-bottom::before {top:100%;border-top-color:var(--anchor-arrow-border,var(--float-edge))}

.anchored.arrow-bottom::after {top:100%;margin-top:-1px;border-top-color:var(--anchor-arrow-fill,var(--color-bg-elevated))}

/* Utilities the camp rows use */
.text-muted {color:var(--color-text-muted)}

.text-sm {font-size:var(--text-sm)}

/* ----------------------------------------------------------------
   MONEY TONE (CSS-foundation plan Phase 7 item 4)
   ONE vocabulary for what a money amount MEANS. It replaces five
   bespoke pairs that all resolved to the same two tokens under
   different names: .charge/.credit (camp + mobile balance rows),
   .balance-type-charge/-credit (the balance card), and
   .balance-total-owes/-credit/-zero (the balance total row).
   The tone says what the amount means; the surface class still
   owns how big it is, because those sizes legitimately differ
   (a total is --text-lg, a row amount inherits) and are not drift.
   Deliberately low specificity (0,1,0) so a surface state that
   overrides the tone entirely — .balance-card-paid's struck-through
   muted amount — keeps winning on its own (0,2,0).
   ---------------------------------------------------------------- */
.tone-owes {color:var(--color-danger)}

.tone-credit {color:var(--color-success)}

.tone-zero {color:var(--color-text-muted)}

/* ================================================================
   APP SHELL COMPOSITION
   The three-column shell, the nav rail and the page wrapper. Shipped:
   the app depends on these, so they live here (one definition, flows
   into v3.css) and not in a page's local style block.
   ================================================================ */
/* No top gutter: the 44px title bar takes that strip (ADR-0158). */
.app-viewport {background:var(--color-bg-subtle);padding:0 var(--win-gutter) var(--win-gutter);box-sizing:border-box;display:flex;min-height:0}

.shell {position:relative;width:100%;max-width:var(--win-max);margin-inline:auto;display:grid;grid-template-columns:48px var(--sched-col) minmax(0,1fr);background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;min-height:660px;font-family:var(--font-sans);--band-h:57px;--ctl-h:39px}

@media (max-width:720px) {
  /* Under 720px the window gives up its gutter, border and radii: a 16px inset
     on a small screen is lost space, and a rounded card inside a rounded device
     reads as a mistake. The frame is a desktop affordance.
     MUST stay BELOW the .shell base rule above. It used to sit ~16 lines higher,
     and since both are plain `.shell` at equal specificity the base rule's
     `border: 1px` won on source order — so the frame never actually dropped at
     any width between 601px and 720px. style.css carried a second copy of this
     same rule on `.app-container`, which lost to the same base rule and was
     therefore equally inert; it is deleted (ADR-0084). */
  :root {--win-gutter:0px}

  .shell {border:0;border-radius:0}
}

.shell.nav-open {grid-template-columns:150px var(--sched-col) minmax(0,1fr)}

/* THE COMPACT SHELL (ADR-0221): narrower than an iPad in landscape (`data-shell`, stamped by viewport-mode.js, the
   one owner of the width), the day's schedule is not a column. It slides over the page from the rail's Schedule
   button and closes when a card in it is picked, on a tap outside it, or on Escape (app.js `wireScheduleDrawer`),
   so every page has the window's width less the rail. The schedule keeps its own markup and width. */
#schedule-toggle {display:none}

html[data-shell="compact"] #schedule-toggle {display:flex}

html[data-shell="compact"] .shell {grid-template-columns:48px minmax(0,1fr)}

html[data-shell="compact"] .shell.nav-open {grid-template-columns:150px minmax(0,1fr)}

html[data-shell="compact"] .shell .schedule-sidebar {position:absolute;top:0;bottom:0;margin-bottom:0;padding-bottom:16px;left:48px;width:var(--sched-col);background:var(--color-bg-elevated);border-right:1px solid var(--color-border);box-shadow:var(--shadow-lg);visibility:hidden;opacity:0;transform:translateX(-12px);transition:opacity .15s ease,transform .15s ease,visibility 0s linear .15s}

html[data-shell="compact"] .shell.nav-open .schedule-sidebar {left:150px}

html[data-shell="compact"] .shell.schedule-open .schedule-sidebar {visibility:visible;opacity:1;transform:none;transition:opacity .15s ease,transform .15s ease}

@media (prefers-reduced-motion: reduce) {
  html[data-shell="compact"] .shell .schedule-sidebar {transition:none}
}

/* With no schedule column, nothing sits left of the page: the main column takes the 16px gutter the column gave it
   (the search band and the card both start there), and the card's outline stops overhanging its left edge, which
   only covered the seam. The drawer above runs the window's full height: its 16px floor moves from a margin, which
   lined it up with the card it no longer stands beside, to padding inside it. */
html[data-shell="compact"] .shell .app-main {padding-left:16px}

html[data-shell="compact"] .page::before {inset:0}

/* The nav rail's own chrome: its column, inset and the hairline that divides it from the next column. It lived in
   v3.css, so an app on the design system alone (TMV-Vault) drew the rail with no right edge (ADR-0291). */
.app-nav {background:var(--color-bg-elevated);border-right:1px solid var(--color-border);padding:10px 8px;display:flex;flex-direction:column;gap:2px}

.app-nav .nav-divider {height:1px;background:var(--color-border);margin:6px 4px}

.shell .app-nav {background:var(--color-bg-elevated)}

.shell .app-nav .nav-item {position:relative}

.shell .app-nav.collapsed {align-items:center;padding:8px 0}

.shell .app-nav.collapsed .nav-list {align-items:center}

.shell .app-nav.collapsed .nav-item {width:34px;height:34px;justify-content:center;gap:0;padding:0}

.shell .app-nav.collapsed .nav-item .nav-label {display:none}

.shell .app-nav.collapsed .nav-item .nav-icon {width:auto}

.shell .app-nav.collapsed .nav-badge {position:absolute;top:0;right:0;margin-left:0;padding:0 4px;font-size: 0.6429rem;line-height:13px}

.shell .app-nav.collapsed .nav-divider {width:24px;margin:6px auto}

.shell .app-main {background:transparent;min-width:0;padding-top:7px}

.shell .app-main-search {background:transparent;border-bottom:none;padding:9px 16px 9px 0;height:var(--band-h);box-sizing:border-box;display:flex;align-items:center}

.page {position:relative;display:flex;flex-direction:column;flex:1;margin:0 16px 16px 0;background:var(--color-bg-elevated);border-radius:var(--radius-md);min-height:0}

/* The card's outline is its TOP layer: one tier above `--z-chrome`, so a pinned
   page header (the page head, the chrome tier) never paints
   over it. At z-index 5 the pinned header covered the outline along its edges.
   Anchored surfaces and modals stay above it. */
.page::before {content:'';position:absolute;inset:0 0 0 -1px;border:1px solid var(--class-color,var(--color-border-strong));border-radius:var(--radius-md);pointer-events:none;z-index:calc(var(--z-chrome) + 1)}

/* The fused tab exists only where the schedule is a column beside the card: in the compact shell (ADR-0221) the
   schedule slides over the page, so there is no seam to fuse across and these, with the selected card's bleed above,
   are scoped out of it. */
html:not([data-shell="compact"]) .page.flush-top::before {border-top-left-radius:0}

html:not([data-shell="compact"]) .page.flush-top .class-checkin-header {border-top-left-radius:0}

/* A shell with no schedule column and no search band (TMV-Vault, ADR-0289): rail + page, read from the markup, no class
   of its own. With no schedule beside it the page takes the compact shell's columns and 16px left gutter at every
   width, and its outline has no seam to overhang. With no band above it, the page's top gap is the same 16px as its
   other three sides (the main column's 7px plus 9px), instead of sitting 7px under the window's top edge. A phone
   keeps its full-bleed page. */
html:not([data-mode="mobile"]) .shell:not(:has(> .schedule-sidebar)) {grid-template-columns:48px minmax(0,1fr)}

html:not([data-mode="mobile"]) .shell.nav-open:not(:has(> .schedule-sidebar)) {grid-template-columns:150px minmax(0,1fr)}

html:not([data-mode="mobile"]) .shell:not(:has(> .schedule-sidebar)) .app-main {padding-left:16px}

.shell:not(:has(> .schedule-sidebar)) .page::before {inset:0}

html:not([data-mode="mobile"]) .shell .app-main > .page:first-child {margin-top:9px}

/* ==========================================================================
   The gate: sign-in screen and boot cover
   ========================================================================== */
/* ==========================================================================
   THE GATE (ADR-0255): the screens before the app, the sign-in page
   (auth.html, standalone/auth-page.js) and the boot cover while auth
   resolves (index.html #boot-splash). They are the TMV Training App's auth
   gate, drawn here on this app's tokens, so every TMV app opens on one
   screen: the word "TMV" in Manrope 800 (never an image), an uppercase label
   under it, and the action stack at the foot. The loading cover is the
   Training App's "Checking your account…" gate: the wordmark and one hint,
   centred, with no spinner. On a phone the frame is the whole screen; from
   600px it is one centred column, as the Training App's tablet gate is.

   The metrics below are the Training App's own (css/tmv/tokens: the
   wordmark is --text-num-xl 34px, the label --text-label 10px, the hint
   --text-hint 11px, the body 13.5px, the button --ctl 50px at --r-pill),
   written in this app's rem (14px, so the ui-font-scale knob still applies);
   the colours are this app's tokens, so the gate follows the staff app's
   light and dark theme.
   ========================================================================== */
@font-face { font-family: "Manrope"; font-style: normal; font-weight: 400; font-display: swap; src: url(../fonts/manrope/manrope-400.woff2) format("woff2"); }

@font-face { font-family: "Manrope"; font-style: normal; font-weight: 700; font-display: swap; src: url(../fonts/manrope/manrope-700.woff2) format("woff2"); }

@font-face { font-family: "Manrope"; font-style: normal; font-weight: 800; font-display: swap; src: url(../fonts/manrope/manrope-800.woff2) format("woff2"); }

.gate {
  --gate-font: "Manrope", system-ui, -apple-system, sans-serif;
  --gate-pad-top: 22px;
  --gate-pad-bottom: 26px;
  --gate-pad-side: 15px;
  --gate-mark-top: 40px;          /* the Training App's --inset-edge + --gate-mark-pad-top */
  --gate-column: clamp(440px, 38vw, 560px);
  --gate-gap: 10px;
  --gate-gap-s: 8px;
  --gate-gap-m: 12px;
  --gate-gap-tight: 6px;
  --gate-ctl: 50px;
  --gate-hd-min: 38px;
  --gate-prose: 34em;

  position: fixed;
  inset: 0;
  z-index: var(--z-gate);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding: calc(var(--gate-pad-top) + env(safe-area-inset-top)) var(--gate-pad-side) calc(var(--gate-pad-bottom) + env(safe-area-inset-bottom));
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--gate-font);
  -webkit-font-smoothing: antialiased;
}

@media (min-width: 600px) {
  .gate { align-items: center; }

  .gate > * { width: min(var(--gate-column), 100%); }
}

/* the content: from the top, or centred when the gate states one fact */
.gate-bd {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: var(--gate-gap);
}

.gate-bd.center { justify-content: center; }

/* the action stack at the foot */
.gate-ft {
  display: flex;
  flex-direction: column;
  gap: var(--gate-gap-s);
}

.gate-ft > .gate-body { margin-bottom: var(--gate-gap-s); }

/* the brand: the word, tracked like the xl numeral it is set in */
.gate-mark { padding-top: var(--gate-mark-top); }

.gate-mark.center { padding-top: 0; text-align: center; }

.gate-wordmark {
  font: 800 2.4286rem/1 var(--gate-font);
  letter-spacing: -.04em;
  color: var(--color-text);
}

.gate-label {
  margin-top: var(--gate-gap-m);
  font: 800 .7143rem/1 var(--gate-font);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.gate-hint {
  margin: var(--gate-gap-m) auto 0;
  max-inline-size: var(--gate-prose);
  font: 700 .7857rem/1.4 var(--gate-font);
  color: var(--color-text-muted);
  text-align: center;
}

.gate-ft > .gate-hint { margin-top: 0; }

/* a signed-in gate's header row: the small wordmark (the page header's, 19px) */
.gate-hd {
  display: flex;
  align-items: center;
  min-height: var(--gate-hd-min);
  padding: 0 2px;
}

.gate-hd > .gate-wordmark { font-size: 1.3571rem; }

/* a gate's title: the heading and the one line under it that says what happens next */
.gate-title h1 {
  font: 800 1.5rem/1.15 var(--gate-font);
  letter-spacing: -.03em;
  color: var(--color-text);
}

.gate-body {
  max-inline-size: var(--gate-prose);
  font: 400 .9643rem/1.6 var(--gate-font);
  color: var(--color-text);
}

.gate-title > .gate-body { margin-top: var(--gate-gap-tight); }

.gate-warn { font: 700 .7857rem/1.4 var(--gate-font); color: var(--color-danger); text-align: center; }

.gate-warn[hidden] { display: none; }

/* the gate's button: a full-width pill in ink; .quiet is the second action */
.gate-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--gate-gap-s);
  width: 100%;
  min-height: var(--gate-ctl);
  border: 0;
  border-radius: var(--radius-full);
  background: var(--color-text);
  color: var(--color-bg);
  font: 800 1rem/1 var(--gate-font);
  letter-spacing: -.01em;
  box-shadow: 0 12px 26px -12px rgba(23, 26, 32, .28);
  cursor: pointer;
  transition: transform .12s ease, opacity .15s ease;
}

.gate-btn.quiet {
  background: var(--color-bg-subtle);
  color: var(--color-text);
  box-shadow: none;
}

.gate-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.gate-btn:active:not(:disabled) { transform: scale(.97); }

.gate-btn:disabled { cursor: default; }

.gate-btn[aria-busy="true"] { cursor: progress; animation: gate-pulse 1.2s ease-in-out infinite; }

@keyframes gate-pulse { 50% { opacity: .6; } }

@media (prefers-reduced-motion: reduce) {
  .gate-btn { transition: none; }

  .gate-btn[aria-busy="true"] { animation: none; }
}

/* The boot cover (index.html): up until auth resolves, so the app shell
   never flashes before a redirect to /auth.html on a fresh device. The shell
   is hidden until body.auth-ready (set in app.js), which takes the cover
   down; on the null-session path the cover stays up through the redirect.
   The theme is right because early-chrome.js runs before first paint. */
body:not(.auth-ready) .app-container,
body:not(.auth-ready) .app-titlebar {
  display: none;
}

body.auth-ready #boot-splash { display: none; }

/* ==========================================================================
   The phone frame and tab bar
   ========================================================================== */
/* ==========================================================================
   MOBILE SHELL — V3 (design-reference/TMV Phone.dc.html)
   ==========================================================================

   The phone chrome. Every rule here is scoped to `html[data-mode="mobile"]`,
   the attribute `design-system/viewport-mode.js` stamps — there is deliberately
   NO media query in this file, so the breakpoint number lives in exactly one
   place (JS) and the two can never disagree.

   Loaded after v3.css. That order matters: the mode scope is what lets these
   rules win over the desktop shell's own `.app-nav { display: flex }`, which
   sits in v3.css at equal specificity to style.css's old
   `@media (max-width: 600px) { .sidebar { display: none } }` and silently beat
   it — the nav rail has been visible on phones since the shell was adopted.
   Scoping is the fix, not a specificity bump: this file OWNS the mobile mode.

   See ADR-0083.
   ========================================================================== */

/* The desk's type tokens on a phone: each desk step lands on a step of the phone's scale (above), so a desk feature (a
   sheet, What's new, a page the phone shows as the desk draws it) reads at phone sizes with no rule of its own. On body, not
   html: the root's font-size reads --text-md, and every rem on the page must keep resolving against 14px. */
html[data-mode="mobile"] body {
  --text-xs:   var(--ph-fs-meta);
  --text-sm:   var(--ph-fs-meta);
  --text-base: var(--ph-fs-body);
  --text-md:   var(--ph-fs-body);
  --text-lg:   var(--ph-fs-title);
  --text-xl:   var(--ph-fs-title);
  --text-2xl:  var(--ph-fs-num);
}

html[data-mode="mobile"] {
  /* The tab bar's height without the safe area (which the notification stack adds itself), published so the stack can
     sit above the bar without hardcoding a number (the old chrome hardcoded 80px in two places and they drifted apart). */
  --mobile-nav-h: calc(var(--ph-tabbar-h) + var(--ph-bar-gap));
  /* The phone's single-line page header (Settings, Camps, Online Orders, Waivers,
     Training): one 48px line. The pinned tab bar starts at this offset, so a
     header and its tab bar read the same number (ADR-0197). */
  --page-head-h: 48px;

  /* THE PHONE'S STEPS (ADR-0248; the approved prototype's phone.css, declared once and read by name). Sizes, insets and
     the chrome chain are the phone's own figures; every colour, radius and shadow is a v3.css token. v3.css's
     MODAL AS A SHEET and the badge / check-in button phone shapes read them too. */
  --ph-inset: 16px;              /* the page's side gutter */
  --ph-gap: 12px;                /* between cards */
  --ph-r-card: var(--radius-lg);
  --ph-r-sheet: var(--radius-xl);
  --ph-r-ctl: var(--radius-md);  /* a field; buttons keep .btn's own radius */
  --ph-ctl: 44px;                /* a control a finger presses */
  --ph-ctl-s: 34px;              /* a chip, a segmented control */
  --ph-btn: 38px;                /* a button's height: the check-in button's, the tallest anything pressable looks */
  --ph-btn-s: 32px;              /* a small button (.btn-sm) */
  --ph-hit: 44px;                /* the finger's target behind a button: invisible, taller than the button is drawn */
  --ph-row: 60px;                /* a list row's floor */
  --ph-head-h: 48px;             /* the compact bar */
  --ph-head-top: 10px;           /* room above the bar's content, so the brand sits with even space above and below (owner, 20 Oct) */
  --ph-tabbar-h: 60px;
  --ph-bar-gap: 10px;            /* the tab bar's float off the bottom edge */

  /* The chrome chain: each clearance is the one below plus its footprint, so nothing sits behind the bar or the
     phone's gesture bar (Training's ADR-0147, the same derivation). */
  --ph-safe-t: env(safe-area-inset-top, 0px);
  --ph-safe-b: env(safe-area-inset-bottom, 0px);
  --ph-bar-clear: calc(var(--ph-tabbar-h) + var(--ph-bar-gap) + var(--ph-safe-b));
  --ph-float-clear: calc(var(--ph-bar-clear) + 12px);
  --ph-toast-clear: calc(var(--ph-float-clear) + var(--ph-ctl) + 10px);
  /* The bar floats over the page (ADR-0248, amended 22 Oct), so a scroller's foot clears the whole chain plus a breath: the last row scrolls
     clear of the bar, and everything above it passes behind the glass. */
  --ph-scroll-foot: calc(var(--ph-bar-clear) + var(--ph-gap) * 2);

  /* THE PHONE'S TYPE SCALE: six sizes, and no text on a phone is drawn at any other (rem: the app's root is 14px, and the
     text-size setting scales it). 13 is the floor for words; only a label is smaller. A field's text is never under 16px
     (max(body, 16px)), or the phone zooms into it. The roles below and the
     desk's --text-* tokens (on body, further down) all read these. */
  --ph-fs-label: .7857rem;   /* 11, uppercase labels, and the caption under a tab or a count */
  --ph-fs-meta:  .9286rem;   /* 13, secondary lines, chips, captions */
  --ph-fs-body:  1.0714rem;  /* 15, rows, reading text, controls */
  --ph-fs-title: 1.2143rem;  /* 17, a sheet's or page bar's title, a section head */
  --ph-fs-num:   1.5714rem;  /* 22, a count, a total */
  --ph-fs-large: 2rem;       /* 28, a screen's large title */

  /* Type roles: a size from the scale with its weight and leading */
  --ph-t-large: 750 var(--ph-fs-large)/1.15 var(--font-sans);
  --ph-t-title: 700 var(--ph-fs-title)/1.25 var(--font-sans);
  --ph-t-row: 600 var(--ph-fs-body)/1.3 var(--font-sans);
  --ph-t-body: 400 var(--ph-fs-body)/1.45 var(--font-sans);
  --ph-t-meta: 400 var(--ph-fs-meta)/1.35 var(--font-sans);
  --ph-t-label: 700 var(--ph-fs-label)/1.2 var(--font-sans);
  --ph-t-num: 750 var(--ph-fs-num)/1 var(--font-sans);

  --ph-glass: color-mix(in srgb, var(--color-bg-elevated) 80%, transparent);
  --ph-scrim: rgba(0, 0, 0, .38);
}

html[data-mode="mobile"][data-theme="dark"] { --ph-scrim: rgba(0, 0, 0, .55); }

/* The motion curve and times live on the root: a page transition (mobile-motion.js) draws outside any one screen. */
:root {
  --ph-ease: cubic-bezier(.32, .72, 0, 1);
  --ph-t-slide: .34s;
  --ph-t-fade: .2s;
}

/* --------------------------------------------------------------------------
   THE WINDOW
   On a phone the app is not an object on a backdrop — it IS the screen.
   -------------------------------------------------------------------------- */
html[data-mode="mobile"] .app-viewport {
  padding: 0;
  background: var(--color-bg);
}

/* The desktop shell is a 3-column grid (48px rail / 240px schedule / content)
   with a 660px floor. On a phone it is one cell: the page fills it, and the tab
   bar floats over the page's foot in the same cell (ADR-0248, amended 22 Oct), so the page
   scrolls behind the glass. `minmax(0, 1fr)` is what bounds the page so its own
   content scrolls instead of stretching the window (ADR-0079's invariant —
   every region scrolls its own overflow inside a bounded height). */
html[data-mode="mobile"] .app-container.shell {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  min-height: 0;
  max-width: none;
  border: 0;
  border-radius: 0;
}

/* Desktop chrome has no place on a phone. The rail and schedule are grid
   items, so hiding them also frees their tracks — the page is then the only
   item in row 1. The title bar (ADR-0158) would cost 44px of a phone's height;
   its account block lives at the top of the More sheet instead. */
html[data-mode="mobile"] .sidebar,
html[data-mode="mobile"] .app-nav,
html[data-mode="mobile"] .schedule-sidebar,
html[data-mode="mobile"] .app-titlebar {
  display: none;
}

/* The content card loses its window chrome and goes full bleed: a rounded card
   inset inside a rounded phone reads as a mistake, and 16px a side is real
   screen. */
html[data-mode="mobile"] .main-content {
  grid-area: 1 / 1;
  min-width: 0;
}

/* A desk page shown on a phone scrolls in its tab (ADR-0079): its foot clears the floating bar as a phone screen's does. A
   phone screen inside the tab is placed over the tab's padding box (inset 0), so the padding moves nothing of its own. */
html[data-mode="mobile"] .main-content-body > .tab-content:not(.hidden) {
  padding-bottom: var(--ph-scroll-foot);
  scroll-padding-bottom: var(--ph-scroll-foot);
}

html[data-mode="mobile"] .main-content-body {
  margin: 0;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

html[data-mode="mobile"] .main-content-body::before {
  display: none;
}

/* --------------------------------------------------------------------------
   THE TAB BAR (ADR-0248, amended 22 Oct)
   Today, Search, History, More. It floats over the page, the approved prototype's glass bar (phone.css's own rule): it
   shares the shell's one cell with the page, held at its foot 12px in from each side and --ph-bar-gap off the bottom edge,
   and the page scrolls behind it. Its own box is the glass: the blur is on the bar itself, because the bar's
   view-transition-name makes it a backdrop root, and a pseudo-element inside it would blur nothing. Every scroller's foot
   clears it by --ph-scroll-foot, and the notification stack sits above it by --mobile-nav-h.
   -------------------------------------------------------------------------- */
html[data-mode="mobile"] .ph-tabbar {
  grid-area: 1 / 1;
  align-self: end;
  position: relative;
  z-index: 20;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  box-sizing: border-box;
  height: var(--ph-tabbar-h);
  margin: 0 12px calc(var(--ph-bar-gap) + var(--ph-safe-b));
  padding: 6px;
  background: var(--ph-glass);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg);
}

html[data-mode="mobile"] .ph-tab {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  border: 0;
  background: none;
  padding: 0;
  border-radius: var(--radius-lg);
  color: var(--color-text-secondary);
  font: 600 var(--ph-fs-label)/1 var(--font-sans);
  cursor: pointer;
}

html[data-mode="mobile"] .ph-tab i {
  font-size: 1.4286rem;
  line-height: 1;
}

html[data-mode="mobile"] .ph-tab .ph-tab-pill {
  display: grid;
  place-items: center;
  width: 52px;
  height: 28px;
  border-radius: var(--radius-md);
  transition: background var(--ph-t-fade);
}

html[data-mode="mobile"] .ph-tab[aria-current="page"] { color: var(--color-accent); }

html[data-mode="mobile"] .ph-tab[aria-current="page"] .ph-tab-pill { background: var(--color-accent-tint); }

/* The More tab's count of what needs a look (updateMobileMoreBadge). */
html[data-mode="mobile"] .ph-tab .ph-dot {
  position: absolute;
  top: 4px;
  left: calc(50% + 12px);
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: var(--radius-full);
  background: var(--color-danger);
  color: #fff;
  font: 700 var(--ph-fs-label)/16px var(--font-sans);
  text-align: center;
}

html[data-mode="mobile"] .ph-banner {
  position: relative;
  z-index: 3;
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
  box-sizing: border-box;
  width: 100%;
  min-height: 40px;
  padding: 8px var(--ph-inset);
  background: var(--color-warning-tint);
  color: var(--color-text);
  font: var(--ph-t-meta);
}

html[data-mode="mobile"] .ph-banner[hidden] { display: none; }

html[data-mode="desktop"] .ph-tabbar,
html[data-mode="desktop"] .ph-banner {
  display: none;
}

/* ==========================================================================
   THE PHONE'S PARTS (ADR-0248; the approved prototype's phone.css, on the live shell)

   The page frame (bar, large title, scroller), the list vocabulary (label, card, row, tile), Today's tiles and timeline, and
   a place's header card: what `mobile-rows.js` and `mobile-today.js` build and the lanes after them draw with. The names are
   `ph-` because they are phone chrome with no desk twin (ADR-0218); a part the desk also has (the check-in button, the badge,
   the count, the chip) keeps its v3.css name and takes its phone size here or in v3.css.
   ========================================================================== */

/* ---- the screen: one scroller, bounded by the shell's row ---------------------------------------------------- */
html[data-mode="mobile"] .ph-screen { --page-pin-h: 0px; position: absolute; inset: 0; display: flex; flex-direction: column; background: var(--color-bg); color: var(--color-text); font: var(--ph-t-body); -webkit-tap-highlight-color: transparent; }

html[data-mode="mobile"] .ph-scroll {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: 0 var(--ph-inset) var(--ph-scroll-foot);
  scroll-padding-bottom: var(--ph-scroll-foot);
}

html[data-mode="mobile"] .ph-scroll > * + * { margin-top: var(--ph-gap); }

html[data-mode="mobile"] .ph-scroll > .ph-label + * { margin-top: 6px; }

/* ---- the header: a large title that scrolls away under a compact bar ----------------------------------------- */
html[data-mode="mobile"] .ph-bar {
  position: relative; z-index: 3; flex: none;
  display: grid; grid-template-columns: minmax(var(--ph-ctl), auto) 1fr minmax(var(--ph-ctl), auto); align-items: center;
  height: calc(var(--ph-head-h) + var(--ph-head-top) + var(--ph-safe-t)); padding: calc(var(--ph-safe-t) + var(--ph-head-top)) 6px 0;
  background: var(--color-bg);
  border-bottom: 1px solid transparent;
  transition: border-color var(--ph-t-fade), background var(--ph-t-fade);
}

html[data-mode="mobile"] .ph-bar.is-stuck { background: var(--ph-glass); backdrop-filter: blur(20px) saturate(160%); -webkit-backdrop-filter: blur(20px) saturate(160%); border-bottom-color: var(--color-border-subtle); }

html[data-mode="mobile"] .ph-bar-title { font: var(--ph-t-title); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: 0; transition: opacity var(--ph-t-fade); }

html[data-mode="mobile"] .ph-bar.is-stuck .ph-bar-title, html[data-mode="mobile"] .ph-bar.is-page .ph-bar-title { opacity: 1; }

html[data-mode="mobile"] .ph-bar-start, html[data-mode="mobile"] .ph-bar-end { display: flex; align-items: center; gap: 2px; }

html[data-mode="mobile"] .ph-bar-end { justify-content: flex-end; }

/* A top-level page's bar is the brand row (the live .app-brand, as on the desktop's top left), with the page's actions at
   its right (Today's Change day, Search's add-person). The large title sits right under it; the brand stays put while the
   page scrolls, so the bar shows no title of its own. */
/* A pushed page's bar is the page's navigation: a full-height row (56) with Back and the page's ⋮ drawn at a finger's
   weight and the title one step up, so it reads as the screen's header, not a thin strip (owner, 20 Oct). */
html[data-mode="mobile"] .ph-bar.is-page { height: calc(56px + var(--ph-head-top) + var(--ph-safe-t)); }

html[data-mode="mobile"] .ph-bar.is-page .ph-bar-title { font: var(--ph-t-title); }

html[data-mode="mobile"] .ph-bar.is-page .ph-back { font-size: 1.7143rem; }

html[data-mode="mobile"] .ph-bar.is-page .ph-bar-end > .ph-icon,
html[data-mode="mobile"] .ph-bar.is-page .ph-bar-end .member-overflow-trigger { font-size: 1.7143rem; }

html[data-mode="mobile"] .ph-bar.is-top { padding-left: var(--ph-inset); }

/* the brand lines up with the page's content */
html[data-mode="mobile"] .ph-bar.is-top .ph-bar-start { grid-column: 1 / 3; }

html[data-mode="mobile"] .ph-bar.is-top .ph-bar-title { display: none; }

html[data-mode="mobile"] .ph-hero { padding: 4px 0 2px; }

html[data-mode="mobile"] .ph-hero h1 { font: var(--ph-t-large); letter-spacing: -.02em; margin: 0; }

html[data-mode="mobile"] .ph-hero .ph-sub { font: var(--ph-t-meta); color: var(--color-text-secondary); margin-top: 2px; }

html[data-mode="mobile"] .ph-sub b { color: var(--color-text); font-weight: 650; }

/* A bar action that is a word (Today's "Today", back to the current day): the app's clear chip, in the accent. */
html[data-mode="mobile"] .ph-bar-end .chip-clear { height: var(--ph-ctl-s); padding: 0 10px; color: var(--color-accent); font-weight: 600; }

/* ---- icon button: a 44px target, the glyph in a 36px disc ---------------------------------------------------- */
html[data-mode="mobile"] .ph-icon {
  position: relative; display: inline-grid; place-items: center;
  width: var(--ph-ctl); height: var(--ph-ctl); padding: 0; border: 0; background: none;
  color: var(--color-text); font-size: 1.4286rem; border-radius: 50%;
}

html[data-mode="mobile"] .ph-icon::before { content: ''; position: absolute; inset: 4px; border-radius: 50%; background: transparent; transition: background var(--ph-t-fade); }

html[data-mode="mobile"] .ph-icon:active::before { background: var(--hover-row); }

html[data-mode="mobile"] .ph-icon > i { position: relative; }

html[data-mode="mobile"] .ph-icon.is-accent { color: var(--color-accent); }

html[data-mode="mobile"] .ph-back { color: var(--color-accent); font-size: 1.5714rem; }

/* ---- labels, cards, rows ------------------------------------------------------------------------------------- */
html[data-mode="mobile"] .ph-label { display: flex; align-items: baseline; gap: 8px; margin-top: 20px; padding: 0 4px; font: var(--ph-t-label); text-transform: uppercase; letter-spacing: .06em; color: var(--color-text-muted); }

html[data-mode="mobile"] .ph-label .ph-label-end { margin-left: auto; text-transform: none; letter-spacing: 0; font: var(--ph-t-meta); }

html[data-mode="mobile"] .ph-card { background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: var(--ph-r-card); box-shadow: var(--shadow-sm); overflow: hidden; }

html[data-mode="mobile"] .ph-card.is-pad { padding: 14px 16px; }

html[data-mode="mobile"] .ph-card.is-note { background: var(--color-warning-tint); border-color: color-mix(in srgb, var(--color-warning) 40%, var(--color-border)); box-shadow: none; }

html[data-mode="mobile"] .ph-card.is-alert,
html[data-mode="mobile"] .ph-waiver.is-danger { background: var(--color-danger-tint); border-color: color-mix(in srgb, var(--color-danger) 35%, var(--color-border)); box-shadow: none; }

html[data-mode="mobile"] .ph-row {
  position: relative; display: flex; align-items: center; gap: 12px;
  min-height: var(--ph-row); padding: 10px 14px; width: 100%; border: 0; background: none; text-align: left; color: inherit;
  text-decoration: none;   /* a row may be a link (a camp, a warning that opens it) */
}

html[data-mode="mobile"] .ph-row + .ph-row::after, html[data-mode="mobile"] .ph-card > .ph-earlier + .ph-row::after, html[data-mode="mobile"] .ph-reveal + .ph-reveal::after { content: ''; position: absolute; top: 0; left: 14px; right: 0; border-top: 1px solid var(--color-border-subtle); }

html[data-mode="mobile"] .ph-row.is-tap:active { background: var(--hover-row); }

html[data-mode="mobile"] .ph-row.is-danger { color: var(--color-danger); }

html[data-mode="mobile"] .ph-row-body { flex: 1; min-width: 0; }

html[data-mode="mobile"] .ph-row-title { font: var(--ph-t-row); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }

html[data-mode="mobile"] .ph-row-meta { font: var(--ph-t-meta); color: var(--color-text-secondary); margin-top: 2px; overflow-wrap: anywhere; }

html[data-mode="mobile"] .ph-row-meta b { color: var(--color-text); font-weight: 600; }

html[data-mode="mobile"] .ph-row-end { flex: none; display: flex; align-items: center; gap: 6px; color: var(--color-text-muted); font: var(--ph-t-meta); }

html[data-mode="mobile"] .ph-chev { color: var(--color-text-muted); font-size: 1rem; }

html[data-mode="mobile"] .ph-well { flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: var(--radius-md); background: var(--color-bg-subtle); color: var(--color-text-secondary); font-size: 1.1429rem; }

html[data-mode="mobile"] .ph-row.is-danger .ph-well { background: var(--color-danger-tint); color: var(--color-danger); }

/* A tinted line of text with an icon (alertLine): waiver, money, a note, medical. */
html[data-mode="mobile"] .ph-row-note { display: flex; gap: 6px; align-items: flex-start; margin-top: 6px; padding: 6px 8px; border-radius: var(--radius-md); background: var(--color-warning-tint); color: var(--color-text); font: var(--ph-t-meta); }

html[data-mode="mobile"] .ph-row-note > i { color: var(--color-warning-ink); margin-top: 1px; }

html[data-mode="mobile"] .ph-row-note > span { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

html[data-mode="mobile"] .ph-row-note.is-full > span { display: block; overflow: visible; }

html[data-mode="mobile"] .ph-row-note.is-danger { background: var(--color-danger-tint); }

html[data-mode="mobile"] .ph-row-note.is-danger > i { color: var(--color-danger); }

html[data-mode="mobile"] .ph-row-note.is-info { background: color-mix(in srgb, var(--color-info) 12%, var(--color-bg-elevated)); }

html[data-mode="mobile"] .ph-row-note.is-info > i { color: var(--color-info); }

html[data-mode="mobile"] .ph-row-note.is-ok { background: var(--color-success-tint); }

html[data-mode="mobile"] .ph-row-note.is-ok > i { color: var(--color-success); }

/* An opened row's actions: one line of the app's small secondary buttons at the row's foot, right-aligned. */
html[data-mode="mobile"] .ph-row-acts { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: 10px; cursor: default; }

html[data-mode="mobile"] .ph-row-acts > .btn { flex: none; }

/* ---- tiles: each its own surface in a list, no hairlines between them ---------------------------------------- */
html[data-mode="mobile"] .ph-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }

html[data-mode="mobile"] .ph-tile {
  position: relative; display: flex; align-items: center; gap: 12px; width: 100%; box-sizing: border-box;
  min-height: var(--ph-row); padding: 9px 11px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); text-align: left;
  color: inherit; background: var(--color-bg-elevated); box-shadow: var(--shadow-sm); /* the card's edge (ADR-0258); the padding gives back the border's 1px, so a tile keeps the size the ring drew */
}

html[data-mode="mobile"] .ph-tile.is-tap:active { background: var(--hover-row); }

html[data-mode="mobile"] .ph-tile.is-open { background: var(--color-bg-elevated); border-color: var(--color-border-strong); }

html[data-mode="mobile"] .ph-tile.is-stacked { display: block; }

/* A row that just changed (someone else checked in): a tint that fades, never a move. */
html[data-mode="mobile"] .ph-tile.is-fresh, html[data-mode="mobile"] .ph-row.is-fresh { animation: ph-fresh 4s ease-out; }

@keyframes ph-fresh {
  from { background-color: color-mix(in srgb, var(--color-accent) 24%, var(--color-bg-elevated)); box-shadow: 0 0 0 2px var(--color-accent); }
}

@media (prefers-reduced-motion: reduce) {
  html[data-mode="mobile"] .ph-tile.is-fresh, html[data-mode="mobile"] .ph-row.is-fresh { animation-duration: 1.5s; }
}

/* ---- buttons on the phone: the app's .btn family at a finger's height ----------------------------------------- */
/* ONE height for the family: no button is drawn taller than the check-in button (`--ph-btn`); .btn-sm is the small one. The
   finger's target is the invisible `--ph-hit` band behind the button, so the drawn button stays small. A sheet (createModal,
   `.is-sheet`) is a phone surface of its own, outside the screen, so its buttons (the footer's above all) wear the same. */
html[data-mode="mobile"] .ph-screen .btn,
html[data-mode="mobile"] .modal-overlay.is-sheet .btn { position: relative; box-sizing: border-box; height: var(--ph-btn); min-height: 0; padding: 0 16px; font-size: var(--ph-fs-body); font-weight: 600; line-height: 1; justify-content: center; }

html[data-mode="mobile"] .ph-screen .btn.btn-sm,
html[data-mode="mobile"] .modal-overlay.is-sheet .btn.btn-sm { height: var(--ph-btn-s); min-height: 36px; padding: 0 12px; font-size: var(--ph-fs-meta); }

html[data-mode="mobile"] .ph-screen .btn::after,
html[data-mode="mobile"] .modal-overlay.is-sheet .btn::after { content: ''; position: absolute; left: 0; right: 0; top: calc(50% - var(--ph-hit) / 2); height: var(--ph-hit); }

html[data-mode="mobile"] .ph-screen .btn i,
html[data-mode="mobile"] .modal-overlay.is-sheet .btn i { font-size: 1.1429rem; }

/* A sheet's footer is ONE row at 320: Delete, Cancel and Save changes are 288px with room to spare only at this padding.
   A footer with more than that (the log entry's Delete, Open account, Go to class check-in, Close) wraps onto a second row
   rather than running off the left edge. */
html[data-mode="mobile"] .modal-overlay.is-sheet .modal-footer .btn { padding-inline: 10px; }

html[data-mode="mobile"] .modal-overlay.is-sheet .modal-footer,
html[data-mode="mobile"] .modal-overlay.is-sheet .modal-footer-left,
html[data-mode="mobile"] .modal-overlay.is-sheet .modal-footer-actions { flex-wrap: wrap; }

html[data-mode="mobile"] .modal-overlay.is-sheet .modal-footer-actions { justify-content: flex-end; }

/* The controls drawn smaller than a finger carry the same invisible band a .btn does, centred on them: a sheet's disclosure
   row (on its ::before: a summary's ::after is its chevron). A field's clear and the scroll hint carry
   theirs in v3.css, beside their own base rules. */
html[data-mode="mobile"] .modal-overlay.is-sheet summary { position: relative; }

html[data-mode="mobile"] .modal-overlay.is-sheet summary::before { content: ''; position: absolute; left: 50%; top: 50%; width: max(100%, var(--ph-hit)); height: var(--ph-hit); transform: translate(-50%, -50%); }

/* The search field stays under the title while the page scrolls. It is the desk's own field (`.attendance-search`),
   moved in by the roster page, wearing the phone's size. */
html[data-mode="mobile"] .ph-searchbar { position: sticky; top: 0; z-index: 2; margin-inline: calc(-1 * var(--ph-inset)); padding: 4px var(--ph-inset) 10px; background: var(--color-bg); }

html[data-mode="mobile"] .ph-searchbar[hidden] { display: none; }

html[data-mode="mobile"] .ph-searchbar .attendance-search { width: 100%; height: 44px; min-height: 44px; box-sizing: border-box; align-items: center; gap: 8px; padding: 0 12px; border-radius: var(--radius-lg); }

html[data-mode="mobile"] .ph-searchbar .attendance-search-scope { display: none; }

html[data-mode="mobile"] .ph-searchbar .attendance-search input { font-size: max(var(--ph-fs-body), 16px); }

/* A person row, wherever the roster offers one. What they hold sits under the name, so the name is never squeezed. */
html[data-mode="mobile"] .ph-row-badges { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 6px; }

html[data-mode="mobile"] .ph-row-time { font-variant-numeric: tabular-nums; }

html[data-mode="mobile"] .ph-tag { display: inline-flex; align-items: center; gap: 4px; height: 22px; padding: 0 8px; border-radius: var(--radius-sm); font: 600 var(--ph-fs-meta)/1 var(--font-sans); white-space: nowrap; background: color-mix(in srgb, var(--tag, var(--color-text-secondary)) 12%, var(--color-bg-elevated)); color: color-mix(in srgb, var(--tag, var(--color-text-secondary)) 80%, var(--color-text)); }

html[data-mode="mobile"] .ph-tag.is-warn { --tag: var(--color-warning); }

html[data-mode="mobile"] .ph-tag.is-accent { --tag: var(--color-accent); }

/* A checked-in row ends in its ⋮: a 44px target that sits in the row's padding, so the row stays its height. */
html[data-mode="mobile"] .ph-row-more { flex: none; margin: -6px -8px -6px 0; }

html[data-mode="mobile"] .ph-screen .member-overflow-trigger:not(.btn) { display: grid; place-items: center; width: var(--ph-ctl); height: var(--ph-ctl); border: 0; border-radius: 50%; background: none; color: var(--color-text-secondary); font-size: 1.2857rem; opacity: 1; }

html[data-mode="mobile"] .ph-screen .member-overflow-trigger:not(.btn):active { background: var(--hover-row); }

/* ---- notifications (design-system/notification-service.js): the phone's toast ------------------------------------- */
/* The desk's card, drawn as the prototype's toast: the full width between the gutters above the tab bar, inverted
   colours so it reads over any screen, the tier in its dot, and a flat action at a finger's height. */
html[data-mode="mobile"] #notification-stack { left: var(--ph-inset); right: var(--ph-inset); align-items: stretch; }

html[data-mode="mobile"] .notification { gap: 10px; min-height: 48px; padding: 6px 6px 6px 14px; border: 0; background: var(--color-text); color: var(--color-bg); }

html[data-mode="mobile"] .notification__icon { background: color-mix(in srgb, var(--color-accent) 70%, var(--color-bg)); }

html[data-mode="mobile"] .notification--success .notification__icon { background: color-mix(in srgb, var(--color-success) 70%, var(--color-bg)); }

html[data-mode="mobile"] .notification--warning .notification__icon { background: color-mix(in srgb, var(--color-warning) 80%, var(--color-bg)); }

html[data-mode="mobile"] .notification--error .notification__icon { background: color-mix(in srgb, var(--color-danger) 70%, var(--color-bg)); }

html[data-mode="mobile"] .notification__text { padding: 6px 0; color: inherit; font: 500 var(--ph-fs-body)/1.35 var(--font-sans); }

html[data-mode="mobile"] .notification__detail { color: color-mix(in srgb, var(--color-bg) 72%, transparent); font: var(--ph-t-meta); }

html[data-mode="mobile"] .notification .notification__action { height: 36px; padding: 0 12px; border: 0; border-radius: var(--radius-sm); background: color-mix(in srgb, var(--color-bg) 14%, transparent); color: inherit; font: 700 var(--ph-fs-meta)/1 var(--font-sans); text-transform: none; letter-spacing: 0; }

html[data-mode="mobile"] .notification .notification__action.is-quiet { background: none; color: color-mix(in srgb, var(--color-bg) 72%, transparent); }

html[data-mode="mobile"] .notification__dismiss { width: 36px; height: 36px; color: color-mix(in srgb, var(--color-bg) 60%, transparent); }

html[data-mode="mobile"] .notification--detailed { padding: 10px 6px 10px 14px; }

html[data-mode="mobile"] .notification--detailed .notification__icon { margin-top: 11px; }

html[data-mode="mobile"] .notification--detailed .notification__text { font-weight: 600; }

html[data-mode="mobile"] .ph-banner-open { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; min-height: 24px; padding: 0; border: 0; background: none; color: inherit; font: inherit; text-align: left; }

html[data-mode="mobile"] .ph-banner-open > i:first-child { flex: none; color: var(--color-warning-ink); font-size: 1.1429rem; }

html[data-mode="mobile"] .ph-banner-open > span { flex: 1; min-width: 0; }

html[data-mode="mobile"] .ph-banner-open b { font-weight: 700; }

html[data-mode="mobile"] .ph-notice { display: flex; align-items: center; gap: var(--ph-gap); padding: 12px 14px; border-radius: var(--radius-lg); background: var(--color-bg-elevated); border: 1px solid var(--color-border); }

html[data-mode="mobile"] .ph-notice > i:first-child { flex: none; font-size: 1.3571rem; color: var(--color-text-secondary); }

html[data-mode="mobile"] .ph-notice.is-warn { background: var(--color-warning-tint); border-color: color-mix(in srgb, var(--color-warning) 45%, var(--color-border)); }

html[data-mode="mobile"] .ph-notice.is-warn > i:first-child { color: var(--color-warning-ink); }

html[data-mode="mobile"] .ph-notice.is-blocker { background: var(--color-danger-tint); border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-border)); }

html[data-mode="mobile"] .ph-notice.is-blocker > i:first-child { color: var(--color-danger); }

html[data-mode="mobile"] .ph-notice.is-info { background: color-mix(in srgb, var(--color-info) 10%, var(--color-bg-elevated)); border-color: color-mix(in srgb, var(--color-info) 35%, var(--color-border)); }

html[data-mode="mobile"] .ph-notice.is-info > i:first-child { color: var(--color-info); }

html[data-mode="mobile"] .ph-tab .ph-dot.is-count { background: var(--color-accent); }

/* --------------------------------------------------------------------------
   THE SEARCH BAND BELONGS TO THE SEARCH TAB (ADR-0083, M3)

   On a desktop the band is shell furniture: it sits above the content card and
   is always available. On a phone that costs 109px of an 812px screen on every
   page that cannot use it — and worse, a destination page then shows TWO
   search fields, the top one global and inert to the roster below it, the
   lower one the page's own "Check someone in by name". The design gives each
   mobile screen its own header for exactly this reason.

   Global search is one tap away in the bottom nav, which IS the phone's
   equivalent of the standing band.
   -------------------------------------------------------------------------- */
html[data-mode="mobile"] .app-main-search {
  display: none;
}

/* v3.css:2124 gives `.shell .app-main` a 7px top padding so the main column
   starts level with the nav rail's first item. There is no rail on a phone,
   and every mobile screen owns its own chrome from the very top — so that
   padding only exposed a 7px band of `.app-container`'s lighter surface
   (oklch .999) above each screen's header, against the page's own oklch .975.
   Read as a random white bar across the top of the roster. */
html[data-mode="mobile"] .shell .app-main {
  padding: 0;   /* all sides: the compact shell's 16px left gutter (v3.css, data-shell="compact") belongs to the 601-1023 card */
}

/* `.chip` is a <button> here, which the base class does not assume. */
html[data-mode="mobile"] button.chip { cursor: pointer; }

/* ============================================================================
   THE DESIGN'S OWN SCALE (measured diff, ADR-0087 §11)

   The phone's design uses radii of 8 / 9 / 10 / 12 / 16 and states its icon
   sizes per component. The desktop tokens (--radius-sm 4, --radius-md 6,
   --radius-lg 8) are a DIFFERENT scale, and inheriting them left every row,
   card and sheet a few pixels squarer than the design and several glyphs a
   size or two small. Each rule below carries the design's number.
   ============================================================================ */


/* A page's numbers in its header (ADR-0213) have no room on the phone's one 48px line: the title and the page's
   actions keep it. */
html[data-mode="mobile"] .header-facts {
  display: none;
}

