

/* ================================================================
   PASS BADGES — LOCKED v3 design (pass-badges.html)
   ================================================================ */
.pass-badge {
  display: inline-flex; align-items: stretch;
  /* A badge is a fixed shape: it never shrinks or wraps inside the row it sits in (it used to be pinned per row
     family, so a row that did not pin it squeezed its label into two clipped lines). */
  flex: none; white-space: nowrap;
  border-radius: var(--radius-sm);
  font-size: 0.7857rem; font-weight: var(--font-semibold);
  overflow: hidden;
  height: 22px;
  /* Single render path, same as the app: a hex on the inline --badge-color var
     drives the whole pill. Category classes below are the static-markup path. */
  --bcat: var(--badge-color, #6b7280);
  border: 1px solid color-mix(in srgb, var(--bcat) 28%, var(--color-border));
}
/* Inline label icons (renderBadge): shared group pass, and payer-funded visit. */
.pass-badge .badge-shared-icon { font-size: .95em; margin-right: 4px; opacity: .9; }
.pass-badge .count-arrow { font-size: .8em; margin: 0 1px; opacity: .9; }
.pass-badge .badge-label {
  padding: 0 8px;
  display: inline-flex; align-items: center;
  background: color-mix(in srgb, var(--bcat) 10%, var(--color-bg-elevated));
  color: color-mix(in srgb, var(--bcat) 75%, var(--color-text));
  letter-spacing: 0.005em;
  /* 90px, matching production's own pre-V3 value. ONE floor for every
     label — icon or not, one icon or two — so the whole column is one width
     regardless of content. Keep both files in sync until they're merged
     into one owner (docs/v3-spec-audit.md, "dual owners and dead names"). */
  min-width: 90px; justify-content: center;
}
/* A label-only pill (the add-on fee chip) has no count segment to carry the
   solid fill, so the label takes it — same weight the app gives it. */
.pass-badge:not(:has(.badge-count)) .badge-label { background: var(--bcat); color: var(--badge-text-color, #fff); }
.pass-badge.athletic:not(:has(.badge-count)) .badge-label { color: #1a1300; }
.pass-badge .badge-count {
  padding: 0 8px;
  display: inline-flex; align-items: center;
  /* A fee count paints its own neutral fill (renderBadge's countColor path);
     everything else takes the resolved category color. */
  background: var(--badge-count-color, var(--bcat));
  color: var(--badge-text-color, #fff);
  font-weight: var(--font-semibold);
  /* 48px: floor raised from 32 so the common count contents — "5", "62d",
     "118d", "$20", and a one-step 8→7 transition — all land on ONE width.
     Only rare double-wide content (e.g. 118d→117d) exceeds it. */
  min-width: 55px; justify-content: center;
  border-left: 1px solid color-mix(in srgb, var(--bcat) 28%, var(--color-border));
}
.pass-badge .badge-action {
  padding: 0 8px;
  display: inline-flex; align-items: center;
  background: color-mix(in srgb, var(--bcat) 80%, #000);
  color: var(--badge-text-color, #fff);
  text-transform: uppercase;
  font-size: 0.7143rem; letter-spacing: 0.02em;
  min-width: 42px; justify-content: center;
  border-left: 1px solid rgba(0,0,0,.12);
  transition: background .15s;
}
.pass-badge .badge-action:hover { background: color-mix(in srgb, var(--bcat) 65%, #000); }

/* THE PHONE BADGE: Stack (ADR-0248; owner pick, 19 Oct).
   The badge's problem on a phone is its shape, not its size. The desk badge is a 145px bar built to line up in desk table
   columns; on a 390px row only two fit across the name column, so it stacks one per line under the name and a person with
   three holdings is four lines of badge. Stack stands the same badge upright: the solid count on top, the tinted label
   under it, 52 x 44, so the badges sit at the row's right on the name's line and a person row is one line. The markup is
   renderBadge's, unchanged (colours, tint, solid count, 1px category border, --radius-sm), so tapping a badge, and its own
   Start, Resume or Renew, spend exactly as before. It applies inside the phone's pages (`.ph-screen`, mobile.css); the
   rows the lanes after Today move onto those pages take it with no caller change, and a screen not yet moved keeps the
   badge it has. Every sheet is a phone surface, so a badge in a sheet (the log entry, the check-in sheet) is Stack too. */
html[data-mode="mobile"] :is(.ph-screen, .modal-overlay.is-sheet) .pass-badge {
  flex-direction: column-reverse; height: 44px; width: auto; min-width: 52px; box-sizing: border-box;
}
/* The label never widens the badge (a long name clips at 52); only a long count (a history 12 → 11) does. */
html[data-mode="mobile"] :is(.ph-screen, .modal-overlay.is-sheet) .pass-badge .badge-label {
  contain: inline-size; min-width: 0; height: 16px; padding: 0 3px; justify-content: center;
  font-size: 0.6429rem; letter-spacing: 0; white-space: nowrap; overflow: hidden; text-overflow: clip;
}
html[data-mode="mobile"] :is(.ph-screen, .modal-overlay.is-sheet) .pass-badge .badge-count {
  flex: 1; min-width: 0; padding: 0 4px; border-left: 0;
  border-bottom: 1px solid color-mix(in srgb, var(--bcat) 28%, var(--color-border));
  font-size: 1.0714rem; font-variant-numeric: tabular-nums;
}
/* Start / Resume / Renew: the action stands full height at the stack's right, so the stack keeps its square. */
html[data-mode="mobile"] :is(.ph-screen, .modal-overlay.is-sheet) .pass-badge:has(.badge-action) { display: inline-grid; grid-template: 1fr 16px / minmax(52px, auto) auto; }
html[data-mode="mobile"] :is(.ph-screen, .modal-overlay.is-sheet) .pass-badge:has(.badge-action) .badge-count { grid-area: 1 / 1; }
html[data-mode="mobile"] :is(.ph-screen, .modal-overlay.is-sheet) .pass-badge:has(.badge-action) .badge-label { grid-area: 2 / 1; }
html[data-mode="mobile"] :is(.ph-screen, .modal-overlay.is-sheet) .pass-badge .badge-action { grid-area: 1 / 2 / 3 / 3; min-width: 0; padding: 0 8px; font-size: 0.6429rem; }
/* A label-only badge (a tag membership such as Staff, an add-on fee) has no count to stand on: it keeps its solid fill, as
   wide as its words. */
html[data-mode="mobile"] :is(.ph-screen, .modal-overlay.is-sheet) .pass-badge:not(:has(.badge-count)) .badge-label { contain: none; height: 100%; padding: 0 8px; font-size: 0.7143rem; }
html[data-mode="mobile"] :is(.ph-screen, .modal-overlay.is-sheet) .pass-badge .badge-shared-icon { display: none; }

/* Category vars — hexes come from the gym's live access config (Settings, Access).
   Both private pass types share one red; a type's own color field is the
   rendering authority, so these classes are the static-markup mirror of it. */
.pass-badge.opengym  { --bcat: var(--color-opengym); }
.pass-badge.classes  { --bcat: var(--color-classes); }
.pass-badge.athletic { --bcat: var(--color-athletic); }
.pass-badge.private  { --bcat: var(--color-private); }
.pass-badge.camp     { --bcat: var(--color-camp); }
/* Tag memberships — identity markers, always win badge priority */
.pass-badge.staff     { --bcat: var(--color-staff); }
.pass-badge.sponsored { --bcat: var(--color-sponsored); }
/* Synthetic chips, not tied to a type (BADGE_COLORS) */
/* Drop-in carries no color of its own: renderBadge colors it by the class's
   access category (class drop-in = classes blue, athletic = gold, day pass =
   open-gym green). This static class defaults to the common class drop-in.
   BADGE_COLORS.dropin #00897b is only the Square Unmapped/Unknown fallback. */
.pass-badge.dropin   { --bcat: var(--color-classes); }
.pass-badge.addon    { --bcat: var(--color-addon); }
/* "Zero" is not its own type — .badge-zero below is a count STATE, not a category. */
/* State overlays — these REPLACE the type color (STATE_COLORS) */
.pass-badge.paused,
.pass-badge.attention { --bcat: var(--color-paused); }
.pass-badge.expired  { --bcat: var(--color-expired); }
.pass-badge.faded    { opacity: .45; }
/* Athletic: yellow needs dark text — but ONLY on the type fill. A state fill
   (decreased, depleted, zero) paints its own dark hue where that ink would be
   unreadable, so those keep white. Declared BEFORE the state block so the
   states win on equal specificity. */
.pass-badge.athletic .badge-count,
.pass-badge.athletic .badge-action { color: #1a1300; }
/* A fee count keeps white ink: it sits on the neutral fill, not on the gold. */
.pass-badge.athletic .badge-count[style*="--badge-count-color"] { color: #fff; }
/* Count-change states. Fixed hues, white text: they must read the same on every
   category, so they do not derive from --bcat. */
.pass-badge .badge-count.increased,
.pass-badge.badge-adding .badge-count   { background: #16a34a; color: #fff; }
.pass-badge .badge-count.decreased,
.pass-badge.badge-removing .badge-count { background: #dc2626; color: #fff; }
.pass-badge .badge-count.depleted       { background: #64748b; color: #fff; }
.pass-badge.badge-zero .badge-count     { background: #9ca3af; color: #fff; }
/* Interaction states */
.pass-badge.clickable { cursor: pointer; transition: transform .1s ease, box-shadow .1s ease; }
.pass-badge.clickable:hover { transform: scale(1.02); box-shadow: var(--shadow-sm); }
.pass-badge.is-pending { opacity: .7; cursor: wait; pointer-events: none; }
[data-theme="dark"] .pass-badge .badge-label {
  background: color-mix(in srgb, var(--bcat) 18%, var(--color-bg-elevated));
  color: color-mix(in srgb, var(--bcat) 55%, #fff);
}
@media (max-width: 600px) { .compare { grid-template-columns: 1fr; } .compare > div + div { border-left: 0; border-top: 1px solid var(--color-border); } }

/* ================================================================
   SCHEDULE SIDEBAR (schedule-cards.html)
   Card anatomy follows live style.css .schedule-class-item ~9933; selection
   and time-of-day are the reworked spec, not the live tab style.
   ================================================================ */
.schedule-list { display: flex; flex-direction: column; gap: var(--space-1); }
.schedule-class-item {
  position: relative;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
  background: color-mix(in srgb, var(--class-color, var(--color-bg-muted)) 10%, var(--color-bg-elevated));
  border: 1px solid color-mix(in srgb, var(--class-color, var(--color-border)) 22%, var(--color-border));
  border-radius: var(--radius-md);
  cursor: pointer;
  /* Explicit list, NOT `all`: width (the 16px bleed) and box-shadow (the ring
     and float) must apply instantly, or a clicked card grows and lights up
     over 150ms instead of simply being selected. */
  transition: background-color .15s ease, border-color .15s ease;
  text-align: left; font-family: inherit;
  font-size: var(--text-sm); color: var(--color-text);
}
.schedule-class-item:hover {
  background: color-mix(in srgb, var(--class-color, var(--color-bg-muted)) 18%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--class-color, var(--color-border)) 55%, var(--color-border));
  box-shadow: 0 2px 8px color-mix(in srgb, var(--class-color) 18%, transparent);
}
/* Selection reads as a 2px edge but keeps the resting 1px border box: the
   second pixel is an inset ring, so selecting a card never changes its size
   and the rest of the list can't shift. No drop shadow: it spilled past the
   seam in the shell, so selection spends tint + ring only. */
.schedule-class-item.selected {
  background: color-mix(in srgb, var(--class-color, var(--color-accent)) 20%, var(--color-bg-elevated));
  border-color: var(--class-color, var(--color-accent));
  box-shadow: inset 0 0 0 1px var(--class-color, var(--color-accent));
}
.schedule-class-item.selected .class-item-name { font-weight: var(--font-semibold); }
/* Same focus treatment as the search bars (.attendance-search:focus-within) —
   a border-color change + soft tinted glow, not an offset outline. An offset
   outline draws OUTSIDE the card's box, so on a scrolling list it can get
   clipped by the sidebar's overflow on whichever edge has the least
   padding to spare (reported as "missing left border"); a border-color
   swap is part of the box itself and always renders on all four sides. */
.schedule-class-item:focus-visible,
.schedule-report:focus-visible,
.schedule-class-item.focus-ring,
.schedule-report.focus-ring {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-tint);
}

/* Category color hooks — locked palette. Applied to every element in the
   card family (cards and time labels) so the category color never
   silently falls back to a neutral or the generic accent. */
.schedule-class-item.opengym, .schedule-time-label.opengym { --class-color: #4caf50; }
.schedule-class-item.opengym.selected { background: color-mix(in srgb, var(--class-color) 15%, var(--color-bg-elevated)); }
.schedule-class-item.classes, .schedule-time-label.classes { --class-color: var(--color-classes); }
.schedule-class-item.athletic, .schedule-time-label.athletic { --class-color: var(--color-athletic); }
.schedule-class-item.private, .schedule-time-label.private { --class-color: var(--color-private); }
.schedule-class-item.camp, .schedule-time-label.camp { --class-color: var(--color-camp); }
.schedule-class-item.today-visitors { --class-color: var(--color-text-secondary); }

/* Quick group-event add — same card shape as the list above (padding,
   radius, layout), but dashed and empty since it's an "add a slot" affordance,
   not a destination. No --class-color of its own, so it never borrows one via
   the base card's color-mix fallback. */
.schedule-quick-add {
  justify-content: center;
  background: transparent;
  border-style: dashed;
  border-color: var(--color-border);
  color: var(--color-text-secondary);
}
.schedule-quick-add:hover {
  background: var(--color-bg-subtle);
  border-color: var(--color-text-muted);
  color: var(--color-text);
  box-shadow: none;
}
.schedule-quick-add .class-item-name {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-weight: var(--font-medium);
}

.class-item-info { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.class-item-name { font-weight: var(--font-medium); color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Same text-metric rule as `.class-item-slot-time` (ADR-0086 §2.1): both are
   boxes whose content is entirely text, with no padding, icon or border of
   their own, so they get the SAME single-line guard `.class-item-name`
   already has, instead of ADR-0086's fix staying scoped to the one cell it
   measured as tight. Without `nowrap` a squeeze is answered by wrapping —
   the exact failure ADR-0086 diagnosed — and these two were the gap: the
   Open Gym card's "11am - 10pm" subtitle and a non-`.mini` age group (e.g.
   `.next-slot`'s name-over-age layout) shared the same column budget as the
   name line but never got the guard. `.mini .class-item-age` already
   declared this triple locally; it now only needs its layout overrides. */
.class-item-time, .class-item-age {
  font-size: var(--text-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* A headcount on a card's detail line: a person icon, then the number, no word
   (ADR-0239). It never breaks between the two; the check-in count chip on the
   card's right edge is the other number, who is HERE, in its own box. */
.class-item-headcount { white-space: nowrap; }
.class-item-headcount .bi { margin-right: 2px; }
/* The room an event is in, by its Settings short name ("R1"), after the
   headcount, 6px from it as the line's parts are, so it lines up beside the check-in
   count on every card in a room. It never breaks or gives way either. */
.class-item-room { white-space: nowrap; margin-left: 6px; font-weight: var(--font-semibold); }
/* A style's icon leads the event's line (ADR-0241, 0242), a word's space before it:
   a size up from the name and in the card's colour, darkened toward the text so
   a light colour still reads on the card's own tint. */
.class-item-kind-icon {
  margin-right: var(--space-1); font-size: 1.2em; line-height: 0;
  color: color-mix(in srgb, var(--class-color) 70%, var(--color-text));
}
/* Its glyph rides the name's line box, so a bigger icon never makes the card taller. */
.class-item-kind-icon::before { line-height: 0; }
/* Two states, and only two, both a tint of the card's colour with no edge:
   a light tint and muted digit when nobody has checked in, a stronger tint
   and ink digit once someone has. The LIGHT tint is the base, so markup that
   omits a modifier renders "no check-ins yet". */
.class-item-count {
  /* Fixed width, identical in all three densities: the time cell is laid out
     relative to this box, so any per-density width change shifts the whole
     time column. Height still varies to carry the ladder. */
  /* padding was `2px 0 0` — copied verbatim from the design's own base.css,
     but a top-only pad on a flex-centered box pushes the number 1px below
     true centre in every density (24px, 18px, and the header tally's 26px).
     User-confirmed live: it reads low. Flex centring alone is exact; the
     padding was never needed. */
  width: 28px; height: 24px;
  display: flex; align-items: center; justify-content: center;
  /* A see-through layer of the card's colour, not a mix toward the surface:
     the card's own tint changes with its density (10% resting, more when live
     or selected), and a layer always lands one step above whatever the card
     is. On a near-white configured colour (#d9d9d9) the states' fills nearly
     meet; the muted digit against the filled state's ink digit separates them. */
  background: color-mix(in srgb, var(--class-color, var(--color-text-muted)) 8%, transparent);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs); font-weight: var(--font-semibold); line-height: 1;
  color: var(--color-text-secondary); flex-shrink: 0;
}

/* Filled chip = someone has checked in (cumulative for the day; check-ins are
   never un-counted when people leave). The category color is the fill. */
/* Filled = at least one check-in. The class color is mixed toward the light
   surface at a fixed 40%, opaque so it never stacks on the card's tint, so ANY hex lands light enough for one near-black
   digit color to stay legible: production supplies --class-color as per-class
   data, so a white digit would fail on every mid-to-light hue (#FFCA28
   measured 1.5:1) and category-keyed guards can't fire on cards that carry no
   category class. */
/* The phone draws this count too (Today's timeline, ADR-0248) and takes the colour from these two rules rather than
   restating them: they are pure colour — the ADR-0093 formula — and it is exactly this half that drifted when a phone
   copy kept a solid fill for months after the desktop chip was rebuilt. The phone's size is mobile.css's. */
.class-item-count.has-checkins {
  background: color-mix(in srgb, var(--class-color, var(--color-success)) 40%, var(--color-bg-elevated));
  color: var(--color-chip-ink);
}

/* DARK: the same chip, on a dark ground.

   The rule above mixes the class colour 40% toward the light surface so that
   ONE fixed near-black ink stays legible on any configured hex. That was
   reasoned in light mode, and it is theme-blind: in dark it paints a pale
   mint chip on a dark panel — a light-theme chip that never got the memo.

   Dark keeps the SAME construction and only swaps the two theme-bound ends:
   the fill mixes toward the dark elevated surface instead of white, and the
   ink follows the theme by mixing the class colour toward --color-text (the
   tint standard's own formula, ADR-0093), which lightens on dark exactly as
   --color-chip-ink darkens on light. Arbitrary hues stay legible for the
   same reason they do in light: the ink is derived from the hue, not fixed
   against it. Light mode is untouched. */
[data-theme="dark"] .class-item-count.has-checkins {
  /* 30%, no higher: dark mixes toward the dark ELEVATED surface, where 40%
     of a bright hue lands too light for a near-white digit — measured 4.27 on
     #ffea00, under AA's 4.5 for text. 30% takes the worst hue to 5.80. */
  background: color-mix(in srgb, var(--class-color, var(--color-success)) 30%, var(--color-bg-elevated));
  color: color-mix(in srgb, var(--class-color, var(--color-success)) 25%, var(--color-text));
}

/* ----------------------------------------------------------------
   Time-of-day: grouped timeline (exploration 2b)
   Small uppercase labels carry the time semantics, so cards stay
   uniform and no card needs opacity, grayscale, or border tricks
   to say when it is. Past classes collapse to one-line rows.
   ---------------------------------------------------------------- */
/* DEPRECATED — superseded by .schedule-visitors inside .schedule-day-block.
   Kept for unmigrated pages.
   Report row: a running total across every destination, so it is NOT a class
   card and does not sit in the class list. Neutral surface, no category tint,
   label set as a small caption so it never reads as a peer of the classes.
   Selectable (the main panel can show it), and its selection uses the generic
   accent because no category owns it. */
.schedule-report {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  width: 100%; margin-bottom: var(--space-2);
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
  background: var(--color-bg-subtle);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  cursor: pointer; text-align: left; font-family: inherit;
  transition: background-color .15s ease, border-color .15s ease;
}
.schedule-report:hover { background: var(--color-bg-muted); border-color: var(--color-text-muted); }
.schedule-report .report-label {
  font-size: var(--text-sm); font-weight: var(--font-medium);
  color: var(--color-text-secondary);
}
.schedule-report .report-total {
  font-size: var(--text-lg); font-weight: var(--font-bold);
  color: var(--color-text); font-variant-numeric: tabular-nums;
}
.schedule-report.selected {
  background: color-mix(in srgb, var(--color-accent) 14%, var(--color-bg-elevated));
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 1px var(--color-accent);
}
.schedule-sidebar .schedule-report { width: calc(100% - var(--space-4)); margin-right: var(--space-3); }
.schedule-sidebar.bleed .schedule-report.selected { width: calc(100% + 16px); margin-right: 0; position: relative; z-index: 10; }

/* ============================================================
   DAY BLOCK — the day caption and the visitors card, one group above the
   list, replacing three stacked bands: the 48px date nav, the full-width
   mode band and the separate report row. 126px of chrome became 85px.
   Ordered by frequency, not importance: the schedule is the page, the day
   is a management errand, the mode flag is a temporary experiment.
   ============================================================ */
.schedule-day-block { padding: var(--space-3) var(--space-3) var(--space-2); border-bottom: 1px solid var(--color-border); }
.schedule-day-caption { display: flex; align-items: center; gap: 4px; min-width: 0; margin-bottom: 6px; }
/* The date is a VALUE, not a section label, so it is set at the same 12px as
   the card's label below it and the date reads identically wherever it
   appears. Explicit 12px, equal to --text-sm now that the type tokens are px. */
.schedule-day-label {
  flex: 1; min-width: 0; height: 26px; padding: 0 var(--space-2);
  display: flex; align-items: center; justify-content: center; gap: 5px;
  border: 0; background: transparent; border-radius: var(--radius-sm);
  font-size: 0.8571rem; font-weight: var(--font-medium); color: var(--color-text);
  white-space: nowrap; cursor: default;
}
.schedule-day-label i { font-size: 0.7143rem; }
/* The date slot IS the return to today, the way the app already works. On
   today there is nowhere to return to, so it is inert text (a span). Off today
   it is a button and says the action in words instead of naming the day,
   because the card below carries the viewed date: the day is never unnamed,
   never named twice, and the slot never moves. */
button.schedule-day-label { color: var(--color-accent); font-weight: var(--font-semibold); cursor: pointer; }
button.schedule-day-label:hover { background: var(--color-accent-tint); }
.schedule-day-btn {
  width: 26px; height: 26px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; background: transparent; border-radius: var(--radius-sm);
  color: var(--color-text-muted); cursor: pointer; font-size: 0.8571rem;
}
.schedule-day-btn.step { width: 20px; }
.schedule-day-btn:hover { background: var(--color-bg-subtle); color: var(--color-text); }
/* A control that does not apply goes inert, never absent. A control that
   appears shifts everything beside it under a hand that is already moving. */
.schedule-day-btn[disabled] { opacity: .28; cursor: default; }
.schedule-day-btn[disabled]:hover { background: transparent; color: var(--color-text-muted); }
.schedule-day-btn:focus-visible, .schedule-day-label:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* Visitors card. A destination the main panel can show, not a caption, so it
   takes the same selection as any row — including the 16px overhang past the
   seam. Bleed is horizontal, so sitting above the block's hairline changes
   nothing and the rule stays one rule: whatever is selected floats the seam. */
.schedule-visitors {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  width: 100%; padding: 6px var(--space-2);
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-bg-subtle); cursor: pointer; text-align: left;
  transition: background-color .15s ease, border-color .15s ease;
}
.schedule-visitors:hover { background: var(--color-bg-muted); }
.schedule-visitors-label { display: flex; align-items: center; gap: 6px; font-size: 0.8571rem; font-weight: var(--font-medium); color: var(--color-text-secondary); }
.schedule-visitors-label i { font-size: 0.7857rem; color: var(--color-text-muted); }
.schedule-visitors-count { font-size: var(--text-md); font-weight: var(--font-semibold); color: var(--color-text); font-variant-numeric: tabular-nums; }
.schedule-visitors.selected {
  background: color-mix(in srgb, var(--color-accent) 14%, var(--color-bg-elevated));
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 1px var(--color-accent);
}
.schedule-visitors.selected .schedule-visitors-label,
.schedule-visitors.selected .schedule-visitors-label i { color: var(--color-accent); }
/* The block owns 12px of right padding, so +28px puts the selected card 16px
   past the seam, the same overhang a selected class card takes. */
.schedule-sidebar.bleed .schedule-visitors.selected { width: calc(100% + 28px); position: relative; z-index: 10; }
.schedule-visitors:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* Empty schedule: no classes booked for the chosen day. */
.schedule-empty {
  padding: var(--space-6) var(--space-3);
  text-align: center; font-size: var(--text-sm);
  color: var(--color-text-secondary);
}
.schedule-empty .empty-hint { display: block; margin-top: 4px; font-size: var(--text-xs); color: var(--color-text-secondary); }

/* The rule separates the always-on block (Open Gym) from today's timed
   schedule. */
/* The all-day block's two slot anchors (camps, day events) add no box of their own: their cards are
   the column's own children, so the column's one gap spaces every all-day card, and an empty anchor
   adds no second gap above the cards that follow (ADR-0243). */
.schedule-slots { display: contents; }
.schedule-allday-end { height: 1px; background: var(--color-border); margin: var(--space-2) 2px 0; }

.schedule-time-label {
  display: block;
  padding: var(--space-3) 2px var(--space-1);
  font-size: 0.6786rem; font-weight: var(--font-bold);
  letter-spacing: .12em; text-transform: uppercase;
  /* text-secondary, not muted: at 9.5px these labels carry the time
     semantics, so they need AA contrast, not decorative grey. */
  color: var(--color-text-secondary);
  white-space: nowrap;
}
/* "Next up" label: the category color is a FILL token, never text — a filled
   dot carries the category, the words stay on a text token so contrast is
   theme-safe for every category. */
.schedule-list > .schedule-time-group:first-child > .schedule-time-header { padding-top: 0; }
/* After the all-day rule, the divider's own top margin already supplies the
   gap, so the label drops to space-1 and reads the same distance from the card
   above it as every other period label. */
.schedule-allday-end + .schedule-list > .schedule-time-group:first-child > .schedule-time-header { padding-top: var(--space-1); }

/* Past: the SAME card as every other row, same anatomy, same border, same
   radius, only compact and lighter. Tint and border drop to roughly half
   strength, padding tightens, and the time sits on a secondary token. No
   opacity or grayscale, so the locked category color stays true. */
.schedule-class-item.past {
  padding: 4px var(--space-2) 4px var(--space-3);
  background: color-mix(in srgb, var(--class-color, var(--color-bg-muted)) 5%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--class-color, var(--color-border)) 12%, var(--color-border));
}
.schedule-class-item.past:not(.selected):hover {
  background: color-mix(in srgb, var(--class-color, var(--color-bg-muted)) 10%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--class-color, var(--color-border)) 30%, var(--color-border));
  box-shadow: none;
}
/* Earlier rows read as a table: name, then a fixed-width right-aligned time
   column so times line up down the list regardless of digit count, then the
   count chip with its own separation. */
.schedule-class-item.past .class-item-info { flex-direction: row; align-items: center; gap: var(--space-2); }
.schedule-class-item.past .class-item-name { flex: 1; min-width: 0; font-size: var(--text-sm); font-weight: var(--font-normal); color: var(--color-text-secondary); }
/* An Earlier row shows name, slot time, count. The age group stays in the
   markup (production supplies it) but is suppressed here: the class is over,
   so "who is it for" is no longer a decision, while the time and the count
   still are. Keeping all three would force the NAME to truncate on a 212px
   compact row, which costs more than the age is worth.

   Cancelled/Ending outranks BOTH the age suppression above and the slot time:
   `class-item-cancelled-label` is a SECOND class on the same span as
   `class-item-age` (app.js), so the bare `.class-item-age` selector hid it
   too — a past cancelled or ending class showed 0 attendance with no
   explanation why. And once a class is known cancelled/ending, WHEN it would
   have run stops mattering — the label replaces the time slot the same way it
   already replaces the age-group slot, matching the live `.mini` rows: those
   never carry a per-item time at all (they sit under one shared time-group
   header instead, app.js), so a bare label with no time beside it is the
   established look for a cancelled/ending class, and this row — the only
   density with its own per-item slot-time cell — follows it too. */
.schedule-class-item.past .class-item-age:not(.class-item-cancelled-label),
.schedule-class-item.past .class-item-time,
.schedule-class-item.class-ending .class-item-slot-time,
.schedule-class-item.cancelled-today .class-item-slot-time { display: none; }
/* The slot time is its own cell on EVERY timed row, a sibling of the info
   column, so one right-aligned column runs the length of the list and a long
   age group can never break it. All-day rows (Open Gym, camps, events) have no
   start time and so have no cell.

   The width is rem, not px, because this box IS its text — unlike a badge or a
   row it holds no padding, no icon and no chrome, so "48px" was never a box
   metric, it was a measurement of "11:00 AM" wearing px clothes (ADR-0085 §2.1).
   At the 14px root that rem is exactly 48px, so scale 1 renders byte-for-byte
   what shipped; above it the cell grows with the string it exists to hold.
   Measured: `11:00 AM` is 45.8px at scale 1 — 2.2px of slack — then 49.9px at
   1.1 and 53.9px at 1.2, so a fixed 48 wrapped it to a SECOND LINE from the
   very first step up, pushing the row from 28px to 34px and breaking the
   right-aligned column this cell exists to keep straight.

   `nowrap` states the invariant the comment above already claims: this is one
   line in a table column. Without it the cell answers a squeeze by wrapping,
   which is the failure mode that shipped; with it, it can only ever overflow
   left into the gap, visibly and without moving its neighbours. */
.class-item-slot-time {
  flex-shrink: 0; width: 3.4286rem; text-align: right; white-space: nowrap;
  font-size: var(--text-xs); color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}
/* Count chip: only smaller. A past class still uses the same two states as
   every other card — strong tint when someone checked in, light when nobody did. */
/* Earlier is the quietest tier: the card itself already fades to a 5%/12%
   tint, but the count chip didn't fade with it — a past class with
   check-ins kept its full-strength 40% category-colour fill, which read
   louder than the muted card around it (and, at a glance, as loud as a live
   Up Next card's own tint). Faded — the SAME --class-color at a much lower
   mix, matching the card's own 5%/12% language — not swapped for a neutral
   grey token, which read as flatly desaturated rather than dimmed. */
.schedule-class-item.past .class-item-count {
  height: 18px;
}
.schedule-class-item.past .class-item-count.has-checkins {
  background: color-mix(in srgb, var(--class-color, var(--color-success)) 15%, var(--color-bg-elevated));
  color: var(--color-text-secondary);
}

/* Selected past card: back to full selection weight — same tint, border,
   and bleed as any other selection. Compact sizing stays. */
.schedule-class-item.past.selected {
  background: color-mix(in srgb, var(--class-color, var(--color-accent)) 20%, var(--color-bg-elevated));
  border-color: var(--class-color, var(--color-accent));
  box-shadow: inset 0 0 0 1px var(--class-color, var(--color-accent));
}
.schedule-class-item.past.selected .class-item-name { font-weight: var(--font-semibold); color: var(--color-text); }


/* Next up is a TIME SLOT, not a single class: today three classes start at
   6:00, so no one card is "the" next one. The emphasis therefore applies to
   EVERY card in the slot, identically: a stronger tint and border at the same
   size, so nothing changes geometry and no card is singled out. */
.schedule-class-item.next-slot {
  background: color-mix(in srgb, var(--class-color, var(--color-accent)) 16%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--class-color, var(--color-accent)) 55%, var(--color-border));
}
/* Size (full vs mini) is what tells Up Next apart from Later, but the tint
   and border above are the SAME color-mix percentages `.mini.is-live` (Later)
   uses — two tiers reading identical except for card size wasn't distinct
   enough at a glance. Bold is the third element: it costs no geometry (unlike
   size) and reads at any density, so it survives if a future pass ever makes
   the two tiers the same size. */
.schedule-class-item.next-slot .class-item-name {
  font-weight: var(--font-bold);
}
/* The slot label is the other half of the cue: larger than the period labels
   around it, on the full text token, so Next up is discriminable from Earlier
   and Later without reading the words. */
.schedule-time-label.now {
  /* 11px against the 9.5px period labels: --text-xs resolves to 9.625px, which
     is not a perceptible step. Letter-spacing tightens slightly as size grows. */
  font-size: 0.7857rem; letter-spacing: .1em;
  color: var(--color-text);
  padding-bottom: var(--space-1);
}
.schedule-sidebar {
  background: var(--color-bg-elevated);
  position: relative;
  padding: 12px 0 12px 12px;
  border-right: 1px solid var(--color-border);
  /* Match .main-content-body's own bottom gutter (`margin: 0 16px 16px 0`,
     style.css) — the design's `.page` rule carries the same 16px (app-shell.html
     `.page{margin:0 16px 16px 0}`). Without it the sidebar's box runs the full
     grid-row height while its floating-card sibling stops 16px short, so the
     sidebar's last item (e.g. "Add group event") visibly hangs lower than the
     card's own bottom edge — measured 4-16px off live. Both panels now share
     one visible floor. */
  margin-bottom: 16px;
}
/* margin-left here is NOT visual inset (.schedule-scroll's own 12px padding
   already provides that) — it's clearance for the focus ring's box-shadow.
   .schedule-list scrolls vertically (overflow-y:auto), which per spec forces
   its own overflow-x to auto too, so it clips anything bleeding past ITS OWN
   box — a tighter boundary than .schedule-scroll's padding, since .schedule-
   list carries none of its own. The right side already had this clearance
   as a side effect of margin-right's inset; the left side had none, so the
   ring rendered on 3 sides and clipped on the 4th. Width shrinks by the same
   amount so the card's right edge doesn't move. */
.schedule-sidebar .schedule-class-item { width: calc(100% - var(--space-4) - 4px); margin-left: 4px; margin-right: var(--space-3); }

/* Extended-bleed selection (exploration 1b): the selected card keeps all four
   corners and its full border, widens 16px past the sidebar seam and floats
   over it with a colored shadow. The seam stays a neutral hairline behind it;
   the selection is the only element that crosses it. */
.schedule-sidebar.bleed .schedule-class-item.selected {
  width: calc(100% + 16px); margin-right: 0;
  border: 1px solid var(--class-color, var(--color-accent));
  border-radius: var(--radius-md);
  z-index: 10;
}
.app-schedule { display: flex; flex-direction: column; padding: 0; }
/* DEPRECATED — superseded by .schedule-day-block. Kept only for the pages not
   yet migrated (check-in screen, mobile). Do not use in new work.
   Matches the old live sidebar: 48px tall (--header-height), 4px side padding,
   one hairline under it. */
.schedule-date-nav { display: flex; align-items: center; gap: 4px; height: 48px; padding: 0 var(--space-1); border-bottom: 1px solid var(--color-border); }
.app-schedule .schedule-date-nav { margin-right: 1px; }
/* The trigger is a PILL, not a text label: the live control has a surface, a
   hairline and a hover fill, because tapping it does something. */
.schedule-date-display {
  flex: 1; text-align: center;
  font-size: var(--text-sm); font-weight: var(--font-medium);
  color: var(--color-text);
  padding: 6px 12px; border-radius: 6px;
  border: 1px solid var(--color-border);
  background: var(--color-bg-subtle);
  cursor: pointer; font-family: inherit;
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.schedule-date-display:hover { background: var(--color-accent); color: #fff; border-color: var(--color-accent); }
/* Off-today the tap is a one-tap return to today, NOT the picker, so the pill
   has to read as an action rather than a caption: accent text and accent edge,
   surface unchanged. The desk must never mistake another day for the live one. */
.schedule-date-display.is-offday { color: var(--color-accent); border-color: var(--color-accent); font-weight: var(--font-semibold); }
.schedule-date-display.is-offday:hover { color: #fff; }

/* DEPRECATED — the mode flag is now a 22px glyph in .schedule-day-caption, not
   a full-width band. Kept for unmigrated pages.
   Mode tag. Sits directly under the date nav and is collapsed to zero height
   unless the VIEWED date has event slots — an event-less day has nothing for it
   to switch, so it takes no room at all rather than sitting there inert. */
.schedule-mode-active-tag {
  display: block; width: 100%; text-align: center;
  font-size: var(--text-xs); font-weight: var(--font-semibold);
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--color-text-secondary);
  background: var(--color-bg-subtle);
  border: 0; border-bottom: 0 solid var(--color-border);
  font-family: inherit; cursor: pointer;
  max-height: 0; opacity: 0; padding: 0; overflow: hidden;
  transition: max-height .3s ease, opacity .25s ease, padding .3s ease, border-bottom-width .3s ease, background .12s ease, color .12s ease;
}
.schedule-mode-active-tag.visible { max-height: 40px; opacity: 1; padding: 6px 0; border-bottom-width: 1px; }
.schedule-mode-active-tag:hover { background: var(--color-bg-muted); color: var(--color-accent); }
.schedule-mode-active-tag.unified { color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 10%, var(--color-bg-subtle)); }

/* Date picker popover. Opens from the pill on today only; 248px, positioned
   under the trigger and flipped above it when there is less than 280px below.
   .static is the documentation variant: same box, no fixed positioning. */
.schedule-date-picker-popover {
  width: 248px; padding: var(--space-3);
}
.date-picker-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-2); }
.date-picker-title { flex: 1; text-align: center; font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); }
.date-picker-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); margin-bottom: 4px; }
.date-picker-weekdays span { text-align: center; font-size: var(--text-xs); color: var(--color-text-muted); padding: 2px 0; }
.date-picker-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.date-picker-cell {
  appearance: none; background: none; border: 0; font-family: inherit;
  border-radius: 6px; padding: 0; width: 100%; aspect-ratio: 1;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--text-sm); color: var(--color-text); cursor: pointer;
  transition: background .1s ease, color .1s ease;
}
.date-picker-cell:hover { background: var(--color-bg-subtle); }
.date-picker-cell.empty { cursor: default; pointer-events: none; }
/* Today is a ring, the viewed date is a fill — different facts, different
   marks. On today they coincide, and the fill wins: .is-selected clears the
   ring, because a filled cell already says "here" and stacking a ring inside
   it would read as a third state. */
.date-picker-cell.is-today { font-weight: var(--font-semibold); box-shadow: inset 0 0 0 1.5px var(--color-accent); color: var(--color-accent); }
.date-picker-cell.is-selected { background: var(--color-accent); color: #fff; font-weight: var(--font-semibold); box-shadow: none; }
.date-picker-footer { display: flex; justify-content: flex-end; margin-top: var(--space-2); }
@media (max-width: 640px) { .spec-block, .spec-block.md { grid-template-columns: 1fr; } }
/* The sidebar owns its own padding once a date nav sits above the list, so
   the nav's bottom rule can run the full width of the column. */
.schedule-sidebar.has-date-nav { padding: 0; }
.schedule-sidebar.has-date-nav .schedule-scroll { padding: 12px 0 12px 12px; }
.date-nav-btn { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border: 0; background: transparent; border-radius: var(--radius-sm); color: var(--color-text-muted); font-size: 1.1429rem; cursor: pointer; transition: background .15s; }
.date-nav-btn:hover { background: var(--color-bg-subtle); color: var(--color-text); }
.app-schedule .schedule-scroll { padding: 12px 0 12px 12px; }
.app-main { display: flex; flex-direction: column; min-width: 0; background: var(--color-bg); }
.app-main-search { padding: 9px 16px 9px 26px; background: var(--color-bg-elevated); border-bottom: 1px solid var(--color-border); }
.app-main-body { padding: 16px 16px 16px 26px; }

/* ================================================================
   STATUS ICONS (search-results.html, universal-search-results.html)
   ================================================================ */
.account-status { display: inline-flex; align-items: center; gap: 4px; margin-left: 6px; vertical-align: -1px; }
.status-icon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; font-size: 0.9286rem; line-height: 1; }
.status-icon i { font-size: inherit; }
/* No `.status-waiver*` here: the waiver icon is an EXCEPTION signal (ADR-0070),
   so the four-state vocabulary is one state — `.status-waiver-blocked`, in
   70-camper-profile-notes.css with the rest of the exception classes. */
.status-group          { color: var(--color-info); }
.status-notes          { color: var(--color-text-muted); }
.status-notes-important{ color: var(--color-warning); }
.status-balance-owes   { color: var(--color-danger); }
.status-balance-credit { color: var(--color-success); }

/* The LIVE dot — the app's one "right now" mark, and the base every scope that
   wears it overrides from (ui-pattern-audit D7). Four places declared these
   five properties in full: the schedule day caption, the next-up time header,
   the phone's date + sheet headers, and the History tab's Live toggle. The
   first three were byte-identical, which is the whole argument for a base;
   only the toggle genuinely differs, and it now says ONLY how (a size up, a
   muted resting colour, a fade) rather than restating the dot. */
.live-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--color-success); flex: none; }

/* ================================================================
   SEARCH RESULT ROWS (search-results.html)
   ================================================================ */
.search-result-list { display: flex; flex-direction: column; background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; }
.search-result-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--color-border); transition: background .12s; cursor: pointer; }
.search-result-item:hover { background: var(--hover-row); }
.search-result-name { font-size: var(--text-md); font-weight: var(--font-normal); color: var(--color-text); letter-spacing: -0.005em; display: inline-flex; align-items: center; }
.search-result-badges { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
/* The subgrid (`.search-dropin-group`, below) gives every row's second column the SAME width --
   whichever row has the widest badge/action content sets it for the whole
   group. .search-result-badges already hugs that column's right edge
   (justify-content:flex-end); this one didn't, so a Drop-in/+Add row (narrower
   than a badge row) sat flush-left in the shared column instead of lining up
   with every badge row's right edge. */
.search-result-actions { display: flex; align-items: center; gap: 6px; justify-content: flex-end; }
.search-result-footer { display: flex; align-items: center; gap: 8px; padding: 10px 14px; font-size: var(--text-xs); color: var(--color-text-muted); background: var(--color-bg-subtle); border-top: 1px solid var(--color-border); }
.search-result-footer i { color: var(--color-info); }

/* Search bar. `.search-input-wrapper` was declared here — a third copy of
   `.attendance-search`'s anatomy, with ZERO emitters in the app (ADR-0128).
   It is gone; the field is `.attendance-search`. */
.search-controls { display: flex; gap: 4px; }
/* A real sub-header, not a floating caption: same register as the table's own
   thead (bg-subtle band, uppercase small label) so the break reads as a genuine
   section boundary instead of a stray line of muted text. */
.archived-results-divider td { padding: 0; border-bottom: 1px solid var(--color-border); }
.archived-results-header { display: flex; align-items: center; gap: 6px; font-size: 0.7143rem; font-weight: var(--font-semibold); letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); background: var(--color-bg-subtle); padding: 10px var(--space-4); }
/* Same row as a live member (H: visual parity, only the lifecycle differs) —
   dimmed as a whole, quick-action icon swapped to Restore (which restores AND
   completes the check-in, same as the live row's Check-in icon does one step). */
.member-table tr.is-archived td { opacity: .6; }
.member-name { font-weight: var(--font-normal); font-size: var(--text-md); color: var(--color-text); letter-spacing: -0.005em; }
/* Last, First — confirmed against a production screenshot: the surname is the
   LIGHT part, the given name is what actually reads bold. Backwards from the
   two earlier guesses here — grounded in a real render this time, not a hunch. */
.name-first { font-weight: var(--font-semibold); }
/* The (age) suffix, the other half of formatNameWithAge's one label (ADR-0126).
   No margin: the owner emits a real space before this span and `.name-wrap`
   keeps that space in normal inline flow, so a margin here would double it. */
.age-suffix { font-weight: var(--font-normal); color: var(--color-text-muted); }
.member-sub { color: var(--color-text-muted); font-size: var(--text-sm); margin-top: 2px; }
/* NOT applied: production puts .member-cell-name on the Name <td> of
   .member-table, while the design's flex row is .member-name-wrapper (styled
   correctly below). display:flex on a <td> drops it out of the table box model
   -- anonymous cell, no vertical-align, no border-collapse -- which is the
   .member-cell collision from 2026-08-21 arriving again under a new name.
   CROSSWALK.md:62 asks us to confirm this; confirmed, it needs scoping. */
/* Notes badge (member table notes column) — lifted from live style.css ~17902 */
.notes-badge { display: inline-flex; align-items: center; height: 22px; gap: var(--space-1); padding: 0 var(--space-2); background: var(--color-bg-subtle); border-radius: var(--radius-sm); font-size: var(--text-xs); color: var(--color-text-muted); cursor: pointer; }
.notes-badge i { font-size: var(--text-sm); }

/* ================================================================
   MEMBER ROW WRAPPER — buttons LEFT, name RIGHT
   ================================================================ */
.member-name-wrapper {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}
.member-info { flex: 1; min-width: 0; }
.member-contact {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  padding-left: 6px;
}

/* ================================================================
   CHECK-IN BUTTON — LOCKED (merged, 2026-08)
   ONE 28×28 icon button at the LEFT of every row (bi-calendar-plus).
   Click opens the check-in popover (desktop) / sheet (mobile):
   waiver-check row on top, open classes below. Two clicks for every
   check-in — no pre-deciding between two buttons.
   State = HOW they checked in, in the TINT language (ADR-0093):
     (idle)          = NO tint, OUTLINE plus glyph — nothing has happened
     .checked-in     = a 14% tint of the destination's category color, that
                       color as a FILLED glyph, a 30% border
     .checked-waiver = the same construction in the waiver grey
                       (--color-text-muted, the status vocabulary's own)
   Hover on either checked state turns to the danger tint (undo).
   .member-icon-single is NOT deprecated (this header long claimed it was):
   it is the desktop camp surface's row action button — check in, check out,
   view — and follows the same standard.
   ================================================================ */

/* Shared shape */
.member-icon-single,
.member-schedule-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px;
  background: var(--color-bg-elevated);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  box-shadow: 0 1px 3px rgba(0,0,0,.12), 0 0 0 1px rgba(0,0,0,.06);
  cursor: pointer; font-size: 1rem;
  vertical-align: middle; flex-shrink: 0;
  transition: all .15s ease;
}
/* The phone's size (ADR-0248): the check-in button and its siblings (check out) are one family at one size inside the
   phone's pages, the 38px that is the tallest anything pressable looks there (`--ph-btn`, mobile.css). The 44px a finger
   needs is the row's tap target, not padding on the button. */
html[data-mode="mobile"] .ph-screen :is(.member-icon-single, .member-schedule-btn) {
  width: var(--ph-btn); height: var(--ph-btn); font-size: 1.1429rem;
}
/* NO PER-GLYPH NUDGE — three passes tried to hand-correct calendar-plus /
   calendar-check with a manual `top` offset and each fought the flex-
   centring rule instead of trusting it. Full account in style.css, next to
   that rule. The flex box already centres the glyph correctly. */

/* THE ROW ACTION BUTTON — the same standard as the check-in button.

   Not deprecated, whatever the header above this block used to say: this is
   the DESKTOP camp surface's action button (camp-checkin-rows.js,
   camp-checkin-attendance.js) — check in, check out, view.

   It followed the pre-tint language: a solid fill on hover and on
   `.checked-in`, plus a colour-coded 1px border in dark that existed to
   replace the shadow. Under the standard (ADR-0093) the mapping is the one
   the check-in button already states:

     an ACTION (check in / check out / view)  no tint, neutral border, the
                                              glyph carries which action
     an ACTIVE STATE (.checked-in)            tint + the colour as the glyph
     undo (hover on a state)                  the danger tint

   The glyph keeps its semantic colour — that is what says WHICH action —
   while the border stays neutral, so a row of three actions reads as three
   buttons rather than three different-coloured boxes. */
.member-icon-single {
  color: var(--color-success);
  background: transparent;
  border-color: var(--color-border);
  box-shadow: none;
}
.member-icon-single:hover:not(.checked-in) {
  background: color-mix(in srgb, var(--color-success) 14%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--color-success) 30%, var(--color-border));
  color: color-mix(in srgb, var(--color-success) 75%, var(--color-text));
}
.member-icon-single.checked-in {
  background: color-mix(in srgb, var(--color-success) 14%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--color-success) 30%, var(--color-border));
  color: color-mix(in srgb, var(--color-success) 75%, var(--color-text));
}
.member-icon-single.checked-in:hover {
  background: color-mix(in srgb, var(--color-danger) 16%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-border));
  color: var(--color-danger);
}

/* THE TINT STANDARD (ADR-0093).

   The button was a SOLID block of the destination's color with a white
   OUTLINE glyph. Measured, that construction failed its own job: white on
   Open Gym green is 2.78 and on Classes blue 3.12, against WCAG's 3.0 floor
   for non-text — the two most-used destinations were the two least legible
   buttons in the app. It also predated the badge work, so the app had two
   ways of saying "this thing is that category": the badge's tint + colored
   label, and this solid block.

   Now there is one. A checked-in button is a 14% TINT of its category color
   carrying that color as a FILLED glyph, bordered at 30% — the same
   construction `.pass-badge .badge-label` uses. The glyph color
   is the badge's own formula, `color-mix(cat 75%, --color-text)`, NOT the raw
   category color: it is what keeps a dark category (camp brown) legible on
   its own pale tint in light mode and a light one legible in dark. Measured
   after the change: Classes 3.12 -> 6.7, Open Gym 2.78 -> 7.5.

   The tint carries almost no LUMINANCE contrast against the row it sits on
   (1.01) — it registers by hue. So the border is not decoration: it is the
   control's edge, and without it the button has none for anyone who cannot
   separate those hues, or in sunlight. The shadow goes with the solid fill it
   belonged to.

   Waiver-only stays NEUTRAL through all of this. The tint answers the weight
   objection that made a solid grey necessary, but not the semantic one: a
   waiver check is a STATUS, not a destination, so it must not wear a
   destination's hue — and success green IS Open Gym's green. It takes
   `--color-text-muted`, the neutral the row's own quiet marks already wear.

   THE RULE, in one line: tint + filled glyph = ACTIVE; no tint + outline
   glyph = INACTIVE. Both halves move together, so a row states whether the
   check-in happened before anything is read — weight and colour are spent on
   that one distinction and on nothing else. Idle therefore has no fill at
   all, only its border and the outline plus glyph. */
.member-schedule-btn {
  color: var(--color-text-secondary);
  /* NO tint and NO fill: that pairing IS the inactive state. A tint here
     would claim a destination the person has not been checked into. */
  background: transparent;
  border-color: var(--color-border);
  box-shadow: none;
}
.member-schedule-btn:hover:not(.checked-in):not(.checked-waiver) {
  background: var(--color-bg-muted);
  color: var(--color-text);
}
.member-schedule-btn.checked-in {
  background: color-mix(in srgb, var(--class-color, var(--color-classes)) 14%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--class-color, var(--color-classes)) 30%, var(--color-border));
  color: color-mix(in srgb, var(--class-color, var(--color-classes)) 75%, var(--color-text));
}
/* Waiver-only takes the NEUTRAL grey: `--color-text-muted`, the token every
   quiet mark on the row wears. Not success green — that is Open Gym's hue, and
   painting a STATUS in a destination's colour is the ambiguity this button's
   rule existed to prevent. The tint removed the WEIGHT objection to a hue,
   not the semantic one: a waiver check is still not a destination. */
