/*
 * Semantic admin component styles — Phase 5 (#521).
 *
 * Plan §7 layer 3: Spruce-owned classes consumed by the `SpruceWeb.AdminUI`
 * function components. Token-only values (admin_tokens.css) except where a
 * content-specific need is documented inline.
 *
 * ── LOAD ORDER ───────────────────────────────────────────────────────────
 * This file is linked AFTER admin_v2.css in both admin layouts. Plan §7
 * lists "semantic components" above "legacy compatibility" as *ownership*
 * layers, but the cascade must run the other way round: the v2 compat
 * adapter grew base-surface element rules that reach every place migrated
 * content renders, and those out-specify a naive `.spruce-admin .sa-*`.
 *
 * ── SCOPE AND SPECIFICITY FLOOR ──────────────────────────────────────────
 * Every selector is led by `.spruce-admin.spruce-admin--v2` AND has
 * specificity >= (0,3,1) — the floor set by admin_v2.css's base surface:
 *
 *   .spruce-admin.spruce-admin--v2 :is(.sa-main, .up-modal-content, .modal)
 *     :is(h1,h2,h3,h4,h5,h6)                                       (0,3,1)
 *
 * A `.spruce-admin .sa-page-title` rule is (0,2,0) and a
 * `.spruce-admin.spruce-admin--v2 .sa-page-title` rule is (0,3,0); both
 * LOSE that rule's font-family and color, so the semantic component would
 * appear to work while the compat layer actually owned its typography.
 * Component parts are therefore selected under their component root —
 * `… .sa-page-header .sa-page-title` (0,4,0) — and component roots carry a
 * tag qualifier to reach (0,3,1), where the later source order wins the
 * tie. The tag qualifiers can be dropped when the compat layer sunsets
 * (plan §7.1). `test/spruce_web/layout_isolation_test.exs` enforces both
 * the scope prefix and the floor.
 *
 * ── CLASS REGISTRY ───────────────────────────────────────────────────────
 * One owning stylesheet per `sa-` class. Shell chrome (`sa-main`,
 * `sa-sidebar`, `sa-topbar`, …) is owned by admin_shell.css and merely
 * REFERENCED as a context by admin_v2.css and admin_tailwind.css; nothing
 * below may be declared in any of those three files. Source of truth:
 * `SpruceWeb.AdminUI.class_registry/0`.
 *
 *   page_header:  sa-page-header, sa-page-header-main, sa-page-title,
 *                 sa-page-title-note, sa-page-description, sa-page-actions,
 *                 sa-page-meta
 *   table_scroll: sa-table-scroll
 *   row_actions:  sa-row-actions
 *   button /
 *   link_button:  sa-button, sa-button--primary, sa-button--secondary,
 *                 sa-button--danger, sa-button--warning, sa-button--link,
 *                 sa-button--outline-primary, sa-button--outline-danger,
 *                 sa-button--sm, sa-button--xs
 *   alert:        sa-alert, sa-alert--danger, sa-alert--warning,
 *                 sa-alert--info, sa-alert--success
 *   close_button: sa-close-button
 *   badge:        sa-badge, sa-badge--primary, sa-badge--secondary,
 *                 sa-badge--success, sa-badge--danger, sa-badge--warning,
 *                 sa-badge--info, sa-badge--light
 *   table:        sa-table, sa-table--striped, sa-table--hover,
 *                 sa-table--centered, sa-table--sm, sa-table--bordered
 *   card:         sa-card, sa-card--brand, sa-card--danger, sa-card--success
 *   card_header:  sa-card-header, sa-card-header--danger,
 *                 sa-card-header--success
 *   card_body:    sa-card-body
 *   card_label:   sa-card-label
 *   stat_card:    sa-stat-card, sa-stat-card-icon, sa-stat-card-glyph,
 *                 sa-stat-card-label, sa-stat-card-value, sa-stat-card-hint
 *   tone_surface_
 *     button:     sa-button--on-tone  (opt-in modifier, emitted by no
 *                 component — a call site passes it through family 2's
 *                 `:class` rider; see the tone-surface section)
 *
 * Non-`sa-` selectors live here on purpose, reproducing legacy theme rules
 * the conversion moves out of scope; each is documented at the rule.
 * `.sa-page-meta p.tip`, `.sa-alert .alert-link`, `.sa-card h3`,
 * `.sa-card--brand .text-muted-primary`, `.sa-card--brand .text-white` and
 * `.sa-card--brand .font-13.text-uppercase`.
 */

/* =========================================================================
 * Page header (design PageHeader) — slice 1
 *
 * PARITY NOTE. Slice 1a re-points `SpruceWeb.Component.header/2` at
 * `AdminUI.page_header/1`, converting 18 admin pages with no template diff;
 * slice 1b converts the pages that hand-rolled the same markup inline.
 * The geometry below reproduces what those pages render today through the
 * compat adapter's `.page-title-box` mapping (flex row, space-between,
 * wrap, --space-4 gap, --space-6 bottom margin, centered cross-axis) rather
 * than re-deciding it, so this slice proves layering rather than changing
 * the look. Two deliberate departures from the legacy Hyper base are marked
 * DEPARTURE below.
 * ========================================================================= */

.spruce-admin.spruce-admin--v2 header.sa-page-header {
  display: flex;
  /* Matches the compat `.page-title-box` mapping these pages render today.
     Not the design's flex-start: no converted call site has a description
     yet, and centering keeps a single action button on the title's optical
     centre-line. Revisit when a slice converts a page whose header carries
     supporting copy (slice 1b group 3). */
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
  line-height: normal;
}

.spruce-admin.spruce-admin--v2 .sa-page-header .sa-page-header-main {
  /* min-width:0 lets a long unbroken title shrink instead of forcing the
     flex row wider than the page (the 375px containment defect class,
     #574 / #609). */
  flex: 1 1 auto;
  min-width: 0;
}

.spruce-admin.spruce-admin--v2 .sa-page-header .sa-page-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
  line-height: var(--leading-tight);
  /* DEPARTURE 1: Hyper's `.page-title-box .page-title` is
     `white-space: nowrap; overflow: hidden; text-overflow: ellipsis`, so
     interpolated titles ("Registrants for <registerable>", "Bulk refund
     for <registerable>") are silently truncated on narrow viewports. A
     Spruce-owned page title wraps; break-word keeps an unbroken token from
     escaping the container. */
  overflow-wrap: break-word;
}

.spruce-admin.spruce-admin--v2 .sa-page-header .sa-page-title-note {
  /* PARITY. The six pages this replaces render the qualifier as a bare
     `<small>` inside `.page-title`, so its appearance comes from the theme's
     Bootstrap `_type.scss`:

       .small, small { font-size: .75rem; font-weight: 400 }

     (`$small-font-size` is overridden from Bootstrap's default 80% to an
     ABSOLUTE .75rem in the theme, so the qualifier does not scale with the
     28px title — 12px is what production renders today, not a new choice.
     `--text-xs` is 12px and the document root is the browser default 16px,
     so this is the same computed value.)

     Two declarations, matching `small` exactly. In particular no `color`:
     the qualifier inherits `--text-heading` from `.sa-page-title` today, and
     muting it would be a visual change this slice is not making. */
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
}

.spruce-admin.spruce-admin--v2 .sa-page-header .sa-page-description {
  margin: var(--space-2) 0 0;
  font-size: var(--text-md);
  color: var(--text-muted);
}

.spruce-admin.spruce-admin--v2 .sa-page-header .sa-page-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  /* Legacy `.page-title-right` is floated by Hyper and un-floated by the
     compat adapter with `margin-left: auto`; keep the trailing-edge
     alignment when the header row has room to spare. */
  margin-left: auto;
}

.spruce-admin.spruce-admin--v2 .sa-page-header .sa-page-meta {
  /* PARITY. Mirrors the compat adapter's
       .page-title-box > :not(.page-title):not(.page-title-right)
         { flex-basis: 100%; order: 2 }
     (admin_v2.css), written for exactly the five pages that put block
     content inside the title box after the <h1>. No `order` counterpart is
     needed: the component renders `meta` AFTER `actions`, so DOM order is
     already visual order.
     min-width:0 for the same reason as .sa-page-header-main — a long
     unbroken token in the meta block must not force the flex row wider
     than the page (#574 / #609). */
  flex-basis: 100%;
  min-width: 0;
}

.spruce-admin.spruce-admin--v2 .sa-page-header .sa-page-meta p.tip {
  /* PARITY, and the one disclosed geometry risk in this conversion.
     `assets/css/theme/custom/structure/_page-head.scss:27` scopes
     `p.tip { margin-top: 1em }` to `.page-title-box, .card-body`;
     registration_invitation/new's tip is inside `.page-title-box` today and
     loses that selector the moment it moves into `.sa-page-meta`. That is a
     16px shift on a real page, so it is reproduced here rather than
     disclosed as a change — this slice converts markup, it does not restyle.
     `1em` and not a token: it is a copied legacy value, and rounding it to
     --space-4 would be the restyle this slice is avoiding. The `.tip` class
     stays the caller's; slice 2 gives it a component and this rule goes. */
  margin-top: 1em;
}

/* DEPARTURE 2: Hyper hides `.page-title-right` outright below **768px**
   (`@include media-breakpoint-down(sm)`), a workaround for the float +
   85px line-height layout that the v2 adapter already replaced with a
   wrapping flex row. Reproducing it would bake "primary actions vanish on
   phones" — New Registration Type, New Program, New Organization, Export
   CSV — into a brand-new Spruce-owned component and into every page slice
   1b and beyond converts. The actions wrap instead.

   CORRECTED 2026-07-26 (slice 1b group 2, R1 review): this comment and plan
   §Phase 5 both said "below 576px". They were wrong. Bootstrap 4's
   `media-breakpoint-down(sm)` means "sm and DOWN", i.e. up to the *next*
   breakpoint minus 0.02 — it compiles to `@media(max-width:767.98px)`,
   verified in the built `priv/static/css/admin.css`:

     @media(max-width:767.98px){ … .page-title-box .page-title-right{display:none} }

   So the departure is visible on tablets and small laptops too, not only
   phones — a materially wider audience than the original note claimed. */

/* =========================================================================
 * Table scroll region — family 5 opening slice (#609, #574)
 *
 * The narrow-screen owner for wide admin tables: a labelled, keyboard-
 * focusable horizontal scroll region, the same containment contract PR #593
 * proved on the five timing tables with Bootstrap's `.table-responsive` —
 * now Spruce-owned. Scroll containment is chosen over responsive stacked
 * rows (re-decides each table's information hierarchy and destroys table
 * semantics for AT) and over priority-column hiding (silently amputates
 * data). All columns stay truthful; the cut edge of a partially visible
 * column is the continuation affordance.
 *
 * `tabindex="0"` + role/aria-label live in the component markup; keyboard
 * users can reach and arrow-scroll the region, so it needs a visible focus
 * treatment here.
 * ========================================================================= */

.spruce-admin.spruce-admin--v2 div.sa-table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* `width: 100%` is LOAD-BEARING, not a tidy-up. `overflow-x: auto`
     establishes a block formatting context, and a BFC box must not overlap a
     float — so without an explicit width it SHRINKS to sit beside any float
     that precedes it. Admin templates put `float-right` Export/Import/Create
     buttons directly above their tables, which is exactly that shape.
     Bootstrap's `.table-responsive` carried `width: 100%` for this reason;
     dropping it during the family 5 conversion narrowed the registrations
     table from 935px to ~670px and clipped its last two columns. Measured in
     Chromium: overflow-x only = 716px, with width:100% = 935px, legacy
     `.table-responsive` = 935px (#521).
     Caught by the e2e visual baseline — `mix test`, the semantic manifest and
     the page-dump control are all blind to it, because the markup, the hrefs
     and the class names are all correct; only the geometry is wrong. */
  width: 100%;
}

.spruce-admin.spruce-admin--v2 div.sa-table-scroll:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* =========================================================================
 * Row actions — family 5 (#574)
 *
 * Per-row action group: one coherent inline unit that never wraps
 * mid-cell, so inside a scroll region the actions travel as a block
 * instead of stacking into a tall ragged cell. `--space-1` matches the
 * ~4px word-space separation the legacy inline `<span>` markup rendered,
 * so desktop presentation is unchanged (#574 acceptance criterion).
 * ========================================================================= */

.spruce-admin.spruce-admin--v2 div.sa-row-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  white-space: nowrap;
}

/* =========================================================================
 * Buttons, links-as-buttons — family 2 (#521)
 *
 * PARITY BASIS. 305 `.btn` call sites render today as Bootstrap's `.btn`
 * base geometry (built `priv/static/css/admin.css`) with `admin_v2.css`'s
 * tone/typography mapping on top. `.sa-button` carries no `.btn` class, so
 * every one of those declarations has to be restated here — the values
 * below are the measured composite, not a new design. Three deliberate
 * departures are marked DEPARTURE.
 *
 * Legacy composite for the default size:
 *   .btn            display:inline-block; text-align:center; white-space:nowrap;
 *                   vertical-align:middle; user-select:none;
 *                   border:1px solid transparent; padding:.45rem .9rem;
 *                   line-height:1.5
 *   admin_v2 .btn   border-radius:var(--radius-md); font-weight:600;
 *                   font-size:14px; transition:… var(--transition-fast)
 *
 * `.45rem .9rem` and `.28rem .8rem` stay as legacy literals rather than
 * rounding to `--space-*`: they are Hyper's `$input-btn-padding-*`
 * (7.2/14.4px and 4.48/12.8px), and the nearest tokens (8/16px, 4/12px)
 * would resize every button in the admin. This family proves the API; it
 * does not restyle. Same rationale as `.sa-page-meta p.tip`'s `1em`.
 * ========================================================================= */

.spruce-admin.spruce-admin--v2 a.sa-button,
.spruce-admin.spruce-admin--v2 button.sa-button {
  display: inline-block;
  padding: 0.45rem 0.9rem;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-normal);
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  vertical-align: middle;
  cursor: pointer;
  user-select: none;
  /* No `-webkit-appearance: none`. Measured in Chromium against the legacy
     composite: a `.btn` <button> computes `appearance: button`, and resetting
     it here was the one unintended difference the parity harness found. */
  transition: background-color var(--transition-fast), border-color var(--transition-fast),
    color var(--transition-fast), box-shadow var(--transition-fast);
}

/* The anchor form has to beat `admin_v2.css`'s base-surface link rules,
   which are (0,3,1) — `… :is(.sa-main, .up-modal-content, .modal) a` for the
   colour and `… a:hover` (0,3,2) for the underline. The tone rules below are
   (0,5,0) and win on class count; this one ties the colour rule at (0,3,1)
   and wins on source order, and the hover rule below is (0,4,1). */
.spruce-admin.spruce-admin--v2 a.sa-button:hover,
.spruce-admin.spruce-admin--v2 a.sa-button:focus {
  text-decoration: none;
}

.spruce-admin.spruce-admin--v2 .sa-button:disabled,
.spruce-admin.spruce-admin--v2 .sa-button[disabled] {
  /* Bootstrap `.btn.disabled, .btn:disabled { opacity: .65 }`. */
  opacity: 0.65;
  cursor: default;
  pointer-events: none;
}

/* DEPARTURE 1: no tone glow shadow.
   Hyper runs a `button-shadow` mixin over `$theme-colors`, so today
   `.btn-primary` also carries `box-shadow: 0 2px 6px 0 rgba(93,151,50,.5)`,
   `.btn-danger` `rgba(250,92,124,.5)`, `.btn-secondary`
   `hsla(0,0%,45%,.5)` and `.btn-warning` `rgba(255,188,0,.5)` — 116 of the
   305 sites. `admin_v2.css` gave every one of those tones a full treatment
   (background, border, colour, radius, size, transition) and set no shadow,
   so the glow survives only because it is a property the adapter never
   named; the design reference's Button (`design/system/components/core/
   Button.jsx`) has no shadow in any variant. Three of the four literals are
   also pre-v2 palette colours (`#fa5c7c`, `#ffbc00`, 45% grey) that no
   token expresses. Dropped rather than hard-coded back in.

   DEPARTURE 2: `:focus-visible` only, no `:focus` ring.
   Bootstrap's `.btn:focus { box-shadow: 0 0 0 .15rem rgba(93,151,50,.25) }`
   fires on mouse click as well as keyboard focus. `admin_v2.css` already
   supersedes it inside `.sa-main` with a `:focus-visible` rule carrying
   `--focus-ring`; restated here so this stylesheet owns the component's own
   focus treatment rather than inheriting it from the compat layer. */
.spruce-admin.spruce-admin--v2 .sa-button:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* ── sizes ──────────────────────────────────────────────────────────────
   `--md` emits no class; it is the 182-site default rule above.

   `--sm` is Bootstrap's `.btn-sm` padding with admin_v2's 13px/radius-sm.

   `--xs` deliberately keeps the DEFAULT padding. Nothing in the tree
   defines `.btn-xs` geometry — Hyper has no such class, and admin_v2.css's
   only rule is `{ border-radius: var(--radius-sm); font-size: var(--text-xs) }`
   — so the 58 `btn-xs` sites render full-size boxes with 12px labels today.
   Tightening the padding to match the name would shrink 58 controls, which
   is a restyle, not a conversion. Revisit when family 5 settles table
   density. */
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--sm {
  padding: 0.28rem 0.8rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
}

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--xs {
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
}

/* ── tones ──────────────────────────────────────────────────────────────
   Values copied from `admin_v2.css`'s `.btn.btn-*` mappings so a converted
   control is indistinguishable from its unconverted neighbours during the
   ~50-file sweep. */

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--primary {
  background-color: var(--green-500);
  border-color: var(--green-500);
  color: var(--text-on-brand);
}

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--primary:hover,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--primary:focus,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--primary:not(:disabled):active {
  background-color: var(--green-600);
  border-color: var(--green-600);
  color: var(--text-on-brand);
}

/* `btn-secondary` (12 sites) and `btn-outline-secondary` (140) collapse
   here: admin_v2.css already paints them identically — same surface, same
   border, same text, same hover. The only rendered difference was the grey
   glow of DEPARTURE 1. */
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--secondary {
  background-color: var(--surface-card);
  border-color: var(--border-strong);
  color: var(--stone-700);
}

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--secondary:hover,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--secondary:focus,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--secondary:not(:disabled):active {
  background-color: var(--stone-100);
  border-color: var(--border-strong);
  color: var(--stone-700);
}

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--danger {
  background-color: var(--danger);
  border-color: var(--danger);
  color: var(--text-on-brand);
}

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--danger:hover,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--danger:focus,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--danger:not(:disabled):active {
  /* Content-specific literal, carried over verbatim from admin_v2.css's
     `.btn.btn-danger:hover` (which documents it as the same value the
     fundraising `.sa-btn-danger` uses). No token expresses it. */
  background-color: #a93226;
  border-color: #a93226;
  color: var(--text-on-brand);
}

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--warning {
  background-color: var(--warning);
  border-color: var(--warning);
  color: var(--text-on-brand);
}