.member-schedule-btn.checked-waiver {
  background: color-mix(in srgb, var(--color-text-muted) 14%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--color-text-muted) 30%, var(--color-border));
  color: color-mix(in srgb, var(--color-text-muted) 75%, var(--color-text));
}
/* Undo, in the same language: the danger tint, a touch stronger so the
   change of intent is unmistakable without returning to a solid block. */
.member-schedule-btn.checked-in:hover,
.member-schedule-btn.checked-waiver:hover {
  background: color-mix(in srgb, var(--color-danger) 16%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-border));
  color: var(--color-danger);
}

/* member-icon-single variants + overflow menu + clickable name + balance
   badge — copied verbatim, at the time, from the then-single `style.css`
   (~6932, ~7708, ~14418, ~20413-20527). That file no longer exists: ADR-0095
   split it into the ten numbered files. Shared across member rows and camp
   check-in rows. */
/* View is NOT in this family. The boxed icon button is the check-in design
   language, and that language means "this control changes the record".
   Opening a record changes nothing, so View rides the members table's
   [View][...] band instead — see ADR-0093. */
.member-icon-single.icon-checkout { color: var(--color-warning); }
/* NO PER-GLYPH NUDGE — same mistake as the calendar-plus/check nudge this
   button family used to carry (the icon-centring family in
   80-groups-notifications-orders.css records all three attempts at it):
   hand-tuning one icon against
   the flex-centring rule instead of trusting it, which is exactly the kind
   of button-to-button misalignment across pages that a per-glyph offset
   causes. Removed. */
.member-icon-single.icon-checkout:hover {
  background: color-mix(in srgb, var(--color-warning) 14%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--color-warning) 30%, var(--color-border));
  color: color-mix(in srgb, var(--color-warning) 75%, var(--color-text));
}

.clickable-name { cursor: pointer; padding: 2px 6px; border-radius: var(--radius-sm); transition: background-color .15s ease, opacity .15s ease; }
.clickable-name:hover { background-color: color-mix(in srgb, var(--color-accent) 12%, transparent); }
.clickable-name:active { opacity: .7; }

.balance-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: var(--text-xs); font-weight: 600; white-space: nowrap; }
.balance-badge.owed { background: color-mix(in srgb, var(--color-warning) 15%, transparent); color: var(--color-warning); }
.balance-badge.credit { background: color-mix(in srgb, var(--color-success) 15%, transparent); color: var(--color-success); }
/* The phone collapse belongs to the same owner as the base. It used to live in
   30-modals' <=600px block, which worked only while .form-row was declared in
   an EARLIER file: a media query carries no extra weight, so once v3.css owned
   the base it beat the override at equal specificity and forms stayed two-up on
   a phone. `#schedule-modal .form-row` (1,1,0) still out-ranks this and is
   unaffected. */
@media (max-width: 600px) { .form-row { grid-template-columns: 1fr; } }
/* On a phone the grid is one column and every span collapses. The explicit
   span classes are (0,2,0) and must be named here: a bare `.form-grid > *`
   (0,1,0) loses to them, which left a .span-3 field sitting at half width on
   a 375px screen. */