/* DEPARTURE 3: a hover state for `warning` (2 sites).
   Today there is none: admin_v2.css maps `.btn.btn-warning` at (0,4,0) with
   no `:hover` counterpart, so Bootstrap's `.btn-warning:hover` at (0,2,0)
   loses and the button does not react to the pointer at all. Derived from
   the tone rather than hard-coded so it tracks `--warning`. */
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--warning:hover,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--warning:focus,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--warning:not(:disabled):active {
  background-color: color-mix(in srgb, var(--warning) 88%, #000000);
  border-color: color-mix(in srgb, var(--warning) 88%, #000000);
  color: var(--text-on-brand);
}

/* A button that reads as a link. NOT the design reference's `ghost`
   (transparent + `--stone-600`): admin_v2.css paints `.btn.btn-link` with
   `--text-link`, and 38 sites render that green today. The design's stone
   ghost has zero consumers, so it is not modelled. */
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--link {
  background-color: transparent;
  border-color: transparent;
  color: var(--text-link);
}

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--link:hover,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--link:focus,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--link:not(:disabled):active {
  background-color: transparent;
  border-color: transparent;
  color: var(--green-700);
}

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--outline-primary {
  background-color: transparent;
  border-color: var(--green-500);
  color: var(--green-600);
}

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--outline-primary:hover,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--outline-primary:focus,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--outline-primary:not(:disabled):active {
  background-color: var(--brand-soft);
  border-color: var(--green-500);
  color: var(--green-700);
}

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--outline-danger {
  background-color: transparent;
  border-color: var(--danger);
  color: var(--danger);
}

.spruce-admin.spruce-admin--v2 .sa-button.sa-button--outline-danger:hover,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--outline-danger:focus,
.spruce-admin.spruce-admin--v2 .sa-button.sa-button--outline-danger:not(:disabled):active {
  background-color: var(--danger);
  border-color: var(--danger);
  color: var(--text-on-brand);
}

/* =========================================================================
 * Alert — family 2 (#521)
 *
 * PARITY BASIS: Bootstrap `.alert` geometry with admin_v2.css's tone washes
 * on top. Composite for a danger alert today:
 *   .alert            position:relative; margin-bottom:1rem
 *   admin_v2 .alert   border-radius:var(--radius-md); padding:12px 16px;
 *                     font-size:13px; color:var(--stone-700)
 *   admin_v2 tone     background:var(--danger-bg);
 *                     border:1px solid color-mix(… var(--danger) 25% …)
 * The `strong` rule is the tone's, not an accident: admin_v2.css gives every
 * tone a matching `.alert-<tone> strong { color: … }`.
 * ========================================================================= */

.spruce-admin.spruce-admin--v2 div.sa-alert {
  position: relative;
  margin-bottom: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--stone-700);
}

.spruce-admin.spruce-admin--v2 .sa-alert.sa-alert--danger {
  background-color: var(--danger-bg);
  border-color: color-mix(in srgb, var(--danger) 25%, transparent);
}

.spruce-admin.spruce-admin--v2 .sa-alert--danger strong {
  color: var(--danger);
}

.spruce-admin.spruce-admin--v2 .sa-alert.sa-alert--warning {
  background-color: var(--warning-bg);
  border-color: color-mix(in srgb, var(--warning) 25%, transparent);
}

.spruce-admin.spruce-admin--v2 .sa-alert--warning strong {
  color: var(--warning);
}

.spruce-admin.spruce-admin--v2 .sa-alert.sa-alert--info {
  background-color: var(--info-bg);
  border-color: color-mix(in srgb, var(--info) 25%, transparent);
}

.spruce-admin.spruce-admin--v2 .sa-alert--info strong {
  color: var(--info);
}

.spruce-admin.spruce-admin--v2 .sa-alert.sa-alert--success {
  background-color: var(--success-bg);
  border-color: color-mix(in srgb, var(--success) 25%, transparent);
}

.spruce-admin.spruce-admin--v2 .sa-alert--success strong {
  color: var(--success);
}

/* PARITY, and the second non-`sa-` selector in this file (see the header
   comment). Three admin sites put an `<a class="alert-link">` inside an
   alert; admin_v2.css styles it as
   `… :is(.sa-main, .up-modal-content, .modal) .alert .alert-link`, a
   selector a converted alert no longer matches. The class stays the
   caller's — one shape is not enough consumers to model a component from —
   and family 2 keeps its appearance intact. */
.spruce-admin.spruce-admin--v2 .sa-alert .alert-link {
  color: inherit;
  font-weight: var(--weight-semibold);
  text-decoration: underline;
}

/* =========================================================================
 * Close button — family 2 (#521)
 *
 * PARITY BASIS: Bootstrap `.close` + `button.close` with admin_v2.css's
 * colour mapping. `1.425rem` is Hyper's `$close-font-size`
 * (`$font-size-base * 1.5` = .95rem * 1.5 = 22.8px); `--text-2xl` is 22px,
 * near but not equal, and this glyph's optical size is the one thing a
 * user notices about it, so the legacy literal is kept.
 *
 * DEPARTURE 4: no `float: right`.
 * Bootstrap floats `.close`. Every call site this component serves renders
 * inside `.sa-page-actions`, which is `display: flex` — a flex item's
 * `float` is ignored outright (CSS Display §Blockification), so the
 * declaration is inert at all eight of them. It is dropped rather than
 * copied forward. The two sites where the float context DOES differ are the
 * `.modal-header` closers, which this component deliberately does not serve
 * (see `close_button/1`'s doc).
 * ========================================================================= */

.spruce-admin.spruce-admin--v2 button.sa-close-button {
  padding: 0;
  border: 0;
  background-color: transparent;
  color: var(--stone-400);
  font-size: 1.425rem;
  font-weight: var(--weight-bold);
  line-height: 1;
  text-shadow: none;
  opacity: 1;
  cursor: pointer;
  -webkit-appearance: none;
  transition: color var(--transition-fast);
}

.spruce-admin.spruce-admin--v2 .sa-close-button:hover,
.spruce-admin.spruce-admin--v2 .sa-close-button:focus {
  color: var(--stone-600);
}

.spruce-admin.spruce-admin--v2 .sa-close-button:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-sm);
}

/* =========================================================================
 * Cards, card sections, card labels and stat cards — family 4 (#521)
 *
 * PARITY BASIS. 100 `.card` roots, 98 card bodies and 7 card headers across
 * 45 admin templates render today as Bootstrap's `.card` family plus Hyper's
 * `_card.scss` overrides plus `admin_v2.css`'s mapping. `.sa-card` carries no
 * `.card` class, so the whole composite is restated here. Every value below
 * was MEASURED in Chromium against the legacy markup with all five admin
 * stylesheets loaded in layout order, not derived from the source rules —
 * the same harness family 2 used for `.btn`. Five deliberate departures are
 * marked DEPARTURE.
 *
 * Legacy composite for a plain card:
 *   bootstrap .card       position:relative; display:flex;
 *                         flex-direction:column; min-width:0;
 *                         word-wrap:break-word; background-clip:border-box
 *   hyper     .card       border:none; box-shadow:$shadow; margin-bottom:30px
 *   admin_v2  .card       background:var(--surface-card);
 *                         border:1px solid var(--border-default);
 *                         border-radius:var(--radius-md);
 *                         box-shadow:var(--shadow-sm);
 *                         margin-bottom:var(--space-4)
 *   bootstrap .card-body  flex:1 1 auto
 *   admin_v2  .card-body  padding:var(--space-5)
 *   bootstrap .card-header padding:.75rem 1.5rem; margin-bottom:0
 *   hyper     .card-header margin-top:0
 *   admin_v2  .card-header background:var(--surface-card);
 *                          border-bottom:1px solid var(--border-default)
 *
 * `.widget-flat` (24 card roots) is NOT reproduced and needs no replacement:
 * measured, its only own declaration is `position: relative`, which
 * Bootstrap's `.card` already sets — so on a `.card` it is inert. Its one
 * real effect is enabling the `i.widget-icon` descendant rule, which
 * `stat_card/1` owns as `.sa-stat-card-glyph`. The 20 `card widget-flat`
 * roots that contain no icon are plain cards wearing a class that does
 * nothing.
 * ========================================================================= */

.spruce-admin.spruce-admin--v2 div.sa-card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* Bootstrap's `word-wrap: break-word`; keeps a long unbroken token inside
     the card instead of forcing the grid column wider (#574 / #609). */
  overflow-wrap: break-word;
  background-color: var(--surface-card);
  background-clip: border-box;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  margin-bottom: var(--space-4);
}

/* Guard: a card carrying Bootstrap's `collapse` state class must still start
   hidden. The `display: flex` above at (0,4,1) out-ranks Bootstrap's
   `.collapse:not(.show) { display: none }` at (0,2,0), which rendered the
   membership-group filter popover permanently open (family 4 batch B) — the
   toggle JS flips `.show`, but display never changed. The state class should
   live on a plain wrapper (the filters.html / timeable/show pattern); this
   rule makes the combination safe if a conversion puts it on the card. */
.spruce-admin.spruce-admin--v2 div.sa-card.collapse:not(.show) {
  display: none;
}

.spruce-admin.spruce-admin--v2 div.sa-card-body {
  flex: 1 1 auto;
  padding: var(--space-5);
}

/* PARITY, and the first non-`sa-` selector in this section.
   `admin_v2.css` styles the metric numeral of every card as
   `.card h3 { font-family: display; font-size: --text-3xl;
   font-weight: 700 !important; color: --text-heading }` — a selector a
   converted card no longer matches. Nine converted-family sites render a
   bare `<h3>` inside a card (compare_results' two comparison counts, the
   five dashboard pods, membership_group/index's two card headers), so the
   rule is reproduced rather than dropped.

   The `!important` is the adapter's and is sanctioned by the same rule
   (plan D10): it competes with Bootstrap's `.font-weight-normal
   { font-weight: 400 !important }`, which four of those call sites carry.
   Without it those h3s would render 400 where they render 700 today.
   `stat_card/1`'s own `.sa-stat-card-value` is (0,4,0) and out-ranks this. */
.spruce-admin.spruce-admin--v2 .sa-card h3 {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: var(--weight-bold) !important;
  color: var(--text-heading);
}

/* ── card sections ──────────────────────────────────────────────────────── */

.spruce-admin.spruce-admin--v2 div.sa-card-header {
  margin: 0;
  padding: var(--space-3) var(--space-6);
  background-color: var(--surface-card);
  border-bottom: 1px solid var(--border-default);
}

/* DEPARTURE 5: the header's top corners follow the CARD's radius.
   Bootstrap sets `.card-header:first-child { border-radius:
   calc(.25rem - 1px) … }` from its own `$card-border-radius` of .25rem,
   and `admin_v2.css` moved the card to `--radius-md` (12px) without
   touching it — so today a header renders 3px corners inside a 12px card
   corner. On a white header that is invisible; on the two tone headers
   (`compare_results`) the coloured block visibly overhangs the card's
   rounded corner (screenshot in the family-4 report). `-1px` because the
   header sits inside the card's 1px border. */
.spruce-admin.spruce-admin--v2 div.sa-card-header:first-child {
  border-radius: calc(var(--radius-md) - 1px) calc(var(--radius-md) - 1px) 0 0;
}