@media (max-width: 600px) {
  .form-grid { grid-template-columns: 1fr; }
  .form-grid > *,
  .form-grid > .span-3,
  .form-grid > .span-4,
  .form-grid > .span-6 { grid-column: 1 / -1; }
}
/* A picked account in a form (camp-account-search.js `pickedAccountHtml`): the name, then Change. */
.picked-account { display: flex; align-items: center; gap: var(--space-3); }

/* ================================================================
   EDIT ACCOUNT — the A2 header's identity slot and the sub-tab band.
   Both moved here from 10-forms with the V3 restructure (ADR-0099);
   .account-layout, .account-header-bar, .account-header-info,
   .account-name, .account-header-actions and .account-actions were
   deleted outright, not restyled — the second header they composed is
   gone and the account's name is now the page title.
   ================================================================ */
/* The identity rides in .class-header-name-row beside #account-form-title;
   updateAccountFormMode shows exactly one of them. */
.account-identity { display: inline-flex; align-items: baseline; gap: var(--space-2); min-width: 0; }

/* The account header is the search row grown into a page (ADR-0216): the row's own check-in button in front of the
   name, and a contact line (last visit, phone, guardian, email) that gives way first when the header is tight. */
#edit-account-header .class-header-name-row { min-width: 0; flex: 1 1 auto; }
#edit-account-header .member-schedule-btn { flex: none; margin-right: var(--space-2); }
#edit-account-header .class-header-meta { overflow: hidden; text-overflow: ellipsis; min-width: 0; margin-left: var(--space-3); }
/* The name copies, exactly as it does in the list row: no underline, a copy cursor. */
#edit-account-name[data-action="copyName"] { cursor: copy; }
/* The row grows into the header and back (foundation/view-morph.js): the same three names on both sides. */
#edit-account-header { view-transition-name: acct-card; }
#edit-account-header .member-schedule-btn { view-transition-name: acct-lead; }
#edit-account-header .class-header-name { view-transition-name: acct-name; }
::view-transition-group(acct-card), ::view-transition-group(acct-lead), ::view-transition-group(acct-name) { animation-duration: .28s; animation-timing-function: ease; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}

/* Send the waiver: the QR window (core/waiver-share.js). */
.waiver-share { text-align: center; }
.waiver-share-qr { display: block; margin: 0 auto var(--space-3); }

/* The form-status strip: silent, staged, or blocked. Tints only — a
   single-side accent bar is the pattern this design system avoids. */
.form-status { display: flex; align-items: flex-start; gap: var(--space-2); padding: 8px var(--space-4); border-bottom: 1px solid var(--color-border); font-size: var(--text-md); line-height: 1.5; }
.form-status[hidden] { display: none; }
/* Its own class, not `> .bi`: a bare icon selector co-matches every other
   .bi rule in the app and buys nothing. */
.form-status-icon { flex: none; margin-top: 1px; font-size: 0.9286rem; }
.form-status.staged { background: var(--color-accent-tint); color: color-mix(in srgb, var(--color-accent) 82%, var(--color-text)); }
.form-status.blocked { background: var(--color-danger-tint); color: var(--color-danger); border-bottom-color: color-mix(in srgb, var(--color-danger) 28%, var(--color-border)); }
.form-status-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.form-status-title { font-weight: var(--font-semibold); }

/* A save in flight. Nothing on the form moves until the server answers, so a
   second click, a row action or a sub-tab switch cannot race the write (audit
   M10). pointer-events rather than `inert`, and no :is(): the support floor is Safari 13.1. */
#tab-edit[data-saving] .page-body,
#tab-edit[data-saving] .account-subnav,
#tab-edit[data-saving] .class-header-actions [data-action="closeEditTab"],
#tab-edit[data-saving] .class-header-overflow-trigger { pointer-events: none; }
#tab-edit[data-saving] .page-body { opacity: 0.65; transition: opacity 0.15s; }
@media (prefers-reduced-motion: reduce) { #tab-edit[data-saving] .page-body { transition: none; } }

/* The held-snapshot line. Persistent, because it stays true until someone
   acts on it — a toast for that is the wrong tier. */
.form-status-held { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; font-size: var(--text-sm); color: var(--color-text-secondary); }
/* Each problem names its tab and goes there. The old flow focused a field on a
   sub-tab the user could not see, which read as "Save is broken". */
.form-problem { display: flex; align-items: center; gap: var(--space-2); width: 100%; padding: 3px var(--space-2); margin-left: calc(var(--space-2) * -1); border: 0; border-radius: var(--radius-sm); background: none; text-align: left; font-size: var(--text-md); color: var(--color-text); }
.form-problem:hover { background: color-mix(in srgb, var(--color-danger) 9%, transparent); }
.form-problem .where { flex: none; height: 18px; padding: 0 6px; display: inline-flex; align-items: center; border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: var(--font-semibold); color: var(--color-danger); background: color-mix(in srgb, var(--color-danger) 14%, transparent); }
.form-problem .go { margin-left: auto; flex: none; color: var(--color-text-muted); font-size: 0.7857rem; }

/* The waiver status: one row, the fact first, the rare destructive actions
   behind the overflow. It used to be a flex div of inline styles with
   Invalidate and Unlink shouting in hardcoded #dc3545 beside the fact. */
.waiver-status { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; min-height: 32px; padding: 5px var(--space-3); border-radius: var(--radius-md); font-size: var(--text-md); }
.waiver-status.good { background: var(--color-success-tint); color: color-mix(in srgb, var(--color-success) 78%, var(--color-text)); }
.waiver-status.warn { background: var(--color-warning-tint); color: color-mix(in srgb, var(--color-warning-ink) 88%, var(--color-text)); }
.waiver-status.dang { background: var(--color-danger-tint); color: var(--color-danger); }
.waiver-status-label { font-weight: var(--font-semibold); }
.waiver-status-detail { color: inherit; opacity: .85; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.waiver-status-actions { display: flex; align-items: center; gap: var(--space-2); margin-left: auto; }
.waiver-status-more { position: relative; }
/* The archived lists sat inside a .card-body that owned their inset; the
   fieldset does not, so the body keeps its own. */
.fieldset-body { min-width: 0; }

/* The staged-change readout is a chip too, so "unsaved" reads as a state
   rather than as an icon and a sentence. */
.form-status .state-chip { margin-right: var(--space-1); }

/* Notes: the text leads, the flags and meta sit under it, the actions stay
   quiet on the right. The old .note-card opened with a badge strip and a date
   line, so you read the chrome before the note. */
.notes-list { border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; background: var(--color-bg-elevated); }
.note-row { display: grid; grid-template-columns: minmax(0,1fr) max-content; gap: var(--space-3); align-items: start; padding: 10px var(--space-3); border-bottom: 1px solid var(--color-border-subtle); }
.note-row:last-child { border-bottom: 0; }
.note-row:hover { background: var(--hover-row); }
/* State by TINT, not by a side stripe. */
.note-row.is-important { background: color-mix(in srgb, var(--color-warning) 7%, var(--color-bg-elevated)); }
.note-row.is-important:hover { background: color-mix(in srgb, var(--color-warning) 11%, var(--color-bg-elevated)); }
.note-row.is-mgmt { background: color-mix(in srgb, var(--color-accent) 6%, var(--color-bg-elevated)); }
.note-row.is-archived { opacity: .65; }
.note-row-main { min-width: 0; }
.note-row-text { font-size: var(--text-md); line-height: 1.5; color: var(--color-text); overflow-wrap: anywhere; }
/* Notes carry rich content from the editor — lists, bold, links. The old
   .note-card styled these; .note-row did not, so an <ol> hung its numbers
   outside the row and overflowed. Padding-inline-start puts the markers
   INSIDE the row's box; `position: inside` would collapse the hanging
   indent on wrapped lines. */
.note-row-text ol,
.note-row-text ul { margin: 4px 0 0; padding-inline-start: 1.35em; }
.note-row-text li { margin: 1px 0; }
.note-row-text li::marker { color: var(--color-text-muted); }
.note-row-text p { margin: 0 0 4px; }
.note-row-text p:last-child { margin-bottom: 0; }
.note-row-text a { color: var(--color-accent); }
.note-row-text strong, .note-row-text b { font-weight: var(--font-semibold); }
/* The editor emits an empty <span> per list item; it must not add height. */
.note-row-text li > span:empty { display: none; }
.note-row-meta { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-top: 3px; font-size: var(--text-xs); color: var(--color-text-muted); }
.note-flag { display: inline-flex; align-items: center; gap: 4px; height: 17px; padding: 0 6px; border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: var(--font-semibold); white-space: nowrap; }
.note-flag .bi { font-size: .85em; }
/* --color-warning-ink, not the raw #92400e this carried: --color-warning
   (#f59e0b) is the fill hue and is far too light to read as ink on its own
   22% tint. The darker companion is the ink, and it is a token so the two
   surfaces that need it cannot drift apart again. */
.note-flag.imp { color: var(--color-warning-ink); background: color-mix(in srgb, var(--color-warning) 22%, transparent); }
.note-flag.mgm { color: var(--color-accent); background: var(--color-accent-tint); }
.note-flag.old { color: var(--color-text-muted); background: var(--color-bg-muted); }

/* Balance: a ledger, not a stack of cards. Money is right-aligned on tabular
   figures so a column of amounts actually lines up, and the sign plus the
   colour carry what a "Charge"/"Credit" label used to repeat. */
#unpaid-balance-list, #paid-balance-list, #archived-balance-list { border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; background: var(--color-bg-elevated); }
.balance-row { display: grid; grid-template-columns: 92px minmax(0,1fr) max-content max-content max-content; align-items: center; gap: var(--space-3); padding: 9px var(--space-3); border-bottom: 1px solid var(--color-border-subtle); }
.balance-row:last-child { border-bottom: 0; }
.balance-row:hover { background: var(--hover-row); }
.balance-row.is-archived { opacity: .6; }
.balance-row .when { font-size: var(--text-sm); color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.balance-row .amt { font-size: var(--text-md); font-weight: var(--font-semibold); font-variant-numeric: tabular-nums; text-align: right; min-width: 78px; }
.balance-row .amt.charge { color: var(--color-danger); }
.balance-row .amt.credit { color: var(--color-success); }
.balance-row .amt.settled { color: var(--color-text-muted); }
/* WHERE A RECORD CAME FROM. Unscoped since ADR-0146: it was `.balance-row .src`
   until the membership and pass-bundle rows needed the same label, and scoping
   it to one row family is how a component ends up copy-pasted four times. One
   builder writes it — foundation/record-provenance.js. */
.src { display: inline-flex; align-items: center; height: 17px; padding: 0 6px; border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: var(--font-semibold); white-space: nowrap; color: var(--color-text-secondary); background: var(--color-bg-muted); }
.src.camp { color: #fff; background: var(--color-camp); }
/* The receipt button sits in the row's ACTION SLOT (ADR-0130), so it inherits
   the ordinary row-button shape and needs no component of its own — only the
   icon's optical size. Putting it on the chip instead made the row's own
   expand/collapse unreliable, which the e2e caught. */
.record-receipt-btn .bi { font-size: 1.05em; }
/* Scoped, because `.stack` / `.trunc` / `.muted` are artboard vocabulary and
   do not exist in the app — using them stacked nothing and ran the staff name
   into the description. */
.balance-row-what { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.balance-row-what .what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.balance-row-what .who { font-size: var(--text-xs); color: var(--color-text-muted); }

/* The household on one screen: who, what they hold, and what is wrong —
   instead of a name, a birthday and Remove. v3 owns the row now; the older
   flex rule in 80-groups is superseded. */
.group-members-list { border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; background: var(--color-bg-elevated); }
.group-member-row { display: grid; grid-template-columns: minmax(0,1fr) max-content max-content; align-items: center; gap: var(--space-3); padding: 8px var(--space-3); min-height: 44px; border-bottom: 1px solid var(--color-border-subtle); }
.group-member-row:last-child { border-bottom: 0; }
.group-member-row:hover { background: var(--hover-row); }
.group-member-row.is-archived { opacity: .6; }
.group-member-row.pending-member { background: var(--color-accent-tint); }
.gm-who { display: flex; align-items: baseline; gap: var(--space-2); min-width: 0; flex-wrap: wrap; }
.gm-who .member-name { font-size: var(--text-md); font-weight: var(--font-medium); }
.gm-who .member-birthday { font-size: var(--text-sm); }
.gm-holds { display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; justify-content: flex-end; }

/* Camp registrations on an account: a row, not a two-line card. */
.events-list { border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; background: var(--color-bg-elevated); }
.camp-reg-row { display: grid; grid-template-columns: minmax(0,1fr) max-content max-content; align-items: center; gap: var(--space-3); padding: 9px var(--space-3); min-height: 44px; border-bottom: 1px solid var(--color-border-subtle); }
.camp-reg-row:last-child { border-bottom: 0; }
.camp-reg-row:hover { background: var(--hover-row); }
.camp-reg-what { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.camp-reg-what .what { font-size: var(--text-md); font-weight: var(--font-medium); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.camp-reg-what .who { font-size: var(--text-xs); color: var(--color-text-muted); }

/* Membership and pass-bundle rows. ONE bordered list with divided rows —
   the same shape as notes, balance, members and camps. They were individually
   bordered, rounded, shadowed cards floating in a gap-3 stack, which is the
   old card design surviving inside the new fieldset: a shadowed card inside a
   bordered list inside a section. State is a TINT, never a coloured border. */
.membership-form-list { display: flex; flex-direction: column; border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; background: var(--color-bg-elevated); }
.membership-card { border-bottom: 1px solid var(--color-border-subtle); background: var(--color-bg-elevated); }
.membership-card:last-child { border-bottom: 0; }
.membership-card.paused { background: color-mix(in srgb, var(--color-paused) 6%, var(--color-bg-elevated)); }
.membership-card.expired,
.membership-card.closed { background: var(--color-bg-subtle); }
.membership-card.archived { background: var(--color-bg-subtle); opacity: .7; }
.membership-card.new-membership { background: var(--color-accent-tint); }
/* "Waiting" (bought, not started) is a TINT like every other row state. It
   used to be a dashed border, which survived the card-to-row conversion as a
   dashed outline across a row in a bordered list — the state chip already
   says "Not started". */
.membership-card.waiting { background: var(--color-bg-subtle); }
.membership-header { display: flex; align-items: center; gap: var(--space-3); padding: 8px var(--space-3); min-height: 44px; cursor: pointer; }
.membership-header:hover { background: var(--hover-row); }
.membership-card.expanded .membership-header { background: var(--hover-row); }
/* The row's action bar mixes commit models — Pause writes now, Close waits
   for Save. The labels say so; this states the rule once, where the buttons
   are, so nobody has to infer it from a toast. */
.row-commit-hint { display: inline-flex; align-items: center; gap: 5px; margin-left: var(--space-2); font-size: var(--text-xs); color: var(--color-text-muted); }
.row-commit-hint .bi { font-size: .95em; }

/* The account-level waiver alert. Loud, because a missing waiver stops the
   person entering the building — and silent when covered, per ADR-0070: an
   alert on every account spends the attention the real ones depend on. */
.account-waiver-alert { display: flex; align-items: center; gap: var(--space-2); padding: 9px var(--space-4); background: color-mix(in srgb, var(--color-danger) 12%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--color-danger) 35%, var(--color-border)); font-size: var(--text-md); line-height: 1.45; }
.account-waiver-alert.hidden { display: none; }
.account-waiver-alert-icon { color: var(--color-danger); flex: none; font-size: 1.05em; }
.account-waiver-alert-text { color: var(--color-text); min-width: 0; }
.account-waiver-alert-text strong { color: var(--color-danger); font-weight: var(--font-semibold); }
.account-waiver-alert .btn { margin-left: auto; flex: none; }
@media (max-width: 600px) { .row-commit-hint { display: none; } }
/* The headline number the section leads with. */
.balance-total-row { display: flex; align-items: baseline; gap: var(--space-2); padding: 0 0 var(--space-2); }
.balance-total-label { font-size: var(--text-2xl); font-weight: var(--font-semibold); letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.balance-total-row.tone-owes .balance-total-label { color: var(--color-danger); }
.balance-total-row.tone-credit .balance-total-label { color: var(--color-success); }
.balance-total-row.tone-zero .balance-total-label { color: var(--color-text-muted); }

/* The band keeps a 44px touch target on a phone: the desk form (a new account, a household) opens inside the phone shell
   with its own band and header menu, and the band scrolls sideways when its tabs outrun the width. */
@media (max-width: 600px) {
  .subnav-item { min-height: 44px; padding: var(--space-3) var(--space-4); }
}

/* Base (unscoped) row anatomy — verbatim from style.css ~7392, ~15075, ~17149.
   `.attendance-row` is deliberately NOT in this selector: v3.css declares two
   different `.attendance-row` components under one class name (the F3 camp
   row, flex — vs the F1 record row, grid — see 90-waivers-misc-v3-ports.css
   ~2045 and 30-modals-checkin.css ~819 for the full account), and the grid
   rule (below, ~line 1542) sits later in this same file so it always wins the
   bare selector — this flex declaration on `.attendance-row` had zero effect.
   The two F3 camp lists get flex back via the higher-specificity
   `#camp-roster-list .attendance-row, #camp-attendance-list .attendance-row`
   rule (30-modals-checkin.css ~838). `.suggestion-row` IS flex here for real —
   it has no competing definition. */
.suggestion-row { display: flex; align-items: center; gap: var(--space-1); padding: var(--space-2) 0; border-bottom: 1px solid var(--color-border-subtle); transition: background .15s ease; }
.attendance-row .member-overflow { margin-left: auto; }

/* The ROSTER row (ADR-0130): one growing main cell, then the action slot. The
   layout is a class the BUILDER writes, so a list cannot land on the wrong one
   by being mounted in the wrong container — which is exactly what happened to
   the group-visit participant list, whose rows emitted this anatomy into
   `.gv-participant-section` and got the record grid. The two ids this used to
   be restored on (`#camp-roster-list`, `#camp-attendance-list`) were only the
   two lists anyone had noticed. */
.attendance-row.is-roster { display: flex; align-items: center; gap: var(--space-1); padding: var(--space-2) 0; }
.attendance-row.is-roster .row-actions { gap: var(--space-2); }

/* ================================================================
   TIME GROUPS + COMPACT UPCOMING ROWS (schedule-sidebar.html)
   Read from the schedule renderer in public/js/app.js: the app wraps each
   start time in .schedule-time-group carrying past / next-up / future and
   prints the clock time in .schedule-time-header. Cards hold the name and
   their detail lines; there is no time cell inside a grouped card.
   ================================================================ */
.schedule-time-group { display: flex; flex-direction: column; gap: var(--space-1); padding-bottom: var(--space-1); }
.schedule-time-header {
  display: flex; align-items: center; gap: 5px;
  font-size: 0.7143rem; font-weight: var(--font-bold);
  text-transform: uppercase; letter-spacing: .06em;
  /* text-muted is the app's value; the hour is a locator, not a fact you read. */
  color: var(--color-text-muted);
  padding: var(--space-2) 2px var(--space-1);
  font-variant-numeric: tabular-nums;
}
/* The next slot's header steps up to the full text token, and gets the SAME
   live-dot the day caption uses for "today" — the sidebar's other "right now"
   signal, not a new one. The cards under it are the only full-size ones, so
   this is reinforcement, not the whole cue. */
.schedule-time-group.next-up .schedule-time-header { color: var(--color-text); }

/* MINI: one row instead of two, for classes that have NOT happened yet.
   The hour is in the group header, so the row spends that space on the age
   group and right-aligns it into the same column the Earlier rows put their
   time in. Padding is the only geometry change; the border, radius and
   selection are the resting card's. */
.mini { padding: 4px var(--space-2) 4px var(--space-3); }
.mini .class-item-info { flex-direction: row; align-items: baseline; gap: 6px; }
/* THE AGE IS THE DISAMBIGUATOR on a class row, so it is the half that
   survives: a day holds four Parkour rows and the age group is the only thing
   telling them apart, while a name clipped to "Handstand & B…" is still the
   class you meant. So the name takes a ZERO flex-basis and ellipsises; the age
   keeps its width and still shrinks if it is longer than the whole row. */
.mini .class-item-name { flex: 1 1 0; min-width: 0; font-weight: var(--font-normal); }
.mini .class-item-age {
  flex: 0 1 auto; min-width: 0; text-align: right;
}
/* An EVENT card is ONE line at every density (ADR-0242): "[icon] Mini - Alex
   Way", then the headcount. The headcount keeps its width and never gives way;
   the name line takes the rest and ends in "..." when it runs out. */
.schedule-class-item.event-slot .class-item-info { flex-direction: row; align-items: baseline; gap: 6px; }
.schedule-class-item.event-slot .class-item-name { flex: 1 1 0; min-width: 0; }
.schedule-class-item.event-slot .class-item-age { flex: 0 0 auto; min-width: 0; text-align: right; }
/* Cancelled outranks the density colour: `.mini .class-item-age` is a
   2-class selector that beat `.class-item-cancelled-label` alone regardless
   of which file loaded second, so a cancelled class in the live "mini" rows
   (most of the schedule, per app.js's densityClass) rendered its "Cancelled"
   label in the same muted secondary grey as an ordinary age line instead of
   the warning colour the sidebar's other cancelled treatment (.cancel) uses. */
.mini .class-item-age:not(.class-item-cancelled-label) {
  color: var(--color-text-secondary);
}
.mini .class-item-count { height: 18px; }
/* Not passed yet carries the SAME tint and border as the next slot, whatever
   its size: colour says passed or not passed, size says which slot is next.
   A Later class must never read as finished. */
.mini.is-live {
  background: color-mix(in srgb, var(--class-color, var(--color-accent)) 16%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--class-color, var(--color-accent)) 55%, var(--color-border));
}
.mini.is-live:not(.selected):hover {
  background: color-mix(in srgb, var(--class-color) 24%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--class-color) 70%, var(--color-border));
}

/* Rules list (spec pages) */
.rules-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(500px, 100%), 1fr)); gap: var(--space-3) 44px; margin: 0; padding: 0; list-style: none; align-items: start; }
.rules-list li { display: grid; grid-template-columns: 128px 1fr; gap: var(--space-3); font-size: var(--text-sm); color: var(--color-text-secondary); line-height: 1.55; }
.rules-list .rule-key { font-weight: var(--font-semibold); color: var(--color-text); }
/* Group heading inside the rules list. Spans both columns and sits on a rule,
   so the reference reads in the same order as the page above it. */
.rules-list .rules-group { grid-column: 1 / -1; display: block; margin-top: var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--color-border); font-size: 0.7143rem; font-weight: var(--font-bold); letter-spacing: .1em; text-transform: uppercase; color: var(--color-text-muted); }
.rules-list .rules-group:first-child { margin-top: 0; }
/* Coach chip — LOCKED borderless treatment (app-shell + Universal Search
   Results agree; class-checkin's bordered pill lost the audit). */
.class-header-coach{display:inline-flex;align-items:center;gap:5px;flex-shrink:0;padding:3px 7px;font-family:inherit;font-size:var(--text-sm);color:var(--color-text-secondary);background:transparent;border:0;border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap}
.class-header-coach:hover{background:color-mix(in srgb,var(--color-text) 6%,transparent)}
.class-header-coach strong{font-weight:var(--font-semibold);color:var(--color-text-secondary)}
.class-header-coach .bi{font-size: 0.7857rem;color:var(--color-text-muted)}
.class-header-coach.is-empty strong{color:var(--color-text-muted);font-weight:var(--font-normal)}
.stat-item-coach-split{display:inline-flex;align-items:stretch;flex-shrink:0;border:1px solid var(--color-border);border-radius:var(--radius-sm);overflow:hidden;background:var(--color-bg-elevated)}
.stat-item-coach-split button{font-family:inherit;font-size:var(--text-sm);border:0;background:transparent;color:var(--color-text);padding:3px 9px;cursor:pointer;white-space:nowrap}
.stat-item-coach-split button+button{border-left:1px solid var(--color-border);color:var(--color-text-secondary);font-variant-numeric:tabular-nums;min-width:30px}
.stat-item-coach-split button:hover{background:var(--color-bg-subtle)}
.attendance-stats-inline{display:flex;align-items:center;gap:8px;font-size:var(--text-sm);color:var(--color-text-secondary);white-space:nowrap}
.attendance-stats-inline .class-item-count{height:26px;min-width:34px;font-size:var(--text-md)}
.attendance-stats-label{font-size:var(--text-sm);color:var(--color-text-secondary)}
/* Narrow: the band becomes a column and everything in it claims the width.
   Declared HERE, beside the band, because 70-camper-profile-notes.css loads
   before this file and lost the `align-items` half of this override to the
   base rule above on every one of the three check-in pages. */
@media(max-width:600px){
  .attendance-toolbar{flex-direction:column;align-items:stretch;gap:var(--space-2)}
  .attendance-toolbar .toolbar-left,.attendance-toolbar .toolbar-right{flex-direction:column;align-items:stretch;margin-left:0;gap:var(--space-2)}
  /* The band's search leads the column. Scoped to the band: a field in a form or a dialog stays where its markup puts it. */
  .attendance-toolbar .attendance-search{order:-1}
}
.class-header-time{font-size:var(--text-sm);color:var(--color-text-muted);text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap;min-width:52px}
.cc-row-dash{color:var(--color-text-muted);font-size:var(--text-sm)}
/* a settings value is not a pass: pass-badge colors are locked to categories */
.info-card-value{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-text-secondary)}
.attendance-waiver-warn{display:inline-flex;align-items:center;gap:4px;margin-left:7px;font-size:var(--text-xs);color:var(--color-warning);font-weight:var(--font-medium)}
.attendance-family-header{display:flex;align-items:center;gap:8px;padding:6px 14px;background:var(--color-bg-subtle);border-bottom:1px solid var(--color-border-subtle);font-size: 0.7857rem;font-weight:var(--font-semibold);letter-spacing:.06em;text-transform:uppercase;color:var(--color-text-secondary)}
.attendance-family-header .bi{color:var(--color-text-muted);font-size: 0.8571rem}
/* Square corners per the system rule "no pill radius"; overturned from
   radius-full at audit sign-off (2026-08-18). */