.spruce-admin.spruce-admin--v2 .sa-card-header.sa-card-header--danger {
  background-color: var(--danger);
  border-bottom-color: color-mix(in srgb, var(--danger) 25%, transparent);
  color: var(--text-on-brand);
}

.spruce-admin.spruce-admin--v2 .sa-card-header.sa-card-header--success {
  background-color: var(--success);
  border-bottom-color: color-mix(in srgb, var(--success) 25%, transparent);
  color: var(--text-on-brand);
}

/* DEPARTURE 6: a heading inside a tone header is white.
   `admin_v2.css` sets `color: #ffffff` on the tone header, but the
   base-surface rule `… :is(.sa-main, …) :is(h1…h6) { color:
   var(--text-heading) }` is (0,3,1) and the tone header's colour is
   inherited, so the `<h5>` both consumers render computes
   `rgb(33, 31, 28)` on `rgb(192, 57, 43)` — measured 2.9:1, below WCAG AA's
   4.5:1, while the badge and the export button beside it are white. The
   adapter's intent was white (5.8:1); this restores it. */
.spruce-admin.spruce-admin--v2
  .sa-card-header:is(.sa-card-header--danger, .sa-card-header--success)
  :is(h1, h2, h3, h4, h5, h6) {
  color: var(--text-on-brand);
}

/* ── card label ─────────────────────────────────────────────────────────────
   PARITY with `admin_v2.css`'s `.card .header-title` (21 admin sites), the
   small-caps section label at the top of a card body. Selected by tag rather
   than under `.sa-card` so a label still resolves while a route family is
   half converted; (0,3,1) ties the base-surface heading floor and wins on
   source order. `line-height: 1.1` and `letter-spacing: 0.045em` are the
   measured legacy values — `--tracking-caps` is 0.06em and would widen every
   one of the 21. */
.spruce-admin.spruce-admin--v2 h4.sa-card-label,
.spruce-admin.spruce-admin--v2 h5.sa-card-label {
  margin: 0 0 var(--space-2);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  line-height: 1.1;
  letter-spacing: 0.045em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ── card tones ─────────────────────────────────────────────────────────────
   Three values, and the asymmetry between them is the census's, not a design
   choice: `:brand` FILLS (5 sites — `.card.bg-primary`) while `:danger` and
   `:success` only tint the border (2 sites — `.card.border-danger|success` on
   compare_results). A filled danger card and a brand-bordered card both have
   zero consumers and are deliberately not modelled. */

.spruce-admin.spruce-admin--v2 .sa-card.sa-card--danger {
  border-color: var(--danger);
}

.spruce-admin.spruce-admin--v2 .sa-card.sa-card--success {
  border-color: var(--success);
}

/* `border: none` and not `border-color: transparent`: the legacy
   `.card.bg-primary` drops the border outright, so keeping a transparent 1px
   would grow the box by 2px on every brand card. */
.spruce-admin.spruce-admin--v2 .sa-card.sa-card--brand {
  background-color: var(--brand);
  border: none;
  color: var(--text-on-brand);
}

.spruce-admin.spruce-admin--v2 .sa-card.sa-card--brand h3 {
  color: var(--text-on-brand);
}

.spruce-admin.spruce-admin--v2 .sa-card.sa-card--brand .sa-card-label {
  /* The legacy `.card.bg-primary .header-title` literal. 90% white, not
     `--text-on-brand`, so the label still recedes from the numeral. */
  color: rgba(255, 255, 255, 0.9);
}

/* PARITY. Two Bootstrap/Hyper utilities that the brand card's own descendant
   rules repaint in `admin_v2.css` (`.card.bg-primary .text-muted-primary`,
   `… .text-white`, `… .font-13.text-uppercase`) and that a converted card no
   longer matches. `dashboard/pod_member_count` carries all three. The classes
   stay the caller's — they are utilities, and family 4 does not replace
   utilities. Both `!important`s mirror the adapter's, competing with
   `.text-white`/`.font-13`, which are themselves `!important` utilities
   (plan D10). The second selector is (0,6,0) to the first's (0,5,0), which is
   how the legacy pair resolves: the caption reads 82% white, not full. */
.spruce-admin.spruce-admin--v2 .sa-card.sa-card--brand .text-muted-primary,
.spruce-admin.spruce-admin--v2 .sa-card.sa-card--brand .text-white {
  color: var(--text-on-brand) !important;
}

.spruce-admin.spruce-admin--v2 .sa-card.sa-card--brand .font-13.text-uppercase {
  color: rgba(255, 255, 255, 0.82) !important;
}

/* ── ghost controls on a tone surface ───────────────────────────────────────
   THE `btn-outline-light` RESOLUTION (family 2 deferred it here).
   `btn-outline-light` was never a button variant: `admin_v2.css` gives it an
   appearance only through an ancestor — `.card.bg-primary .btn.btn-outline-light`
   and `.card-header.bg-danger|bg-success .btn.btn-outline-light`. Modelling it
   as `AdminUI.button variant={:outline_light}` would have produced a variant
   that renders as invisible white-on-white anywhere else.

   So the tone surface owns it — but as an OPT-IN, not as a property of
   position. A converted call site passes an ordinary family-2 variant
   (`:secondary`, the closest semantic — these are secondary actions) plus the
   `sa-button--on-tone` modifier through the existing `:class` rider:

     <%= Component.link_button [to: …, variant: :secondary, size: :sm,
                                class: "sa-button--on-tone"] do %>…<% end %>

   Three reachable consumers, all converted in this family:
   `dashboard/pod_member_count`'s Details link on a brand card, and
   `membership_group/compare_results`' two Export CSV links on tone headers.
   The fourth legacy site, `admin/session/new`, is unreachable —
   `SessionController` renders the `basic` layout, which does not link this
   stylesheet (`layout_isolation_test` asserts that), so it keeps its
   Bootstrap classes.

   THE MODIFIER IS LOAD-BEARING, NOT DECORATION (R1 review, P2). These rules
   first targeted a bare `.sa-button`, i.e. "any button under a tone
   surface". At (0,5,0) that beat every family-2 variant rule at (0,4,0), so
   a `:danger` button inside a brand card rendered transparent-and-white
   instead of red — and because it is an unrestricted DESCENDANT selector, a
   `:secondary` button inside an untoned card nested beneath a brand card was
   repainted ghost too: white controls on a white surface. The census
   justifies this treatment for exactly three controls; it never justified
   overriding six variants or crossing a nested surface boundary.

   Requiring the modifier fixes both. A variant under a tone surface now
   keeps its own colour, and the nested-card case cannot arise from position
   alone — someone has to have written `sa-button--on-tone` on that specific
   button. What remains, disclosed: a button that DOES opt in while sitting
   on a nested untoned surface still gets the ghost treatment. That is a
   call-site error rather than an ambient trap, and closing it structurally
   would mean a child-combinator chain that none of the three real consumers
   fit (`pod_member_count`'s link is card > body > div > a).

   (0,6,0) still beats the (0,4,0) tone rules and their (0,5,0) hovers
   outright rather than on source order, so a later-added variant cannot
   silently win. `rgba(255,255,255,.5)` is the legacy literal; no token
   expresses "half white", and it must track the surface it sits on, not the
   palette. */
.spruce-admin.spruce-admin--v2 .sa-card.sa-card--brand .sa-button.sa-button--on-tone,
.spruce-admin.spruce-admin--v2
  .sa-card-header.sa-card-header--danger
  .sa-button.sa-button--on-tone,
.spruce-admin.spruce-admin--v2
  .sa-card-header.sa-card-header--success
  .sa-button.sa-button--on-tone {
  background-color: transparent;
  border-color: rgba(255, 255, 255, 0.5);
  color: var(--text-on-brand);
}

.spruce-admin.spruce-admin--v2 .sa-card.sa-card--brand .sa-button.sa-button--on-tone:hover,
.spruce-admin.spruce-admin--v2 .sa-card.sa-card--brand .sa-button.sa-button--on-tone:focus,
.spruce-admin.spruce-admin--v2
  .sa-card-header.sa-card-header--danger
  .sa-button.sa-button--on-tone:hover,
.spruce-admin.spruce-admin--v2
  .sa-card-header.sa-card-header--danger
  .sa-button.sa-button--on-tone:focus,
.spruce-admin.spruce-admin--v2
  .sa-card-header.sa-card-header--success
  .sa-button.sa-button--on-tone:hover,
.spruce-admin.spruce-admin--v2
  .sa-card-header.sa-card-header--success
  .sa-button.sa-button--on-tone:focus {
  background-color: rgba(255, 255, 255, 0.12);
  border-color: rgba(255, 255, 255, 0.5);
  color: var(--text-on-brand);
}

/* ── stat card ──────────────────────────────────────────────────────────────
   PARITY BASIS: six admin sites render the same label/value/hint composite
   inside a card body, four of them with the `.card.bg-primary` brand
   treatment applied conditionally. The geometry below is the measured legacy
   composite of `h5.font-weight-normal.mt-0.text-muted` + `h3.mt-3.mb-3` +
   `p.mb-0.text-muted` and is reproduced literally where no token matches:
   `15px` is Hyper's `$h5-font-size` (.9375rem), `1.1` its
   `$headings-line-height`, `10px` its `$headings-margin-bottom`. Rounding
   them to `--text-lg`/`--leading-tight`/`--space-3` would resize every stat
   card in the admin; this family proves the API, it does not restyle.
   (Same rationale as family 2's `.45rem .9rem` button padding.) */

.spruce-admin.spruce-admin--v2 .sa-stat-card .sa-stat-card-icon {
  /* Legacy `<div class="float-right">`. A flex row would be the better
     layout, but it changes the vertical rhythm of the label/value stack —
     that is a restyle, and belongs to whoever redesigns the metric tile. */
  float: right;
}

/* DEPARTURE 7: the untoned icon chip uses the v2 neutral tone pair.
   Hyper paints `.widget-flat i.widget-icon` `#727272` on
   `hsla(0, 0%, 45%, .1)` — pre-v2 palette greys that no token expresses, and
   the one variant `admin_v2.css` left behind when it gave the four
   `.widget-icon-<tone>` chips token pairs. `--stone-600` on `--stone-100` is
   the adapter's OWN neutral pair (its `.widget-icon-secondary` mapping), so
   the chip stops being the only one still on the old palette. Three sites:
   `order/index`'s non-brand rows and `dashboard/admin_dashboard`'s two
   tiles. Geometry (40px box, 20px glyph, 3px radius) is unchanged — the
   departure is colour only. `3px` stays a literal: `--radius-sm` is 8px and
   would visibly round a 40px chip. */
.spruce-admin.spruce-admin--v2 .sa-stat-card .sa-stat-card-glyph {
  display: inline-block;
  width: 40px;
  height: 40px;
  line-height: 40px;
  text-align: center;
  font-size: 20px;
  border-radius: 3px;
  color: var(--stone-600);
  background-color: var(--stone-100);
}

.spruce-admin.spruce-admin--v2 .sa-stat-card .sa-stat-card-label {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: var(--weight-regular);
  line-height: 1.1;
  color: var(--text-muted);
}

.spruce-admin.spruce-admin--v2 .sa-stat-card .sa-stat-card-value {
  margin: var(--space-6) 0;
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: var(--weight-bold);
  line-height: 1.1;
  color: var(--text-heading);
}

.spruce-admin.spruce-admin--v2 .sa-stat-card .sa-stat-card-hint {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--text-md);
  font-weight: var(--weight-regular);
  line-height: var(--leading-normal);
  color: var(--text-muted);
}