.class-header-cancelled-pill{display:inline-block;padding:1px 7px;border-radius:var(--radius-sm);background:var(--color-warning);color:#1a1300;font-size: 0.7143rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase}
.is-cancelled .attendance-name{text-decoration:line-through;text-decoration-thickness:1.5px;text-decoration-color:var(--color-warning)}
.search-no-account-banner{display:flex;align-items:center;gap:9px;padding:10px 14px;font-size:var(--text-md);color:var(--color-text);background:color-mix(in srgb,var(--color-danger) 8%,var(--color-bg-elevated));border-bottom:1px solid var(--color-border-subtle)}
.search-no-account-banner>.bi{color:var(--color-danger)}
.ban-pill{display:inline-flex;align-items:center;gap:4px;padding:1px 8px;font-size:var(--text-xs);font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#fff;background:var(--color-danger);border-radius:var(--radius-full)}
/* Search dropdown (L3): floats OVER the dimmed list on real elevation
   instead of blending into the panel (ADR-0082). */
.search-dropin-group{background:var(--color-bg-elevated);border:0;border-radius:var(--radius-lg);box-shadow:0 24px 64px -12px rgba(28,25,23,.30),0 8px 24px rgba(28,25,23,.13),0 0 0 1px var(--float-edge);overflow:hidden}
/* The card owns its own display, once (ADR-0094). `.on` shows it; placement
   (.ov-drop / .ov-float) never touches display. The subgrid lets the GROUP
   own the two tracks so every row's badge column starts at the same x --
   no magic 190px; below the subgrid floor (Safari < 16) each row keeps its
   own `auto` track and the list is merely ragged. Rows stay real boxes
   spanning both tracks, so backgrounds, hover and hit targets are unaffected.
   The phone (ADR-0092) forces the group to `block` for full-bleed placement,
   which also voids the subgrid; mobile.css carries the row-level flex
   companion for that one case, next to the rule that causes it. */
.search-dropin-group{display:none}
.search-dropin-group.on{display:block}
@supports (grid-template-columns: subgrid){
  .search-dropin-group.on{display:grid;grid-template-columns:minmax(90px,1fr) auto}
  .search-dropin-group>*{grid-column:1/-1}
  .search-dropin-group>.search-result-item,.search-dropin-group>.search-dropin-option{display:grid;grid-template-columns:subgrid}
}
/* Floating placement for scroll-container hosts (modals, sheets): the
   component portals the card to <body> and sets viewport-fixed geometry
   (top/left/width/max-height) inline, anchored to the field — placement
   only; the look above is untouched. Above .modal-overlay (10000). The
   max-height scroll is a safety net for a field near the viewport edge;
   the 8-row cap keeps a card from ever needing it (ADR-0082). */
.search-dropin-group.ov-float{position:fixed;z-index:var(--z-above-modal);overflow-y:auto}
/* Row weights beyond the base: a static row states a fact and is not a
   target ("Already checked in"); a banned row states the ban (ADR-0082). */
.search-result-item.is-static{cursor:default;opacity:.6}
.search-result-item.is-static:hover{background:transparent}
.search-result-item--banned,.search-result-item--banned:hover{background:color-mix(in srgb,var(--color-danger) 10%,transparent);cursor:default}
.search-dropin-group>:first-child{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}
.search-dropin-group>:last-child{border-bottom-left-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}
.search-dropin-option{display:grid;grid-template-columns:minmax(90px,1fr) auto;align-items:center;gap:var(--space-2);min-height:42px;padding:6px 14px;border-bottom:1px solid var(--color-border-subtle);cursor:pointer}
.search-dropin-option:hover{background:var(--color-accent-tint)}
/* Mode header (ADR-0082) -- the dropdown's state is the headline. Suggested
   is the neutral head; `.is-search` is the accent-washed query state. */
.search-mode-head{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--color-bg-subtle);border-bottom:1px solid var(--color-border)}
.search-mode-head.is-search{background:color-mix(in srgb,var(--color-accent-tint) 60%,var(--color-bg-elevated));border-bottom-color:color-mix(in srgb,var(--color-accent) 18%,var(--color-border))}
.search-mode-ico{width:28px;height:28px;border-radius:var(--radius-md);background:var(--color-bg-muted);color:var(--color-text-secondary);display:inline-flex;align-items:center;justify-content:center;flex:none}
.search-mode-head.is-search .search-mode-ico{background:var(--color-accent);color:var(--color-accent-fg)}
.search-mode-title{display:flex;flex-direction:column;gap:1px;min-width:0}
.search-mode-t{font-size:var(--text-md);font-weight:var(--font-semibold);color:var(--color-text);line-height:1.2}
.search-mode-t .search-mode-q{color:var(--color-accent)}
.search-mode-s{font-size:var(--text-xs);color:var(--color-text-secondary);line-height:1.2}
/* Archived section head (ADR-0140) -- a plain divider, subordinate to the
   mode header: muted strip + label. Archived rows below it render through
   the SAME .search-result-item as live rows -- no per-row tint, no Restore
   button; picking one restores the account as a side effect of the write it
   feeds (core/account-restore.js), not a separate step here. */
.search-archived-head{display:flex;align-items:center;gap:8px;padding:7px 14px;background:var(--color-bg-muted);border-top:1px solid var(--color-border);border-bottom:1px solid var(--color-border)}
.search-archived-head .bi{color:var(--color-text-secondary)}
.search-archived-t{font-size:var(--text-base);font-weight:var(--font-semibold);color:var(--color-text-secondary)}
.search-result-meta{font-size:var(--text-xs);color:var(--color-text-muted);margin-left:7px}
.search-dropin-note{padding:8px 14px;font-size:var(--text-sm);color:var(--color-text-secondary);background:var(--color-bg-subtle);border-bottom:1px solid var(--color-border-subtle);display:flex;gap:7px;align-items:flex-start}
.search-dropin-note .bi{color:var(--color-text-muted);margin-top:2px}
.search-dropin-footer{padding:8px 14px;font-size:var(--text-sm);color:var(--color-text-muted);background:var(--color-bg-subtle);display:flex;gap:7px;align-items:center}
/* Guest fallback (ADR-0082) -- footer furniture in the no-match state ONLY,
   never a peer of person rows: every real visit belongs on an account. */
.search-guest-fallback{display:flex;align-items:baseline;justify-content:center;gap:7px;padding:9px 14px;background:var(--color-bg-subtle);border-top:1px solid var(--color-border-subtle);cursor:pointer}
.search-guest-link{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-text-secondary);text-decoration:underline;text-underline-offset:2px;text-decoration-color:var(--color-border-strong)}
.search-guest-fallback:hover .search-guest-link{color:var(--color-text)}
.search-guest-sub{font-size:var(--text-xs);color:var(--color-text-muted)}
.dropdown-section-divider{flex:none;height:1px;background:var(--color-border-subtle);margin:var(--space-1) calc(-1 * var(--space-1))}
.class-header-stats{padding:var(--space-2) var(--space-3) 6px;font-size:var(--text-sm);color:var(--color-text-muted);display:flex;flex-direction:column;gap:3px}
.class-header-stats .line{display:flex;justify-content:space-between;gap:var(--space-4);font-variant-numeric:tabular-nums}
.class-header-stats .line strong{color:var(--color-text-secondary);font-weight:var(--font-semibold)}
/* No-account banner + next steps (search results with zero matches) */
.search-no-account-title{flex:1;min-width:0;font-size:var(--text-md);font-weight:var(--font-semibold);color:var(--color-text)}
.search-next-steps{margin:14px 14px}
.search-waiver-hint{display:flex;align-items:flex-start;gap:8px;font-size:var(--text-sm);color:var(--color-text-secondary);line-height:1.5}
.search-waiver-hint .bi{color:var(--color-text-muted);margin-top:2px}