/* On brand, the legacy markup swaps `.text-muted` for `.text-muted-primary`,
   which `admin_v2.css` maps to full `--text-on-brand` — the "muted" label is
   not muted at all on a brand card. The component owns that swap so the four
   conditional `if row.label == "Payments", do: "text-muted-primary", else:
   "text-muted"` expressions disappear from the call sites. (0,5,0) so it
   beats the (0,4,0) base above regardless of source order. The value's white
   comes from `.sa-card--brand h3` above. */
.spruce-admin.spruce-admin--v2 .sa-stat-card.sa-card--brand .sa-stat-card-label,
.spruce-admin.spruce-admin--v2 .sa-stat-card.sa-card--brand .sa-stat-card-hint {
  color: var(--text-on-brand);
}

/* =========================================================================
 * Badge — family 5 (#521)
 *
 * PARITY BASIS: measured in Chromium against the real admin stack
 * (admin.css + admin_tokens + admin_shell + admin_v2 + admin_ui, in layout
 * order) rather than read off the source. Every value below reproduces a
 * measured computed property of the legacy `.badge.badge-<tone>` it replaces.
 *
 * Selected under the `:is(.sa-main, .up-modal-content, .modal)` context that
 * admin_v2.css already uses for every badge TONE rule, and — for the tone
 * rules — with the root class REPEATED (`.sa-badge.sa-badge--secondary`),
 * exactly as admin_v2.css writes `.badge.badge-secondary-lighten` and as
 * family 2 writes `.sa-alert.sa-alert--info`. Both are load-bearing, and the
 * second one was found by measurement, not reasoning:
 *
 *   the base surface carries
 *     … :is(.sa-main, …) a:hover { color: var(--green-700) }   (0,4,1)
 *   so a single-class tone rule at (0,4,0) LOSES it, and the census's one
 *   anchor badge (membership_group/index's member-source chips) turned green
 *   on hover while the legacy `.badge.badge-secondary-lighten` at (0,5,0)
 *   held its stone tone. Caught by a real-pointer hover in the parity
 *   harness; the resting-state diff was clean, which is why family 2's rule
 *   about measuring BOTH states exists.
 *
 * The doubled class also clears the file's (0,3,1) floor without a tag
 * qualifier, which this component cannot carry — its host element is span,
 * div OR a.
 *
 * `badge-pill` has no counterpart here because it is INERT under v2, not
 * because it was dropped: Bootstrap's `.badge-pill` is `(0,1,0)` and loses
 * both its `border-radius: 10rem` and its `padding: .25em .6em` to
 * `.spruce-admin.spruce-admin--v2 .badge` at `(0,3,0)`. Measured: identical
 * border-radius (999px), padding (1px 10px) and width (38.3281px) with and
 * without the class.
 * ========================================================================= */

.spruce-admin.spruce-admin--v2 :is(.sa-main, .up-modal-content, .modal) .sa-badge {
  display: inline-block;
  border-radius: var(--radius-full);
  /* 1px 10px is admin_v2.css's own badge geometry, not Bootstrap's
     `.25em .4em`; it is already token-free there and stays literal here so
     the two are diffable. */
  padding: 1px 10px;
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  line-height: 1.4;
  text-align: center;
  white-space: nowrap;
  vertical-align: baseline;
}

.spruce-admin.spruce-admin--v2 :is(.sa-main, .up-modal-content, .modal) .sa-badge.sa-badge--primary {
  background-color: var(--brand-soft);
  color: var(--brand);
}

.spruce-admin.spruce-admin--v2 :is(.sa-main, .up-modal-content, .modal) .sa-badge.sa-badge--secondary {
  background-color: var(--stone-100);
  color: var(--stone-600);
}

.spruce-admin.spruce-admin--v2 :is(.sa-main, .up-modal-content, .modal) .sa-badge.sa-badge--success {
  background-color: var(--success-bg);
  color: var(--success);
}

.spruce-admin.spruce-admin--v2 :is(.sa-main, .up-modal-content, .modal) .sa-badge.sa-badge--danger {
  background-color: var(--danger-bg);
  color: var(--danger);
}

.spruce-admin.spruce-admin--v2 :is(.sa-main, .up-modal-content, .modal) .sa-badge.sa-badge--warning {
  background-color: var(--warning-bg);
  color: var(--warning);
}

.spruce-admin.spruce-admin--v2 :is(.sa-main, .up-modal-content, .modal) .sa-badge.sa-badge--info {
  background-color: var(--info-bg);
  color: var(--info);
}

/* `:light` is NOT a duplicate of `:secondary`. --stone-50 against
   --stone-100 — measured rgb(250,250,248) vs rgb(244,244,240). The two
   `badge-light` sites pair it with `text-success` / `text-danger`
   utilities, which are `!important` in admin_v2.css and still win. */
.spruce-admin.spruce-admin--v2 :is(.sa-main, .up-modal-content, .modal) .sa-badge.sa-badge--light {
  background-color: var(--stone-50);
  color: var(--stone-600);
}

/* Bootstrap gives `a.badge` a hover underline through the base link rules and
   admin_v2.css suppresses it with `a:is(.btn, .badge):hover`, a selector a
   converted badge no longer matches. The one anchor consumer has no `href`,
   so this is belt-and-braces — but the component permits `tag="a"`, and a
   later caller adding an href must not get an underlined chip. */
.spruce-admin.spruce-admin--v2 :is(.sa-main, .up-modal-content, .modal) a.sa-badge:hover,
.spruce-admin.spruce-admin--v2 :is(.sa-main, .up-modal-content, .modal) a.sa-badge:focus {
  text-decoration: none;
}

/* DEPARTURE 5: no stray 4px top margin on the `:warning` tone.
 * Historically `admin_shell.css` carried `.spruce-admin .badge-warning-lighten`
 * for the topbar DEMO org-status chip (`Format.organization_status/1`) with
 * NO context qualifier, so it leaked `margin-top: var(--space-1)` onto every
 * in-page warning badge. MEASURED: legacy `.badge.badge-warning-lighten`
 * computed `margin-top: 4px`; every other tone, including plain
 * `.badge-warning`, computed `0px`.
 *
 * That shell rule is now scoped to
 * `.spruce-admin .sa-context .sa-badge.sa-badge--warning` (shell chrome only).
 * `.sa-badge--warning` under `.sa-main` does not reproduce the 4px top margin
 * — in-page warning badges align with other tones. The shell DEMO chip keeps
 * its chrome geometry via the narrowed shell selector.
 * ========================================================================= */

/* =========================================================================
 * Table — family 5 (#521), design Table.jsx
 *
 * PARITY BASIS: measured, same harness as the badge above. The legacy stack
 * paints an admin `<table class="table">` from four places — Bootstrap's
 * `.table`, Hyper's `$table-cell-padding: .95rem`, admin.scss, and
 * admin_v2.css's design-token contract — and the values below are the
 * composed result, not any one of those sources.
 *
 * `table_scroll/1` (#645) still owns narrow-screen containment; this styles
 * the table only. `.table-responsive*` is therefore NOT reproduced here.
 *
 * Specificity: the root carries a `table` tag qualifier to reach the file's
 * (0,3,1) floor, exactly as `header.sa-page-header` does; parts are selected
 * under the root and clear it outright.
 *
 * CONTAINMENT vs DROPDOWN ESCAPE (#675 / kebab hit-test) — a real tension.
 *
 * Absolute descendants only clip against an ancestor's `overflow` when that
 * ancestor is in their *containing-block* chain. Tailwind `.sr-only` is
 * `position:absolute` with no `left`/`inset`, so the 1px box lands at its
 * static position (measured x≈505 inside a ~589px-wide table) and escapes
 * `div.sa-table-scroll` (`overflow-x: auto`), inflating
 * `document.documentElement.scrollWidth` at 375px (~506).
 *
 * Making `table.sa-table { position: relative }` puts the table in that
 * chain and contains the `.sr-only` nodes — but it also contains kebab
 * `.dropdown-menu` descendants that *must* escape the scroll region to be
 * hittable (`e2e/17` one-row program kebab). Interceptor: with the table as
 * CB, `elementFromPoint` at the last item's centre hits `.content-page.sa-main`
 * instead of the menu item.
 *
 * Resolution: do NOT make the table a containing block. Pin table `.sr-only`
 * nodes with `left: 0` / `inset-inline-start: 0` so the 1px box cannot sit at
 * x≈505 regardless of which ancestor is the CB. Dropdowns keep escaping;
 * document scrollWidth stays equal to clientWidth.
 *
 * Mutation detectors (rewrite priv/static/css/admin_ui.css + .gz + digest —
 * Plug.Static gzip:true + digests hide asset-only edits):
 *   A) delete the `.sr-only` left/inset pin → ExUnit pin reds AND
 *      e2e/16 fundraising scrollWidth goes 375→~506 (e2e/17 registerables
 *      scrollWidth stays green — those tables have no absolute `.sr-only`).
 *   B) re-add `position: relative` on `table.sa-table` → e2e/17 kebab
 *      `lastItemHittable` reds (hit `.content-page.sa-main`).
 * ========================================================================= */

.spruce-admin.spruce-admin--v2 table.sa-table {
  width: 100%;
  margin-bottom: var(--space-6);
  background: var(--surface-card);
  color: var(--stone-700);
  font-size: var(--text-sm);
}

/* #675: pin absolute .sr-only boxes to the inline-start of their containing
   block so they cannot inflate document scrollWidth. Do not solve this by
   making the table `position: relative` — that clips kebab menus (above). */
.spruce-admin.spruce-admin--v2 table.sa-table .sr-only {
  left: 0;
  inset-inline-start: 0;
}

/* 0.95rem is Hyper's `$table-cell-padding`, kept as the literal it is in the
   theme rather than rounded to a spacing token: --space-4 is 16px and the
   measured value is 15.2px, and cell padding is the one dimension that
   changes every row height on every admin table. Same call as the close
   button's `1.425rem` in family 2. */
.spruce-admin.spruce-admin--v2 .sa-table th,
.spruce-admin.spruce-admin--v2 .sa-table td {
  padding: 0.95rem;
  vertical-align: top;
  border-top: none;
  border-bottom: 1px solid var(--border-default);
  font-variant-numeric: tabular-nums;
}

.spruce-admin.spruce-admin--v2 .sa-table thead th {
  background: var(--surface-sunken);
  color: var(--stone-600);
  font-weight: var(--weight-semibold);
  font-size: var(--text-xs);
  vertical-align: bottom;
  /* Bootstrap's reboot resets `th { text-align: inherit }` on the ELEMENT,
     so a converted table keeps its left-aligned headers without this — but
     the UA default is `center`, and one deleted reboot away that is a
     silent, tree-wide regression on every admin table header. Asserted
     rather than assumed. */
  text-align: inherit;
  border-top: none;
  border-bottom: 1px solid var(--border-default);
}

.spruce-admin.spruce-admin--v2 .sa-table tbody tr:last-child td {
  border-bottom: none;
}

/* ── modifiers ──────────────────────────────────────────────────────────
   Placed after the base rules: `.sa-table--centered th` (0,3,1) ties
   `.sa-table th` (0,3,1) and wins on source order, which is how the legacy
   `!important` on `.table-centered` is retired rather than copied. */

.spruce-admin.spruce-admin--v2 .sa-table--striped tbody tr:nth-of-type(odd) {
  background-color: var(--stone-50);
}

.spruce-admin.spruce-admin--v2 .sa-table--hover tbody tr:hover {
  background-color: var(--surface-sunken);
  color: var(--stone-700);
}

/* DEPARTURE 6: `vertical-align: middle` without `!important`.
 * Hyper's `_tables.scss` writes `.table-centered th, td { vertical-align:
 * middle !important }` because `.table-centered th` (0,2,1) merely TIES
 * Bootstrap's `.table th` (0,2,1) and its own `thead th` rule at (0,2,2)
 * BEATS it. Under `sa-` the tie is resolved by source order and the
 * `thead th` case is spelled out explicitly below, so the escape hatch is
 * not needed. MEASURED identical: legacy and semantic `thead th` and `td`
 * both compute `vertical-align: middle`. Dropping it matters because an
 * `!important` here would out-rank a later per-page override. */
.spruce-admin.spruce-admin--v2 .sa-table--centered th,
.spruce-admin.spruce-admin--v2 .sa-table--centered td,
.spruce-admin.spruce-admin--v2 .sa-table--centered thead th {
  vertical-align: middle;
}

.spruce-admin.spruce-admin--v2 .sa-table--sm th,
.spruce-admin.spruce-admin--v2 .sa-table--sm td {
  padding: var(--space-2);
}

/* Left/right only, and the frame on the table itself. The legacy
   `.table-bordered th, td { border: 1px solid }` sets all four edges and is
   then overridden on the vertical axis by admin_v2.css's later `.table td,
   .table th` rule — measured: a bordered cell computes border-top 0 and
   border-bottom from the shared contract, never from `.table-bordered`.
   Writing only the axis that survives removes the ordering dependency. */
.spruce-admin.spruce-admin--v2 table.sa-table--bordered {
  border: 1px solid var(--border-default);
}

.spruce-admin.spruce-admin--v2 .sa-table--bordered th,
.spruce-admin.spruce-admin--v2 .sa-table--bordered td {
  /* `border-color` on all four edges, widths on the horizontal axis only.
     The vertical edges are zero-width here, so their colour paints nothing
     today — but the first parity run diffed on exactly that
     (`border-top-color` falling back to `currentColor`, stone-700, instead
     of `--border-default`), and a later per-page rule that adds a top width
     would then inherit the wrong colour. Matching it costs one declaration. */
  border-color: var(--border-default);
  border-left-width: 1px;
  border-left-style: solid;
  border-right-width: 1px;
  border-right-style: solid;
}

/* =========================================================================
 * Form fields — family 3 (#521)
 *
 * PARITY BASIS. Two populations render the same field group today:
 * `SpruceWeb.InputHelpers` calls (128 admin sites) and hand-rolled
 * `<div class="form-group">` blocks (385, of which 218 are the canonical
 * label + one control + error_tag shape). Both render
 * `.form-group` / `.control-label` / `.form-control` / `.custom-control` /
 * `.text-muted` / `.help-block`, whose appearance comes from Bootstrap 4 +
 * Hyper in the compiled `admin.css` as remapped by admin_v2.css §3.
 *
 * Every declaration below was MEASURED in Chromium against a legacy /
 * semantic sibling pair rendered under `.spruce-admin.spruce-admin--v2
 * .sa-main` with the real stylesheet stack in layout order, in the resting,
 * :focus, :hover and invalid states. Four DEPARTUREs are marked at their
 * rules; everything else is computed-style identical.
 * ========================================================================= */

/* Field wrapper ← `.form-group` (Bootstrap: margin-bottom 1rem). */
.spruce-admin.spruce-admin--v2 div.sa-field {
  margin-bottom: var(--space-4);
}

/* Field label ← admin_v2.css §3 `.form-group label:not(.form-check-label)`.
   Bootstrap's `label { display:inline-block; margin-bottom:.5rem }` supplies
   the display; the v2 mapping supplies everything else. Restated in full
   here because a converted field matches neither selector any more. */
.spruce-admin.spruce-admin--v2 .sa-field .sa-field-label {
  display: inline-block;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--stone-700);
  margin-bottom: 6px;
}

/* Control ← Bootstrap `.form-control` geometry + admin_v2.css §3's Input
   contract. The literals are Hyper's compiled `.form-control` values, kept
   as literals rather than approximated with tokens: `--space-2`/`--space-3`
   are 8px/12px against Hyper's .45rem/.9rem = 7.2px/14.4px, and the control
   height is what a form's vertical rhythm is built on. */