/* ================================================================
   CHECK-IN POPOVER (.mini-schedule-popup) — CANONICAL (Locked)
   Was duplicated in app-shell.html and checkin-popover.html; internals
   are scoped under .mini-schedule-popup because .col/.dropdown-section-label/.mini-schedule-item are too generic loose.
   Floats fixed by default; .anchored is the production beside-the-
   trigger mode; spec pages showing one inline override position only.
   ================================================================ */
/* The elevation is `--z-anchored`, the tier token every anchored surface now
   spells (see ELEVATION at the top of this file). It was raised from 40 to
   1000 because the shell's own chrome (.sidebar/.schedule-sidebar) sits at
   101/102: at tablet widths, where the popup flips left and lands over the
   schedule column, it rendered BEHIND the sidebar it was overlapping. */
.mini-schedule-popup{position:fixed;z-index:var(--z-anchored);width:660px;max-width:94vw;transition:width .12s}
.mini-schedule-popup .id-line{display:flex;align-items:center;gap:8px;padding:7px 12px;border-bottom:1px solid color-mix(in srgb,var(--color-accent) 22%,var(--color-border));background:var(--color-accent-tint);font-size: 0.8571rem;color:var(--color-text-secondary)}
.mini-schedule-popup .id-line strong{color:var(--color-accent);font-size: 0.9286rem}
.mini-schedule-popup .id-line.in{background:var(--color-success-tint);border-bottom-color:color-mix(in srgb,var(--color-success) 25%,var(--color-border))}
.mini-schedule-popup .id-line.in strong{color:var(--color-text)}
.mini-schedule-popup .id-st{margin-left:auto;display:flex;align-items:center;gap:6px;font-size: 0.8571rem;font-weight:600;color:var(--color-success)}
.mini-schedule-popup .id-st .bi-check-circle-fill{color:var(--color-success);font-size: 0.8571rem}
.mini-schedule-popup .undo-all{display:none;font-family:inherit;font-size: 0.7857rem;font-weight:600;color:var(--color-danger);background:transparent;border:1px solid color-mix(in srgb,var(--color-danger) 35%,transparent);border-radius:var(--radius-sm);padding:2px 8px;cursor:pointer;margin-left:10px;flex-shrink:0}
.mini-schedule-popup .undo-all:hover{background:color-mix(in srgb,var(--color-danger) 10%,var(--color-bg-elevated))}
.mini-schedule-popup .id-line.in .undo-all{display:inline-block}
.mini-schedule-popup .cols{display:grid;grid-template-columns:1fr 1fr}
.mini-schedule-popup .col+.col{border-left:1px solid var(--color-border)}
.mini-schedule-popup .col{padding:6px;display:flex;flex-direction:column;gap:1px;min-width:0}
.mini-schedule-popup .dropdown-section-label{display:flex;align-items:center;gap:7px;margin:6px -6px 3px;padding:3px 12px;background:var(--color-bg-subtle);border-top:1px solid var(--color-border);border-bottom:1px solid var(--color-border);font-size: 0.7143rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--color-text-secondary)}
.mini-schedule-popup .col .dropdown-section-label:first-child{margin-top:-6px;border-top:0}
.mini-schedule-popup .dropdown-section-label .bi{font-size: 0.7857rem}
.mini-schedule-popup .dropdown-section-label .dropdown-section-count{margin-left:auto;font-weight:600;letter-spacing:0;text-transform:none;color:var(--color-text-muted)}
/* Deliberate deviation from the design seed (_design-reference/v3.css: a fixed 100px name column and a 1fr aside): the NAME is the column that flexes, and the aside (room / age group) is sized to its text, capped at 40% so a long age label can't take the space back. The fixed 100px truncated "Charles GONZALES" beside a mostly empty "Room 2" column. */
.mini-schedule-popup .mini-schedule-item{display:grid;grid-template-columns:14px minmax(0,1fr) fit-content(40%) max-content;align-items:center;gap:0;padding:3px 8px;border-radius:var(--radius-sm);cursor:pointer;font-size:var(--text-sm);color:var(--color-text);line-height:1.2;user-select:none;min-height:29px}
.mini-schedule-popup .mini-schedule-item:hover{background:color-mix(in srgb,var(--class-color,var(--color-bg-muted)) 14%,var(--color-bg-elevated))}
.mini-schedule-popup .mini-schedule-check{grid-column:1;justify-self:center;width:8px;height:8px;border-radius:50%;background:var(--class-color)}
.mini-schedule-popup .mini-schedule-item>.bi{grid-column:1;justify-self:center}
.mini-schedule-popup .mini-schedule-name{grid-column:2;padding-left:8px;font-weight:var(--font-medium);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mini-schedule-popup .mini-schedule-age{grid-column:3;min-width:0;font-size:var(--text-xs);color:var(--color-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 0 0 8px}
.mini-schedule-popup .mini-schedule-time{grid-column:4;font-size:var(--text-xs);color:var(--color-text-muted);font-variant-numeric:tabular-nums;min-width:52px;text-align:right;display:flex;align-items:center;justify-content:flex-end;gap:5px}
/* An event row's aside is its room: a fixed share of the row, so every room in a list starts at the same x, left-aligned between the name and the time. */
.mini-schedule-popup .mini-schedule-item.event{grid-template-columns:14px minmax(0,1fr) 38% max-content}
.mini-schedule-popup .cancel{color:var(--color-warning);font-weight:600}
.mini-schedule-popup .mini-schedule-item.next{background:color-mix(in srgb,var(--color-accent) 10%,var(--color-bg-elevated))}
.mini-schedule-popup .mini-schedule-item.next .mini-schedule-name{font-weight:var(--font-bold,700)}
.mini-schedule-popup .mini-schedule-item.next .mini-schedule-age,.mini-schedule-popup .mini-schedule-item.next .mini-schedule-time{font-weight:var(--font-semibold,600);color:var(--color-text-secondary)}
.mini-schedule-popup.quiet .mini-schedule-item.next:not(.in){background:transparent}
.mini-schedule-popup.quiet .mini-schedule-item.next:not(.in) .mini-schedule-name{font-weight:var(--font-medium)}
.mini-schedule-popup.quiet .mini-schedule-item.next:not(.in) .mini-schedule-age,.mini-schedule-popup.quiet .mini-schedule-item.next:not(.in) .mini-schedule-time{font-weight:400;color:var(--color-text-muted)}
.mini-schedule-popup .mini-schedule-item.dim{opacity:.45}
.mini-schedule-popup .mini-schedule-item.dim:hover{opacity:.8}
.mini-schedule-popup .mini-schedule-item.in{background:color-mix(in srgb,var(--color-success) 10%,var(--color-bg-elevated));border-radius:var(--radius-md);opacity:1}
.mini-schedule-popup .mini-schedule-item.in:hover{background:color-mix(in srgb,var(--color-danger) 8%,var(--color-bg-elevated))}
.mini-schedule-popup .chk{display:flex;align-items:center}
.mini-schedule-popup .mini-schedule-name{display:flex;align-items:center;gap:6px;min-width:0}
.mini-schedule-popup .mini-schedule-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mini-schedule-popup .mini-schedule-name>.chk{flex-shrink:0}
.mini-schedule-popup .chk .bi{line-height:1}
.mini-schedule-popup .chk .bi-check-circle-fill{color:var(--color-success);font-size: 0.8571rem}
.mini-schedule-popup .chk .bi-dash-circle-fill{display:none;color:var(--color-danger);font-size: 0.8571rem}
.mini-schedule-popup .mini-schedule-item.in:hover .bi-check-circle-fill{display:none}
.mini-schedule-popup .mini-schedule-item.in:hover .bi-dash-circle-fill{display:inline}

/* ================================================================
   CAMP CHECK-IN (V3 port)
   Ported 2026-08-18 from camp-checkin-style.css — the verbatim
   production snapshot at the project root, which is now REFERENCE
   ONLY and never linked from a V3 page. Deltas from the snapshot:
   data-bs-theme → data-theme, --color-bg-secondary → --color-bg-subtle,
   #2196f3 → var(--color-classes). Modal internals (roll call, alert
   panels) stay in the snapshot until a V3 page needs them.
   ================================================================ */
/* The two slots moved up beside `.attendance-toolbar` itself (ADR-0124) — they
   were never camp's, they were the band's, and event had a second spelling of
   the right one (`.attendance-toolbar-right`) for the same job. */
/* `.search-wrap` removed — the toolbar rewrite (this port) replaced that
   markup with `.attendance-search`, the shared field object; nothing
   renders this selector anymore. */
/* `.camp-toolbar .filter-chip` (+ `.is-active`, `.chevron`, `-clear`) removed
   — six rules for a dropdown-trigger chip the camp band never rendered. Camp's
   band is a field and two buttons; its filtering is the field. The one live
   `.filter-chip` is the Members / History applied-filter readout above. */
.btn-camp{background:var(--color-camp);color:#fff;border-color:var(--color-camp)}
.btn-camp:hover{background:var(--color-camp-dark);border-color:var(--color-camp-dark)}
/* `.camp-lock-banner` / `.camp-day-warning` removed — the camp's day-state
   notices are B (`.class-checkin-banner.warn`), like every other page's. The
   bundle carried these because it absorbed our own pre-V3 CSS. */
.class-checkin-layout.is-locked .attendance-row .row-actions,.class-checkin-layout.is-locked .camp-family-header .family-action-btn,.class-checkin-layout.is-locked .camp-family-header .member-overflow,.class-checkin-layout.is-locked [data-action="campAddCamper"]{opacity:.45;pointer-events:none}
.class-checkin-layout.is-camp-locked [data-action="campRollCall"]{opacity:.45;pointer-events:none}
.class-item-completed-label{color:var(--color-text-secondary);font-weight:600}
.schedule-class-item.camp-completed .class-item-name{opacity:.75}
#tab-camp-checkin .card-body{padding:0}
#tab-camp-checkin .attendance-row{padding-left:var(--space-4);padding-right:var(--space-4)}
#tab-camp-checkin .empty-state{padding:var(--space-3) var(--space-4)}
#tab-camp-checkin .attendance-list{margin:0}
#tab-camp-checkin .row-main{flex-direction:column;align-items:flex-start;gap:1px}
#tab-camp-checkin .row-name{display:flex;align-items:center;gap:5px;flex-wrap:wrap;line-height:1.4}
#tab-camp-checkin .row-actions{margin-left:auto}
#tab-camp-checkin .row-time{font-size: 0.7857rem;color:var(--color-text-muted);line-height:1.3}
#tab-camp-checkin .attendance-row.is-out .row-main{opacity:.5}
#tab-camp-checkin .attendance-row.camp-noshow-row{opacity:.6}
#tab-camp-checkin .attendance-row.camp-noshow-row .row-name{text-decoration:line-through;color:var(--color-text-muted)}
.roster-item-medical,.late-badge,.roster-item-buddy,.roster-item-campnote{display:inline-flex;align-items:center;gap:3px;line-height:1;font-size: 0.7143rem;font-weight:700;border-radius:var(--radius-sm);padding:2px 6px;white-space:nowrap;text-transform:none;letter-spacing:normal}
.roster-item-medical{color:var(--color-danger);background:color-mix(in srgb,var(--color-danger) 12%,transparent);border:1px solid color-mix(in srgb,var(--color-danger) 22%,transparent)}
.late-badge{color:var(--color-warning);background:color-mix(in srgb,var(--color-warning) 12%,transparent);border:1px solid color-mix(in srgb,var(--color-warning) 22%,transparent)}
.roster-item-buddy{color:var(--color-accent);background:color-mix(in srgb,var(--color-accent) 12%,transparent);border:1px solid color-mix(in srgb,var(--color-accent) 22%,transparent)}
.roster-item-campnote{color:var(--color-info);background:color-mix(in srgb,var(--color-info) 12%,transparent);border:1px solid color-mix(in srgb,var(--color-info) 22%,transparent)}
#tab-camp-checkin .camp-section-header{display:flex;align-items:center;gap:6px;padding:7px var(--space-4);background:var(--color-bg-subtle);color:var(--color-text-muted);font-size: 0.7857rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em}
#tab-camp-checkin .camp-section-header>i{font-size: 0.7857rem;flex-shrink:0}
#tab-camp-checkin .camp-section-label{flex:1}
#tab-camp-checkin .camp-section-count{font-weight:600}
#tab-camp-checkin .camp-section-header.late{background:color-mix(in srgb,var(--color-warning) 12%,var(--color-bg-elevated));color:var(--color-warning)}
#tab-camp-checkin .camp-section-header.late>i{color:var(--color-warning)}
#tab-camp-checkin .camp-section-header.danger{background:color-mix(in srgb,var(--color-danger) 10%,var(--color-bg-elevated));color:var(--color-danger)}
#tab-camp-checkin .camp-section-header.danger>i{color:var(--color-danger)}
#tab-camp-checkin .camp-family-header{display:flex;align-items:center;gap:8px;padding:6px 14px;background:color-mix(in srgb,var(--color-classes) 14%,var(--color-bg-elevated));color:var(--color-text-secondary);font-size: 0.7857rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--color-border-subtle)}
#tab-camp-checkin .camp-family-icon{width:auto;height:auto;background:none;color:var(--color-text-muted);font-size: 0.8571rem}
#tab-camp-checkin .camp-family-icon .bi{display:block}
#tab-camp-checkin .camp-family-name{flex:1}
#tab-camp-checkin .camp-family-count{font-weight:600}
#tab-camp-checkin .camp-family-header .member-overflow{margin-left:2px}
/* Camp check-in is a CHECK-IN, so it wears the check-in button — the same
   component the schedule uses — carrying camp's own colour on --class-color.
   It used to be `.member-icon-single` + `bi-clipboard2-plus`, which is the
   WAIVER CHECK button (tour-manager labels that pair "Waiver check", and
   app.js reads `.member-icon-single.checked-in` into a var named `waiver`):
   a lookalike wired to a different action. It was also success green, which
   is Open Gym's hue (--color-opengym #4caf50), so a camp row advertised
   another destination.

   The roster button never reaches `.checked-in` in place — checking a camper
   in moves the row to the Present column — so the destination colour lands on
   HOVER, as a preview of the commit, using the standard's own active recipe.
   Idle stays neutral and unfilled, per the rule. */
#camp-roster-list .member-schedule-btn:hover:not(.checked-in):not(.checked-waiver),
#tab-camp-checkin .camp-family-header .family-action-btn:hover:not(.checked-in):not(.checked-waiver) {
  background: color-mix(in srgb, var(--class-color, var(--color-camp)) 14%, var(--color-bg-elevated));
  border-color: color-mix(in srgb, var(--class-color, var(--color-camp)) 30%, var(--color-border));
  color: color-mix(in srgb, var(--class-color, var(--color-camp)) 75%, var(--color-text));
}
#tab-camp-checkin .attendance-row.is-family-member{background:color-mix(in srgb,var(--color-classes) 6%,var(--color-bg-elevated));padding-left:calc(var(--space-4) + var(--space-2))}
#tab-camp-checkin #camp-header-stats{margin-left:auto;min-width:0;overflow:hidden}
@media(max-width:600px){.camp-toolbar .toolbar-right .btn-ghost{display:none}}
/* .class-checkin-body: V3 definition lives above (one rule per component). */
.class-checkin-column{display:flex;flex-direction:column;gap:var(--space-4)}
.camp-header-strip{display:flex;align-items:center;gap:3px;margin-left:8px}
/* height:26px matches every other header count chip (.attendance-stats-inline
   .class-item-count — Today's Visitors, class header tally, event header
   tally) — one consistent chip height across the app instead of camp's day
   strip being the one outlier at 22px. */
.chs-day{display:inline-flex;align-items:center;gap:3px;height:26px;padding:0 7px;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg-elevated);cursor:pointer;transition:all .15s ease;user-select:none}
.chs-day:hover{border-color:var(--color-border-strong)}
.chs-name{font-size: 0.6429rem;font-weight:var(--font-semibold);text-transform:uppercase;color:var(--color-text-muted)}
.chs-num{font-size: 0.7857rem;font-weight:var(--font-semibold);color:var(--color-text);font-variant-numeric:tabular-nums}
.chs-day-active{background:var(--color-accent);border-color:var(--color-accent)}
.chs-day-active .chs-name,.chs-day-active .chs-num{color:#fff}
.chs-day-today:not(.chs-day-active){border-color:var(--color-accent)}
.chs-day-past:not(.chs-day-active){opacity:.5}
/* Sidebar column is 240px, not the design's authored 220px: at 220,
   "Acro Dance 2 · Teens & adults" only had 128px for name+age combined and
   needed 136+ (docs/v3-spec-audit.md, "On the age group"). The app has room
   the reference didn't design for — main is minmax(0,1fr), so widening this
   column only reallocates its own slack, it doesn't touch --win-max. User
   decision 2026-08-25: widen the column instead of always sacrificing the
   name to ellipsis. */
/* ...and 240px only holds at scale 1. The column is chrome plus ONE line of
   text: 96px of it is fixed px box (12px scroll pad + 20px card inset + 2px
   border + 20px card padding + the 8px and 6px gaps + the 28px count chip),
   and the remaining 144px is the name+age budget the `.mini` rows spend.
   ADR-0081 scales type only, so at 120% that budget stays 144 while
   "Gymnastics" + "Teens & adults" grows 127 -> 150px and the name loses to
   ellipsis again -- the same failure the 220 -> 240 widening fixed at scale 1.
   So the TEXT SHARE scales and the chrome does not (ADR-0085): 240px at 1,
   254px at 1.1, 269px at 1.2, where the widest live pairing ("Acro Dance 2" +
   "Teens & adults", 160px at 1.2) clears with room. A naive 240px * scale
   would reach 288px by scaling the chrome too, taking 19px more from the
   content card than the text actually needs. */
.shell {
  --sched-col:calc(96px + 144px * var(--ui-font-scale, 1));
}
.shell .schedule-sidebar{background:transparent;border-right:none;position:relative;padding-top:7px}
/* padding-bottom:0 too — the base rule's 12px left the last card (Add group
   event) sitting 12px above the sidebar's own bottom edge, which .schedule-
   sidebar's margin-bottom (above) now aligns exactly with the main content
   card's bottom. Flush was the ask, not a resting gap. */
.shell .schedule-sidebar .schedule-scroll{padding-top:0;padding-bottom:0}
.shell .schedule-day-block{height:var(--band-h);box-sizing:border-box;padding:9px 16px 9px 12px;display:flex;align-items:center;border-bottom:none}
.shell .schedule-day-caption{height:var(--ctl-h);width:100%;box-sizing:border-box;display:flex;align-items:center;margin-bottom:0}
.shell .schedule-day-btn{height:var(--ctl-h);border-radius:var(--radius-sm)}
.shell .schedule-day-label{flex:1;justify-content:center;font-size:var(--text-md)}
.shell .schedule-sidebar .schedule-allday-end{margin:var(--space-2) calc(var(--space-3)*2) 0 var(--space-3)}
/* app-shell.html: "visitors is a destination with no category, so it drives the
   same neutral colour as the page it opens rather than the app accent". It
   publishes that through --class-color like every other destination, and the
   selected rules read it — naming the token directly here instead left the CARD
   free to pick a different colour, which is exactly what happened: app.js sent
   the card var(--color-accent) while the tab painted neutral, so the two ran
   different colours across the seam they are supposed to fuse at. */
/* margin-left/width match .schedule-sidebar .schedule-class-item exactly
   (v3.css ~805) — both are cards in the same column and must share one left
   edge. Without it Today's Visitors sat 4px further left than Open Gym and
   every class/camp card below it (measured live). */
.shell .schedule-sidebar .schedule-visitors{--class-color:var(--color-text-secondary);width:calc(100% - var(--space-4) - 4px);margin-left:4px;margin-right:var(--space-3);box-sizing:border-box}
/* 9%, the SAME mix the content header uses (.class-checkin-header / base.css's
   .cc-head). The selected tab and the header it opens are one fused shape
   across the seam, so a different fill strength on each half reads as a step
   right at the join — most visibly on Today's Visitors, where both halves take
   the same NEUTRAL hue and only the strength differs. The 18% here was drift
   anyway: base.css authors 14% (against the accent, before production made
   visitors neutral). Selected-ness does not depend on this fill — border-color
   below and the full-strength label/count on the next line both carry it. */
.shell .schedule-sidebar .schedule-visitors.selected{background:color-mix(in srgb,var(--class-color) 9%,var(--color-bg-elevated));border-color:var(--class-color)}
.shell .schedule-sidebar .schedule-visitors.selected .schedule-visitors-label,.shell .schedule-sidebar .schedule-visitors.selected .schedule-visitors-count{color:var(--color-text)}
/* width is calc(100% - 4px), NOT 100%: both cards carry `margin-left: 4px`
   (focus-ring clearance, ~line 808) which the base rule compensates for in its
   own width. Dropping to a bare 100% here kept the 4px margin and pushed the
   selected card's right edge 4px PAST the seam, so it overhung the content
   card instead of landing on it — the fused-tab contract, and what
   shell-composition-v3.spec.js measures. */
html:not([data-shell="compact"]) .shell .schedule-sidebar .schedule-class-item.selected,html:not([data-shell="compact"]) .shell .schedule-sidebar .schedule-visitors.selected{width:calc(100% - 4px);margin-right:0;border-right:0;border-top-right-radius:0;border-bottom-right-radius:0;position:relative;z-index:10;box-shadow:none}
.page.no-tab{--class-color:var(--color-border-strong)}
.page.no-tab::before{inset:0}
.page.no-tab .class-checkin-header{background:var(--color-bg-elevated);border-bottom:1px solid var(--color-border)}
/* `.page.no-tab .attendance-toolbar{display:none}` was here and is DELETED
   (ADR-0124). `.page` is `.main-content-body` — ONE element wrapping every tab
   — so `.no-tab` is not a property of a page, it is a property of whatever the
   app is showing right now. The rule therefore reached EVERY toolbar in the
   document whenever the current tab was not a schedule destination, which is
   exactly the set of pages this row adopts the band onto.
   It came from app-shell.html, where it guards a genuinely different shape: the
   prototype has ONE shared band serving every destination, so a destination
   with no check-in field must hide it (its `go()` does the same thing a second
   time, off `d.noField`). Production gives each page its own band inside its
   own `.tab-content`, and `.no-tab` is set only while a NON-destination tab is
   showing — at which point all three bands are inside hidden tabs already. So
   the rule protected nothing, and the moment a utility page adopts the band it
   would hide it on exactly that page. */
.ov-wrap{position:relative;flex:1;display:flex;flex-direction:column;min-height:0}
/* Anchored to the field, not spanning the panel: a popover of the FIELD.
   680px, clamped on narrow screens (ADR-0082; widened from 520px same-day —
   two pass badges left too little room for the name). */
.ov-drop{position:absolute;left:14px;top:2px;width:680px;max-width:calc(100% - 28px);z-index:3}
.shell .schedule-list .cancel{color:var(--color-warning);font-weight:600}
/* REMOVED: the bundle's five-column %-widths + table-layout:fixed describe the
   reference's ~570px demo card (app-shell.html:114-116). Production's table is
   EIGHT columns, so nth-child(5) lands on the Access cell (hidden at desktop)
   and the action cell falls back to 50px+70px with three nowrap buttons in it.
   This was skipped deliberately once and reinstated by the wholesale v3.css
   swap. Report to the designer -- it needs scoping, not a local override. */
.shell .member-table .member-passes{display:flex;gap:6px;flex-wrap:wrap;min-width:0}
.shell .member-table .notes-badge{margin-left:6px}