.spruce-admin.spruce-admin--v2 .sa-field .sa-field-control {
  display: block;
  width: 100%;
  /* `calc(2px + 2.325rem)` is Hyper's compiled `$input-height` literal, read
     off `.form-control` in the built admin.css. Deriving it as
     `calc(1.5em + 0.9rem + 2px)` — Bootstrap's own formula — computes 37.4px
     here, not 39.2px, because admin_v2.css re-declares the control's
     font-size as 14px while Hyper's `em` was resolved against the .95rem
     base. The literal is 1.8px taller and is what every admin form's
     vertical rhythm is actually built on. */
  height: calc(2px + 2.325rem);
  padding: 0.45rem 0.9rem;
  line-height: 1.5;
  background-clip: padding-box;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  font-size: 14px;
  font-family: var(--font-body);
  color: var(--stone-800);
  background-color: var(--surface-card);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.spruce-admin.spruce-admin--v2 .sa-field textarea.sa-field-control {
  height: auto;
}

.spruce-admin.spruce-admin--v2 .sa-field .sa-field-control:focus {
  border-color: var(--border-focus);
  box-shadow: var(--focus-ring);
  color: var(--stone-800);
  background-color: var(--surface-card);
  outline: 0;
}

/* No `:disabled` background. Bootstrap's `.form-control:disabled` sets
   `background-color: #e9ecef`, but admin_v2.css's `.form-control` mapping is
   (0,3,0) against its (0,2,0) and re-declares `background-color:
   var(--surface-card)` — so a disabled admin input is white TODAY, and the
   four `disabled:` call sites in this family look exactly like enabled ones.
   Painting them here would be an unrequested visible change on pages this
   family is only converting; measured, then deliberately not reproduced.
   Filed as a follow-up rather than smuggled into a conversion slice. */

.spruce-admin.spruce-admin--v2 .sa-field .sa-field-control::placeholder {
  color: var(--text-muted);
  opacity: 1;
}

/* DEPARTURE 1: a field with server-side errors gets a danger border.
   Bootstrap paints `.form-control.is-invalid`, and admin_v2.css maps it to
   `border-color: var(--danger)` — but NOTHING in the admin ever adds that
   class. `ErrorHelpers.error_tag/2` renders the message and leaves the
   control untouched, so today a field can carry "can't be blank" beneath a
   control that looks exactly like a valid one. `field/1` knows it has
   errors (it renders them), so it also sets `aria-invalid="true"`, and this
   rule paints from that attribute.
   MEASURED, `membership_group[name]` with a "can't be blank" error:
   before border-color rgb(214, 212, 204) → after rgb(192, 57, 43); every
   other computed property identical. Nothing else changes: geometry, type
   and the focus treatment are shared with the resting rule above. */
.spruce-admin.spruce-admin--v2 .sa-field .sa-field-control[aria-invalid="true"] {
  border-color: var(--danger);
}

/* Checkbox control + label ← `.custom-control.custom-checkbox`'s
   `.custom-control-input` / `.custom-control-label`.
   DEPARTURE 2: a native checkbox, not Bootstrap's ::before/::after box.
   The legacy control is a visually-hidden `<input>` with the box drawn as a
   pseudo-element on the label, positioned by `.custom-control`'s
   `padding-left: 1.5rem` + `position: relative`. admin_v2.css already had
   to restate FIVE separate rules to make that box legible (resting border,
   checked, disabled, disabled+checked, focus) because Bootstrap 4.1's
   ::before carries no border at all. `accent-color` gets the same green-500
   checked state from the platform control in one declaration, is what
   admin_v2.css §3 already does for `.form-check-input`, and keeps the hit
   target a real checkbox for AT and for the platform's own high-contrast
   handling.
   MEASURED at 100% and 200% zoom, resting/checked/disabled/focus: the box
   is 13x13 native vs Bootstrap's 16x16 drawn box, and sits on the text
   baseline rather than 0.25rem below the line box top. Everything else —
   label type ramp, gap, checked fill — matches. This is the one visible
   pixel change in the family and it is deliberate: it removes 5 compat
   rules and a pseudo-element from the component's contract. */
.spruce-admin.spruce-admin--v2 div.sa-field--checkbox {
  display: flex;
  align-items: baseline;
  /* `.custom-control` carries NO bottom margin and `min-height: 1.425rem`.
     Consecutive checkboxes therefore stack tight today — 12 of the 44 sites
     are runs of 2-6 in a row (registerable/form, registerable/live_form,
     organization/form) — so inheriting `.sa-field`'s 16px would visibly
     spread every one of them. Pages that want the gap already wrap their
     checkboxes in their own spacing div, and a converted call site can pass
     `wrapper_class: "mb-3"`. */
  margin-bottom: 0;
  min-height: 1.425rem;
  /* Column gap only: the hint and the errors wrap onto their own rows and
     carry their own top margin, exactly as they do under a legacy
     `.custom-control`. A symmetric `gap` adds 8px of dead space there. */
  column-gap: var(--space-2);
  row-gap: 0;
  flex-wrap: wrap;
}

.spruce-admin.spruce-admin--v2 .sa-field--checkbox .sa-field-checkbox {
  accent-color: var(--green-500);
  width: 13px;
  height: 13px;
  margin: 0;
  flex: none;
}

.spruce-admin.spruce-admin--v2 .sa-field--checkbox .sa-field-checkbox-label {
  margin: 0;
  font-size: 14px;
  /* MEASURED, not designed: Hyper's global `label { font-weight: 600 }`
     reaches `.custom-control-label`, and admin_v2.css's `.form-check-label`
     mapping sets size and colour but not weight — so an admin checkbox
     label is semibold stone-700 today, in both the custom-control and the
     form-check spellings. The design reference calls for body text here;
     reproducing what ships is the family's job, and re-deciding it is a
     separate, page-wide call. */
  font-weight: var(--weight-semibold);
  color: var(--stone-700);
}

/* A DISABLED checkbox's label must read as disabled (#728).
 *
 * This is a real gap in the family, found by converting `edit_mfa`'s
 * "Text message" row, which is `disabled:` until a phone number is verified.
 * Bootstrap ships `.form-check-input:disabled ~ .form-check-label { color:
 * #aaaaaa }`; the family had no equivalent, so a converted disabled checkbox
 * kept its ordinary `--stone-700` label. The box was still natively disabled
 * and unclickable — it just stopped LOOKING it, which is the worse of the two
 * failure directions: the user sees an ordinary option that silently ignores
 * them.
 *
 * It is not specific to that page. Every `disabled:` checkbox any conversion
 * touches has the same gap, which is why the rule lives with the family rather
 * than as a page override.
 *
 * BLAST RADIUS TODAY IS ONE CONTROL, enumerated in R2 review: `edit_mfa`'s
 * `mfa_sms_enabled` is the only `Component.field :checkbox` passing `disabled:`
 * anywhere in the app, and no `Component.checkbox` site passes it either. So
 * "family-wide" describes where the rule BELONGS, not how much it changes now —
 * nothing outside this diff renders differently.
 *
 * `--stone-400` (#aeaba1) is the token nearest Bootstrap's literal #aaaaaa;
 * the family's rule is token-only values, so the literal is not reproduced. */
.spruce-admin.spruce-admin--v2 .sa-field--checkbox .sa-field-checkbox:disabled ~ .sa-field-checkbox-label {
  color: var(--stone-400);
}

.spruce-admin.spruce-admin--v2 .sa-field--checkbox .sa-field-checkbox:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: 3px;
}

/* The hint and the errors are full-width rows under a flex checkbox field,
   INDENTED to the label's left edge. Bootstrap's `.custom-control` is
   `position: relative; padding-left: 1.5rem` with the box absolutely
   positioned in that gutter, so a legacy hint sits under the LABEL, not
   under the checkbox. The flex layout here has no gutter, so without this
   the hint started at the checkbox edge — a layout change nobody declared
   (R1 P2). 21px is the label's actual left edge: the 13px native control
   plus the 8px column gap. It differs from Bootstrap's 24px because the
   control is 13px rather than the 16px drawn box (DEPARTURE 2), so aligning
   to the label is the faithful reading rather than copying the literal. */
.spruce-admin.spruce-admin--v2 .sa-field--checkbox .sa-field-hint,
.spruce-admin.spruce-admin--v2 .sa-field--checkbox .sa-field-error {
  flex-basis: 100%;
  padding-left: calc(13px + var(--space-2));
}

/* Hint ← `InputHelpers.hint/1`'s `<div class="text-muted">`, which inherits
   the body size and takes its colour from admin_v2.css's `.text-muted`. */
.spruce-admin.spruce-admin--v2 .sa-field .sa-field-hint {
  display: block;
  color: var(--text-muted);
}

/* Error ← `ErrorHelpers.error_tag/2`'s `<span class="help-block">`, mapped
   by admin_v2.css §3 to 12px/danger with a 5px top margin. */
.spruce-admin.spruce-admin--v2 .sa-field .sa-field-error {
  display: block;
  font-size: 12px;
  /* Hyper's `.help-block` is `font-weight: 700`; admin_v2.css remaps the
     size and colour but not the weight, so admin validation messages are
     bold today. Reproduced rather than quietly lightened. */
  font-weight: var(--weight-bold);
  margin-top: 5px;
  color: var(--danger);
}

/* Datetime composite ← `.form-inline.datetime-select-with-timezone`.
   Bootstrap's `.form-inline` is `display:flex; flex-flow:row wrap;
   align-items:center`, and its `.form-inline .form-control` override is
   what makes the six part-selects inline and auto-width instead of the
   block/100% they would otherwise be. Both are reproduced here so the
   composite no longer depends on a Bootstrap layout class. */
.spruce-admin.spruce-admin--v2 .sa-field .sa-field-datetime {
  display: flex;
  flex-flow: row wrap;
  align-items: center;
}

.spruce-admin.spruce-admin--v2 .sa-field .sa-field-datetime-part {
  display: inline-block;
  width: auto;
  vertical-align: middle;
  /* `.datetime-select-with-timezone select { margin-right: 6px }` in the
     compiled admin.css is the composite's entire inter-part spacing: both
     wrappers are flex, so the literal whitespace between the six selects
     produces no anonymous flex items in either. */
  margin-right: 6px;
  height: calc(2px + 2.325rem);
  padding: 0.45rem 0.9rem;
  line-height: 1.5;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  font-size: 14px;
  font-family: var(--font-body);
  color: var(--stone-800);
  background-color: var(--surface-card);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.spruce-admin.spruce-admin--v2 .sa-field .sa-field-datetime-part:focus {
  border-color: var(--border-focus);
  box-shadow: var(--focus-ring);
  outline: 0;
}

.spruce-admin.spruce-admin--v2 .sa-field .sa-field-datetime-zone {
  /* The legacy zone label is a bare `<span>` inheriting the surface text
     colour, measured as stone-700. Declared rather than left to `inherit`
     so the class has an owning rule in the registry. */
  color: var(--stone-700);
}

/* NOTHING here owns `:date` / `:datetime_stacked`, and that is deliberate.
   Those two types emit `Phoenix.HTML.Form`'s stacked builders verbatim, and
   MEASURED (render probe, phoenix_html 3.3.4) those builders drop the
   `class:` every legacy call site passes — so the parts are class-less
   UA-native selects today and stay class-less after conversion. Giving them
   `.sa-field-control` would be a restyle, which this extraction explicitly
   is not (Bruce, 2026-07-28). See `AdminUI.field_types/0`. */

/* Colour swatch ← `card_design/edit`'s inline
   `max-width: 4rem; height: 2.5rem; padding: 0.15rem` on its `type="color"`.
   The base `.sa-field-control` rule supplies border, radius, focus ring and
   the shared 39.2px height so the swatch lines up with the text controls
   around it; only the geometry a full-width text box would impose is
   overridden. `width` rather than `max-width`: the base rule sets
   `width: 100%`, which `max-width` alone would not defeat. */
.spruce-admin.spruce-admin--v2 .sa-field .sa-field-control--color {
  width: 4rem;
  padding: 0.15rem;
}

/* Mono hex boxes — organization/form's three theme colour fields (#727),
   carried over from admin_v2.css §14's `.form-control.color-picker`. Hex
   values are code-like, so they take contract §1.3's mono role; the class is
   a call-site ride-along passed through `field/1`'s `:class`, the same
   convention `:wrapper_class` uses for `float-right`/`mb-0`, NOT a component
   modifier — nothing in `AdminUI` emits it and no JS binds it (grepped
   `assets/`: the only consumer was ever this font-family rule).

   It has to live here rather than in admin_v2.css because that file may not
   declare a class admin_ui.css owns (plan §7, pinned by admin_ui_test's
   "no other stylesheet declares a registered class"). Specificity: the base
   `.sa-field .sa-field-control` rule sets `font-family: var(--font-body)` at
   (0,4,0), so the extra class token — (0,5,0) — is what makes this win. */
.spruce-admin.spruce-admin--v2 .sa-field .sa-field-control.color-picker {
  font-family: var(--font-mono);
}

/* =========================================================================
 * Input groups — `field/1`'s `:prefix` / `:suffix` (#521 family 3)
 *
 * Reproduces Bootstrap 4.1's `.input-group` + `.input-group-text` fused
 * look, read off the COMPILED `priv/static/css/admin.css` at this branch's
 * start commit, with the chip skin admin_v2.css §4 already maps
 * (`.input-group-text, .input-group-addon` → surface-sunken / border-strong
 * / text-muted / text-sm / radius-md).
 *
 * Two things are NOT copied, both because the legacy rules key off classes a
 * converted field no longer carries:
 *
 *   - the `.input-group-prepend` / `.input-group-append` wrapper divs and
 *     their `margin-right: -1px` / `margin-left: -1px`. `sa-input-group`
 *     puts the chip in the flex row directly, so the overlap belongs on the
 *     items themselves;
 *   - `.input-group > .form-control:not(:last-child)`. A converted control
 *     is `.sa-field-control`, so the legacy wrapper would have produced an
 *     UNFUSED group — two fully rounded rectangles touching. That is why
 *     input groups were out of scope until this family owned the joins.
 *
 * SPECIFICITY, stated rather than left to source order (the `.table` /
 * `.signup-sheet-table` incident, AGENTS.md): the base control rule
 * `.spruce-admin.spruce-admin--v2 .sa-field .sa-field-control` is (0,4,0)
 * and sets `display`, `width` and the `border-radius` shorthand. Every rule
 * below that has to beat it names the element class explicitly and adds a
 * pseudo-class, landing at (0,5,0) — so these win on specificity and not on
 * being written later in the file.
 * ========================================================================= */

.spruce-admin.spruce-admin--v2 .sa-field .sa-input-group {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

/* Bootstrap's `.input-group > .form-control` verbatim. `width: 1%` with
   `flex: 1 1 auto` is its idiom for "take the leftover room": the 1% basis
   keeps the control from claiming intrinsic width and starving the chip.
   `min-width: 0` is added — without it a long `placeholder` sets a min
   content width and the control refuses to shrink below it, which Bootstrap
   gets away with only because these groups are wide. */
.spruce-admin.spruce-admin--v2 .sa-field .sa-input-group > .sa-field-control {
  position: relative;
  flex: 1 1 auto;
  width: 1%;
  min-width: 0;
  margin-bottom: 0;
}

/* The focus ring must paint OVER the neighbour it overlaps by -1px,
   otherwise the chip's border draws on top of it along the shared edge.
   Bootstrap uses `z-index: 3` on the same rule for the same reason. */
.spruce-admin.spruce-admin--v2 .sa-field .sa-input-group > .sa-field-control:focus {
  z-index: 3;
}

/* Scoped through `.sa-field` to reach (0,4,0). At (0,3,0) this sat BELOW the
   compat base surface's (0,3,1) floor and would have lost its typography to
   admin_v2.css — caught by `layout_isolation_test`'s specificity floor, not
   by any render assertion. `field/1` is the only emitter of this class, so
   the extra ancestor costs nothing. */
.spruce-admin.spruce-admin--v2 .sa-field .sa-input-group-text {
  display: flex;
  align-items: center;
  padding: 0.45rem 0.9rem;
  margin-bottom: 0;
  line-height: 1.5;
  text-align: center;
  white-space: nowrap;
  background-color: var(--surface-sunken);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-family: var(--font-body);
  font-weight: 400;
}

/* The joins. `:is()` takes the highest specificity of its arguments — one
   class — so each of these is (0,3,0) + (0,1,0) + (0,1,0) = (0,5,0), which
   clears the (0,4,0) base control rule and the (0,3,0) chip rule above
   without depending on order. `.sa-button` is in the list for the search
   shape, whose suffix is a submit button rather than a chip. */
.spruce-admin.spruce-admin--v2
  .sa-input-group
  > :is(.sa-field-control, .sa-input-group-text, .sa-button):not(:last-child) {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

.spruce-admin.spruce-admin--v2
  .sa-input-group
  > :is(.sa-field-control, .sa-input-group-text, .sa-button):not(:first-child) {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  /* Collapse the doubled border along the seam. Bootstrap spends three
     selectors on this because its chips live in wrapper divs; with a flat
     flex row one rule covers every adjacency. */
  margin-left: -1px;
}

/* =========================================================================
 * Family 6 — JS-coupled interactives (#521)
 *
 * ONE RULE, and the reason there is only one is the family's finding.
 *
 * `pagination/1`, `nav_tabs/1`, `dropdown/1` and `dropdown_item/1` emit the
 * legacy Bootstrap contract classes verbatim and declare no `sa-` class of
 * their own, so they contribute nothing here. Three independent reasons,
 * each verified rather than assumed:
 *
 *   1. the class name IS the JS contract — `bootstrap@4.1.3`'s
 *      `dropdown.js` finds its menu with `.dropdown-menu` and its keyboard
 *      targets with `.dropdown-menu .dropdown-item`, and `modal.js` finds
 *      its dialog with `.modal-dialog`;
 *   2. the class name IS the e2e contract — `.dot-dropper`,
 *      `.dropdown-menu.show`, `a.dropdown-item`, `a.page-link`,
 *      `a.nav-link.active` and `.modal-title` are selected on by the
 *      Playwright suite, which no conversion gate runs;
 *   3. `admin_v2.css` already maps every one of `.pagination`/`.page-item`/
 *      `.page-link`, `.dropdown-menu`/`.dropdown-item`/`.dropdown-divider`,
 *      `.nav-tabs`/`.nav-pills`/`.nav-bordered .nav-link` and the whole
 *      `.modal-*` set to v2 tokens. A second owner here would be a
 *      duplicate, not an improvement.
 *
 * What IS left over is the skip family 2 handed forward: the two
 * `data-dismiss="modal"` closers in `revenue_breakdown/index` and
 * `autorenewal/index`. Family 2 declined them because their positioning
 * comes from Bootstrap's `.modal-header .close`, which the `sa-` class name
 * escapes. That rule is reproduced below so those two can convert.
 * ========================================================================= */

/* Bootstrap 4.1.3 `bootstrap.css:5448`:
 *
 *     .modal-header .close { padding: 1rem; margin: -1rem -1rem -1rem auto }
 *
 * reproduced verbatim for the renamed class. `.modal-header` is
 * `display: flex; justify-content: space-between`, so the bare `.close`
 * rule's `float: right` is inert on this element and is deliberately NOT
 * carried over — the `margin-left: auto` is what puts the glyph at the
 * trailing edge, and `.sa-close-button` never had the float.
 *
 * (0,4,0), which clears the (0,3,1) floor and out-ranks the base
 * `button.sa-close-button` rule at (0,3,1) that it is adjusting.
 *
 * MEASURED IN CHROMIUM, both markups side by side under this exact
 * stylesheet stack (admin.css → admin_tokens → admin_shell → admin_v2 →
 * admin_ui), not reasoned:
 *
 *   RESTING — identical on every property and every geometry value:
 *     box 46.83 x 54.8, padding 16px, margin -16px/-16px/-16px/auto,
 *     8px gap to the header's right edge, 2px to its top, header height
 *     57.8px, colour rgb(174,171,161), font 22.8px/700.
 *
 *   WITHOUT this rule the converted button measures 14.83 x 22.8 with
 *   padding 0, margin 0 and a 24px right gap — i.e. the rule was proven
 *   load-bearing by measuring the tree with it removed, not assumed.
 *
 *   HOVER / FOCUS differ, and that difference is family 2's already-shipped
 *   `.sa-close-button` treatment rather than anything new here — it is
 *   DEPARTURE 9, and it makes these two closers match the eight `up-close`
 *   closers family 2 already converted:
 *
 *     hover  legacy rgb(174,171,161) (no hover feedback at all)
 *            ->  sa- rgb(100,97,91)
 *     focus  legacy rgb(0,0,0) at opacity .75, square corners
 *            ->  sa- rgb(100,97,91) at opacity 1, 8px radius
 *            (the focus ring itself, rgba(93,151,50,.3) 0 0 0 3px, is
 *             identical on both) */
.spruce-admin.spruce-admin--v2 .modal-header .sa-close-button {
  padding: 1rem;
  margin: -1rem -1rem -1rem auto;
}

/* =========================================================================
 * Auth-shell companions — the `basic` layout's dark account pages (#728)
 *
 * `layout/basic.html.eex` renders sign-in, the two-step challenge,
 * forgot-password and reset-password. Those four screens are the ONLY place a
 * semantic component sits on a dark surface, and until #728 they were skinned
 * through the Bootstrap spellings the conversion removes:
 *
 *   .admin-login-card .login-form .form-control   (admin_v2.css)
 *   .account-pages    .form-check-label           (admin_v2.css)
 *
 * A converted field carries neither, so without these two rules the sign-in
 * inputs lose the card's translucent border and height, and the "Remember this
 * browser" label renders stone-700 — dark grey on a dark card, effectively
 * invisible. Both were MEASURED in the browser before and after, not inferred.
 *
 * WHY THEY LIVE HERE AND NOT IN admin_v2.css. Plan §7 gives every registered
 * `sa-` class exactly one stylesheet owner, and `admin_ui_test`'s "no other
 * stylesheet declares a registered class" enforces it — restating
 * `.sa-field-control` in admin_v2.css goes red. The adapter rules above are
 * deliberately left in place rather than deleted: nothing emits their class
 * pairs on these four screens any more, but deleting adapter rules is a
 * separate call (§7.1 sunset), and the reward here is zero.
 *
 * SPECIFICITY. The base component rules are
 * `.spruce-admin.spruce-admin--v2 .sa-field .sa-field-control` and
 * `… .sa-field--checkbox .sa-field-checkbox-label`, both (0,4,0). Each rule
 * below adds one page-scope class for (0,5,0), so it wins on specificity
 * rather than on source order. Both are above the (0,3,1) floor
 * `layout_isolation_test` enforces and carry the required
 * `.spruce-admin.spruce-admin--v2` prefix.
 *
 * The page-scope ancestors (`.admin-login-card`, `.account-pages`) exist only
 * on the auth shell, so no admin content page can pick these up. */
.spruce-admin.spruce-admin--v2 .admin-login-card .login-form .sa-field-control {
  min-height: calc(var(--space-8) + var(--space-3));
  margin-bottom: 0;
  border: 1px solid color-mix(in srgb, var(--text-on-brand) 20%, transparent);
  border-radius: var(--radius-sm);
}

.spruce-admin.spruce-admin--v2 .account-pages .sa-field--checkbox .sa-field-checkbox-label {
  color: color-mix(in srgb, var(--text-on-brand) 80%, transparent);
}
