/*
 * Spirit of Christmas -- base styles.
 * Every color/spacing value here is a CSS custom property from themes.css
 * (Section 7: no hardcoded values, no inline styles anywhere in the app).
 * Layout is responsive-first and touch-friendly -- volunteers use this on
 * a tablet on the warehouse floor (Section 15).
 */

/* Not theme-varying (unlike themes.css's :root, which redefines its own
   set of tokens per [data-theme]) -- this stays the same value regardless
   of color theme, so it lives in its own :root block here rather than
   being repeated in every theme block over there.
   --touch-target-size (2026-09-07): the one shared size for every
   per-section collapse/expand icon site-wide (Households report and Audit
   listing's household-group toggles, Audit's own per-fieldset section
   toggles) -- 44px, confirmed with the project owner rather than guessed,
   matching Apple's Human Interface Guidelines minimum touch target and
   comfortably tablet-friendly for volunteers on the warehouse floor
   (Section 15). Deliberately NOT applied to any "Expand All"/"Collapse
   All" control that toggles a whole page's sections at once (e.g.
   review/_audit-phase-child.php's ⏬/⏫ buttons) -- only a per-section
   toggle counts for this rule. (A later round tried reusing this same
   value as the Expand All/Collapse All button's own height too, for
   visual consistency between the two kinds of control -- superseded by a
   follow-up asking to match Print/Donor's own height in the same header
   instead, further down, rather than this touch-target value.)
   --icon-button-fill-ratio (2026-09-07): how large an icon glyph renders
   relative to the height of the button containing it, for any icon-only
   button that wants the icon to visually dominate the button rather than
   float in a lot of unused padding -- 0.7 (70%), confirmed exactly with
   the project owner after two earlier values (0.72, then 0.648) each
   read wrong once seen live. A ratio, not a fixed px value, so it stays
   correct if the containing button's own size ever changes (e.g. a
   differently-sized icon button elsewhere reusing this same property)
   rather than needing its own hand-tuned pixel figure. */
:root {
  --touch-target-size: 44px;
  --icon-button-fill-ratio: 0.7;
}

* {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.6;
  background: var(--color-bg);
  color: var(--color-fg);
}

.wrap {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

a {
  color: var(--color-link);
  text-decoration: none;
}
a:hover {
  text-decoration: underline;
}

/* A <button> styled to read exactly like the <a> rules above -- for a
   table cell that opens a modal (PreStage's Child # column) rather than
   navigating, where a real <a href> would be misleading. */
.link-button {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--color-link);
  text-decoration: none;
  cursor: pointer;
}
.link-button:hover {
  text-decoration: underline;
}

/* Group-by-household banded rows (Audit Phase 2/Final tabs, display-
   technique revision 2026-09-05) -- the header row names the group and
   toggles it; .household-band tints every member row so the block reads
   as one unit even before you've looked closely at who's in it. */
.household-group-header td {
  background: var(--color-bg-inset);
  font-weight: 600;
  padding-top: 0.4rem;
  padding-bottom: 0.4rem;
}
.household-toggle-btn {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  margin-right: 0.4rem;
}
.household-toggle-btn[aria-expanded="false"] .household-toggle-icon {
  transform: rotate(-90deg);
}
.household-group-header .badge {
  /* 2026-09-06: the household-wide Audit Status badge sits right after the
     header's own link, in the same cell -- a small explicit gap reads more
     deliberate than relying on the single collapsed whitespace character
     between the two inline elements, same reasoning as .household-toggle-btn's
     own margin-right just above. */
  margin-left: 0.5rem;
}
.household-band {
  background: var(--color-accent-bg);
}
.household-band td:first-child {
  /* Indented under the group header, tree-view style, so a member row
     reads as subordinate to its household rather than a peer of the
     singleton rows around it -- purely horizontal, so it doesn't touch
     row height/CLAUDE.md's shared th,td vertical-padding convention. */
  padding-left: 2rem;
}

/* Collapsible sections on the Audit detail pages' 7 fieldsets
   (templates/review/_audit-form-fields.php, 2026-09-06) -- expanded by
   default, per-child/per-section toggle state (see that file's own
   docblock on $sectionOpen/$sectionClose). Same chevron-rotation mechanic
   as .household-toggle-btn above, kept as its own class rather than reused
   directly since it's a different UI context (a form section, not a
   listing-page row group) -- a future maintainer grepping for one
   shouldn't land in the other. */
.section-toggle-btn {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  margin-right: 0.4rem;
}
.section-toggle-btn[aria-expanded="false"] .section-toggle-icon {
  transform: rotate(-90deg);
}

/* Site-wide per-section collapse/expand icon size (2026-09-07) -- every
   toggle whose own header controls just that one section (Households/
   Audit household-group rows above, Audit's own per-fieldset sections
   here) shares this one rule/one value (--touch-target-size, defined at
   the top of this file) rather than each having its own hardcoded
   font-size. Deliberately excludes review/_audit-phase-child.php's
   .section-expand-all-btn/.section-collapse-all-btn (⏬/⏫) -- those
   toggle every section on the page at once and were explicitly asked to
   stay as-is, not a per-section toggle this rule is about. line-height: 1
   keeps the enlarged glyph from inflating the button's box beyond the
   font-size itself; vertical-align: middle (on both *-toggle-btn rules
   above) keeps it centered against the adjacent inline text instead of
   baseline-aligned, which would look off once the icon is this much
   bigger than the surrounding type. */
.household-toggle-icon,
.section-toggle-icon {
  font-size: var(--touch-target-size);
  line-height: 1;
}

/* review/_audit-phase-child.php's own Expand All/Collapse All buttons
   (2026-09-07, a later round than the per-section rule above, which
   still deliberately excludes these two): the glyph read as too small
   against a lot of unused button padding. First tried sizing the button
   itself to --touch-target-size (44px, matching the per-section icons
   above) -- superseded by this version, asked for once that first one
   was live: same height as the Print/Donor buttons in the same header
   specifically, not the touch-target value. Both are plain .btn,
   whose min-height is --button-height, so they match Print's height
   exactly and stay an exact match if it ever changes. Only the icon's
   own font-size is set here, to --icon-button-fill-ratio (70%) of
   --button-height. line-height: 1 keeps the enlarged glyph from inflating the
   button past that same 36px. Combined selector (not just .btn-icon,
   which several other, unrelated icon buttons across the app also use)
   so this sizing stays scoped to just these two; combined with .btn
   specifically (not a bare class) since .btn-icon's own font-size: 1rem
   (defined later in this file, same specificity as a bare
   .section-expand-all-btn) would otherwise win regardless of source
   order. */
.btn.section-expand-all-btn,
.btn.section-collapse-all-btn {
  height: var(--button-height);
  font-size: calc(var(--button-height) * var(--icon-button-fill-ratio));
  line-height: 1;
  /* Bug fix (2026-09-07): these buttons' direct parent is a plain
     display: block wrapper (not .page-heading's own flex row directly),
     so it's ordinary inline layout -- and *not* flexbox align-items --
     governing how each inline-level button aligns against its
     siblings (Print/Donor/the status badge/Delete) on the same line.
     Confirmed live before guessing: align-self (a flexbox-only
     property) had zero effect here no matter the value, since the
     actual parent isn't a flex container. vertical-align: middle is the
     correct property for this situation -- ordinary inline boxes
     default to vertical-align: baseline, which (like flexbox's own
     baseline alignment) aligns by each element's text/content baseline
     rather than its box edges, and an icon-only button's baseline sits
     wherever the browser puts the emoji glyph's own baseline, not the
     same position as a text button's baseline even at an identical
     36px height -- confirmed live this, not the height itself, was why
     these two buttons rendered ~4px higher than Print/Donor despite
     already matching their height exactly. */
  vertical-align: middle;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.02em;
  margin: 0 0 0.6rem;
}
h1 { font-size: 1.9rem; }
h2 { font-size: 1.35rem; }
h3 { font-size: 1.1rem; }

/* Visible focus indicator on every interactive element (Section 15: WCAG 2.1 AA) */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--color-focus-ring);
  outline-offset: 2px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--color-primary);
  color: var(--color-primary-fg);
  padding: 0.75rem 1rem;
  z-index: 100;
}
.skip-link:focus {
  left: 0.5rem;
  top: 0.5rem;
}

/* ---------- Header / nav ---------- */

.app-header {
  background: var(--brand-teal-color);
  color: var(--color-header-fg);
  padding: 0 1.25rem;
  min-height: 58px;
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: wrap;
  position: sticky;
  top: 0;
  z-index: 900;
  box-shadow: var(--shadow-md);
  border-bottom: 3px solid var(--color-header-bg);
}

.app-header__brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-decoration: none;
  color: #000000;
  text-shadow: 0 0 6px #ffffff, 0 0 12px #ffffff;
  white-space: nowrap;
  padding: 0.6rem 0;
}
.app-header__brand:hover {
  text-decoration: none;
}
/* Matches .login-logo span's accent color for "Christmas" on the login page. */
.app-header__brand-accent {
  color: var(--color-accent);
}

/* The logo file has its own white canvas baked in (not transparent), so it
   gets a fixed-white chip to sit on regardless of theme -- every other
   surface in this app is theme-adaptive (and dark in half the themes),
   which would otherwise clip or clash with the logo's own background. */
.brand-logo-chip {
  display: block;
  background: #ffffff;
  border: 1px solid var(--brand-ink-color);
  border-radius: var(--radius-sm);
  padding: 3px 8px;
  line-height: 0;
}
.brand-logo-chip img {
  display: block;
  height: 30px;
  width: auto;
}
.brand-logo-chip--lg {
  padding: 8px 18px;
  border-radius: var(--radius-md);
  margin: 0 auto 1rem;
  width: fit-content;
}
.brand-logo-chip--lg img {
  height: 52px;
}

/* Active program year badge -- a fixed light chip (like .brand-logo-chip)
   rather than the dark #theme-select/.user-menu__btn treatment, so it reads
   distinctly against those and stays legible regardless of which of the 8
   header colors is active. Text/border colors are fixed, not theme-driven
   (--color-fg/--color-header-fg would flip to light-on-dark in dark
   themes, wrong for a permanently white chip). */
.app-header__program-year {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  background: #ffffff;
  border: 1px solid rgba(0, 0, 0, 0.2);
  border-radius: var(--radius-sm);
  padding: 0.25rem 0.85rem;
  line-height: 1.15;
}
.app-header__program-year-label {
  font-family: var(--font-display);
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--brand-ink-color);
}
.app-header__program-year-value {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 800;
  color: #000000;
}

.app-nav {
  display: flex;
  gap: 2px;
  flex: 1;
  flex-wrap: wrap;
}

/* Hamburger toggle for .app-nav -- hidden here, shown only under the
   max-width: 768px rule below where .app-nav itself collapses to a panel. */
.nav-toggle-btn {
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.15);
  border: 1px solid rgba(255, 255, 255, 0.3);
  color: #ffffff;
  font-size: 1.25rem;
  line-height: 1;
  padding: 0.4rem 0.7rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.nav-toggle-btn:hover {
  background: rgba(255, 255, 255, 0.25);
}

/* Solid white, not the old per-theme translucent white -- the header
   background is a fixed --brand-teal-color in every theme now, so one
   plain, fully-opaque color reads clearly against it everywhere. */
.app-nav a {
  font-family: var(--font-display);
  font-size: 0.92rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: #ffffff;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-sm);
  transition: background 0.15s ease, color 0.15s ease;
}

.app-nav a:hover {
  color: var(--color-nav-highlight);
  background: rgba(255, 255, 255, 0.1);
  text-decoration: none;
}

/* Current-page highlight is a fixed dark gray in every theme (not
   theme-driven like hover above), so it reads the same regardless of which
   of the 8 header colors is active. */
.app-nav a[aria-current="page"] {
  color: var(--color-nav-highlight);
  background: #333333;
  text-decoration: none;
}

/* v0.6.5: the Donors menu (Manage Donors / Assign Donors / Sponsor Requests). The button looks exactly like the other nav entries; the panel
   below it is a light card like the user menu's, with its links in the page's normal type instead of the nav's white capitals. */
.app-nav__group {
  position: relative;
  display: flex;
}
.app-nav__group-btn {
  font-family: var(--font-display);
  font-size: 0.92rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: #ffffff;
  background: none;
  border: none;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
/* The arrow after "Donors" (the owner, 2026-10-16: "make the dropdown icon bigger"): about half again the nav's own letter size, with a line-height of
   1 so the bigger glyph does not make the nav row taller. */
.app-nav__caret {
  display: inline-block;
  font-size: 1.6em;
  line-height: 1;
  vertical-align: -0.12em;
  margin-left: 0.1rem;
}
.app-nav__group-btn:hover,
.app-nav__group-btn[aria-expanded="true"] {
  color: var(--color-nav-highlight);
  background: rgba(255, 255, 255, 0.1);
}
.app-nav__group-btn[data-current="true"] {
  color: var(--color-nav-highlight);
  background: #333333;
}
.app-nav__submenu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  min-width: 13rem;
  flex-direction: column;
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  padding: 5px;
  display: none;
  z-index: 950;
}
.app-nav__submenu.is-open {
  display: flex;
}
.app-nav .app-nav__submenu a {
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  color: var(--color-fg);
  padding: 0.5rem 0.75rem;
  text-align: left;
  white-space: nowrap;
}
.app-nav .app-nav__submenu a:hover {
  color: var(--color-fg);
  background: var(--color-bg-inset);
}
.app-nav .app-nav__submenu a[aria-current="page"] {
  color: var(--color-nav-highlight);
  background: #333333;
}

.app-header__user {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-left: auto;
}

/* Theme select -- kept a fixed very-light-gray regardless of the header's
   own background (Header is --brand-teal-color; theme-driven text/border
   colors would flip to light-on-light in some themes, wrong for a
   permanently light control), matching the .user-menu__btn treatment below. */
.theme-select-wrap {
  position: relative;
}
/* appearance: none below strips the native dropdown arrow entirely, so this
   stands in for it -- sized to sit in the padding-right the select already
   reserves, and non-interactive so clicks still reach the select under it. */
.theme-select-wrap::after {
  content: '\25BE';
  position: absolute;
  top: 50%;
  right: 0.65rem;
  transform: translateY(-50%);
  color: #333333;
  font-size: 0.75rem;
  pointer-events: none;
}
#theme-select {
  appearance: none;
  width: auto;
  height: 37.6px; /* matches .user-menu__btn's rendered height, which is set by its 26px avatar circle */
  min-height: 0; /* overrides the generic select{} 44px touch-target floor below so height above actually wins */
  background: #d9d9d9;
  border: 1px solid rgba(0, 0, 0, 0.2);
  color: #1a1a1a;
  font-family: var(--font-body);
  font-size: 0.8rem;
  padding: 0.35rem 1.6rem 0.35rem 0.6rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
#theme-select option {
  background: #d9d9d9;
  color: #1a1a1a;
}

/* User menu (avatar + nickname, dropdown with sign out) */
.user-menu {
  position: relative;
}
.user-menu__btn {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: #d9d9d9;
  border: 1px solid rgba(0, 0, 0, 0.2);
  color: #1a1a1a;
  font-family: var(--font-body);
  font-size: 0.82rem;
  padding: 0.3rem 0.7rem 0.3rem 0.3rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.user-menu__btn:hover {
  background: #c9c9c9;
}
.user-menu__avatar {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--color-accent);
  color: #fff;
  font-family: var(--font-display);
  font-size: 0.85rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.user-menu__dropdown {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 190px;
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  padding: 5px;
  display: none;
  z-index: 950;
}
.user-menu__dropdown.is-open {
  display: block;
}
.user-menu__dropdown-label {
  padding: 6px 10px 4px;
  font-size: 0.78rem;
  color: var(--color-fg-muted);
  white-space: nowrap;
}
.user-menu__dropdown hr {
  border: none;
  border-top: 1px solid var(--color-border);
  margin: 4px 0;
}
.user-menu__dropdown button,
.user-menu__dropdown a {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 0;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  border: none;
  background: none;
  color: var(--color-fg);
  font-size: 0.85rem;
  font-weight: 500;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.user-menu__dropdown button:hover,
.user-menu__dropdown a:hover {
  background: var(--color-bg-inset);
}

main {
  flex: 1;
  /* 1300px + 1in (96px) per side -- widened so the working area (tables,
     forms) makes better use of a typical 14"+ laptop screen instead of
     leaving the same amount of empty margin on both sides regardless of
     window width. */
  max-width: 1492px;
  width: 100%;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 1.5rem;
}

.page-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 0.75rem;
}

.page-heading h1 {
  margin: 0;
}

.app-footer {
  text-align: center;
  padding: 1.25rem;
  background: var(--brand-teal-color);
  color: var(--color-header-fg);
  font-size: 0.82rem;
  border-top: 1px solid var(--color-border);
}
.app-footer__version {
  opacity: 0.75;
}

/* ---------- Buttons & form controls ----------
   Every button is one size, --button-height (36px), wherever it sits: a form, a window, a toolbar, a filter bar, a table row (v0.6.5, the
   owner's "make all the buttons the same height"; there used to be three sizes, .btn 44px, .btn-sm 36px and .btn-row about 20px). Text fields,
   selects and the like keep their 44px touch-target minimum (Section 15), and so does the full-width .btn-full call-to-action of the public
   Sponsor Request pages and the sign-in card. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  min-height: var(--button-height);
  padding: 0.35rem 0.75rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-bg-elevated);
  color: var(--color-fg);
  font-family: var(--font-body);
  font-size: 0.85rem;
  font-weight: 600;
  /* v0.6.5: an explicit line-height. A <button> defaults to "normal" (about 1.2) but an <a> inherits the page's 1.5, so an
     <a class="btn"> stood taller than a <button class="btn"> beside it (Assign next to Edit and Delete in a Sponsor Requests
     row; Clear filters next to Search). The same value on both makes every .btn the same height, and an emoji in the label no longer
     stretches the line. */
  line-height: 1.2;
  cursor: pointer;
  text-decoration: none;
  transition: background 0.15s ease, filter 0.15s ease;
}

.btn:hover {
  filter: brightness(0.97);
  text-decoration: none;
}

.btn-primary {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-primary-fg);
}
.btn-primary:hover {
  background: var(--color-primary-hover);
  border-color: var(--color-primary-hover);
  filter: none;
}

.btn-danger {
  background: var(--color-danger);
  border-color: var(--color-danger);
  color: #ffffff;
}

/* Admin > Audit Log's "View" button -- fixed black/white regardless of
   theme, per the project owner's explicit request, not a themed token. */
.audit-details-btn {
  background: #000000;
  border-color: #000000;
  color: #ffffff;
}
.audit-details-btn:hover {
  background: #262626;
  border-color: #262626;
  filter: none;
}

/* Icon-only variant: a symbol with a .visually-hidden label, for the few places that are really a toggle (Review's Expand all / Collapse all).
   Square-ish padding instead of the wide text padding. The filter bars' buttons are NOT icon-only any more: see .btn-filter. */
.btn-icon {
  padding: 0.35rem 0.6rem;
  font-size: 1rem;
}

/* The plus sign at the start of every Add button (the owner, 2026-10-16: "a very distinct white so it is very clear what it is"). It was the colour
   emoji ➕, which draws as a dull purple-grey plus whatever the button's own colours are; now a plain bold plus, larger than the label, in white
   (--color-plus-sign) with a thin dark edge (--color-plus-sign-outline) so it also stands out on the Dark theme's light-blue buttons. Every Add button is a
   .btn-primary, and the glyph is hidden from screen readers (the label says Add). */
.btn-plus {
  color: var(--color-plus-sign);
  font-size: 1.5em;
  font-weight: 800;
  line-height: 1;
  text-shadow: 0 0 1px var(--color-plus-sign-outline), 0 0 2px var(--color-plus-sign-outline);
}

/* The two buttons that end every filter bar ("Filter" and "Clear", workflow/_filter-buttons.php): labelled, and WHITE (the owner, 2026-10-16) in
   every theme -- a fixed colour, not the theme's own surface, like the Done button's yellow, so they read the same wherever they sit. The four
   tokens are in themes.css, one set per theme. Dark text, because a white button on the Dark theme must not inherit that theme's light text. */
.btn-filter {
  background: var(--color-filter-button-bg);
  border-color: var(--color-filter-button-border);
  color: var(--color-filter-button-fg);
}
.btn-filter:hover {
  background: var(--color-filter-button-hover-bg);
  filter: none;
}

.btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.btn-full {
  width: 100%;
  /* The one exception to --button-height: a full-width call-to-action (the public Sponsor Request pages, which donors use on a phone, and the
     sign-in card) keeps the 44px touch-target minimum. */
  min-height: 44px;
}

label {
  display: block;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-fg-muted);
  margin-bottom: 0.3rem;
}

input[type="text"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="search"],
input[type="email"],
input[type="tel"],
select,
textarea {
  width: 100%;
  min-height: 44px;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg-elevated);
  color: var(--color-fg);
  font-size: 1rem;
  font-family: var(--font-body);
}

input:focus, select:focus, textarea:focus {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-bg);
  outline: none;
}

textarea {
  min-height: 80px;
}

.field {
  margin-bottom: 1rem;
}
.field-inline {
  margin-bottom: 0;
}

/* Puts two or more .field elements side by side in a stacked form (e.g. a
   modal) -- unlike .inline-form, which shrinks every control to its own
   content width, a plain .field-row child keeps filling its share of the
   row (flex: 1) so First/Last name or City split evenly. Give a field
   .field-row__fixed when it should stay its input's own natural width
   instead (e.g. Zip next to City) -- pair that with .input-narrow/
   .input-medium on the input itself to size it. */
.field-row {
  display: flex;
  gap: 0.75rem;
}
.field-row .field {
  flex: 1 1 0;
  min-width: 0;
}
.field-row .field-row__fixed {
  flex: 0 0 auto;
}

/* Donor Add/Edit modal: tighter vertical rhythm than the site-wide form
   defaults above, scoped to #donor-modal only (not a global .field/input
   change) since this is the one form dense enough -- 7 rows across 11
   fields -- that showing more of it at once without scrolling is worth
   trading some breathing room for. */
#donor-modal .modal-hdr {
  padding: 0.6rem 1.25rem;
}
#donor-modal .modal-body {
  padding: 0.75rem 1.25rem;
}
#donor-modal .modal-ftr {
  padding: 0.5rem 1.25rem;
}
#donor-modal .field-hint {
  margin: 0 0 0.5rem;
}
#donor-modal .field {
  margin-bottom: 0.5rem;
}
#donor-modal label {
  margin-bottom: 0.1rem;
}
#donor-modal input[type="text"],
#donor-modal input[type="email"],
#donor-modal select {
  min-height: 34px;
  padding: 0.3rem 0.6rem;
}
#donor-modal textarea {
  min-height: 44px;
  padding: 0.3rem 0.6rem;
}
#donor-modal .alert {
  margin-bottom: 0.5rem;
}
#donor-modal .field-error {
  margin-top: 0.15rem;
}
/* A message shown under a whole row of fields (see donor-grid.js showDonorFieldErrors()). */
#donor-modal .field-row + .field-error {
  margin: -0.25rem 0 0.5rem;
}
/* The corrected address the address service suggests, shown above the buttons that accept or decline it. */
#donor-modal .donor-address-choice__suggested {
  font-weight: 600;
  margin: 0.25rem 0 0.5rem;
}

.field-hint {
  font-size: 0.8rem;
  color: var(--color-fg-subtle);
  margin-top: 0.25rem;
}

.checkbox-field {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.checkbox-field input {
  width: 22px;
  height: 22px;
  min-height: 0;
  accent-color: var(--color-accent);
}
.checkbox-field label {
  margin: 0;
  text-transform: none;
  font-weight: 500;
}

/* Groups OR SIMILAR + Requires assembly (audit page, per-wish row) onto one
   line with a vertical rule between them, instead of each .checkbox-field
   stacking as its own block. */
.checkbox-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.checkbox-row .checkbox-field + .checkbox-field {
  padding-left: 0.75rem;
  border-left: 1px solid var(--color-border);
}

/* Wish Item Pricing Enhancement -- sits in the same .checkbox-row as the
   Or Similar/Requires Attention checkboxes (review/_audit-form-fields.php);
   .checkbox-row's own gap already provides spacing, so no positioning rules
   are needed here. */
.wish-price-estimate,
.wish-price-unavailable {
  font-size: 0.85rem;
  white-space: nowrap;
}
.wish-price-unavailable {
  color: var(--color-fg-subtle);
  font-style: italic;
}

/* Ethnicity checkboxes (audit page): fixed 3-column grid so the 6 options
   lay out as 3 across, 2 down rather than each .checkbox-field stacking as
   its own block. */
.checkbox-grid-3col {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.4rem 0.75rem;
}

/* Stacks a handful of .checkbox-inline labels vertically, left-aligned, in
   one table cell (PreStage's Received/Requires assembly pair) instead of
   .inline-form's default of laying every child out in a single row. */
.checkbox-stack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
}

.checkbox-inline {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.85rem;
  font-weight: 500;
  white-space: nowrap;
}
.checkbox-inline input {
  width: 20px;
  height: 20px;
  min-height: 0;
  accent-color: var(--color-accent);
}

.radio-inline {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-weight: 500;
  font-size: 0.85rem;
}
.radio-inline input {
  width: auto;
  height: auto;
  min-height: 0;
  accent-color: var(--color-accent);
}

/* The Accept / Reject choice on the batch-review page (templates/upload/batch.php): a fieldset with no box of its own, its two radio buttons stacked
   (no visible title: the fieldset is named for screen readers by aria-label). Neither radio is checked until the operator chooses. */
.decision-choice {
  border: none;
  padding: 0;
  margin: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
/* The Action column is as wide as its longest choice and the choices never wrap (the owner, 2026-10-26: "ACCEPT -- IMPORT FILE (USE ONLY FIRST INITIAL)" must stay
   on one line, and it is fine for the Status column, whose text can wrap, to give way instead). The table's cells are nowrap already; the label says so itself. */
.decision-choice .radio-inline {
  white-space: nowrap;
}

fieldset {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 1rem;
  margin: 0 0 1.25rem;
}

/* A <fieldset disabled> used purely for its native behavior of disabling
   every descendant form control at once (audit.php's read-only-when-
   program-year-closed wrapper) -- not an actual visual grouping, so it
   shouldn't get fieldset's own border/padding/margin box on top of the
   real fieldsets already nested inside it. */
.fieldset-plain {
  border: none;
  padding: 0;
  margin: 0;
}

/* Separates the Pant/Shirt/Shoe groupings within the Sizes fieldset (audit
   page) -- plain <hr>, no border/box-model surprises to work around. */
.field-group-divider {
  border: none;
  border-top: 1px solid var(--color-border);
  margin: 0 0 1rem;
}

/* Audit page's Siblings fieldset: query container (not the viewport) so
   the 2-column grid below responds to the fieldset's own available width
   -- it lives in .grid-2col's left (form) column, which is a hard 50% of
   the page (see that rule's comment), so what matters is this column's
   width, not the window's. Same technique .pdf-frame-wrap already uses
   for the Source PDF sizing. */
.siblings-fieldset {
  container-type: inline-size;
}
/* 2 columns x 4 rows for its fixed 8 slots (ChildRepository::
   loadSiblingsForReview() is always padded out to 8 in the template)
   rather than one slot per row -- down to 1 column below the container
   query threshold, where even First name's 15ch floor (see
   .sibling-cell__fields below) plus Child # can't fit two across without
   the right column running under the Source PDF panel.
   minmax(0, 1fr), not bare 1fr: same fix as .grid-2col above and for the
   same reason -- a bare 1fr track still won't shrink below its content's
   min-content size, so the second column was rendering ~314-324px wide
   (blowing well past the fieldset's own ~600px box once padding and the
   column gap were added in) regardless of the 15ch floor on First name,
   which only constrains the flex item inside, not the grid track it
   sits in. */
.siblings-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 0.5rem 1rem;
}
/* Each cell stacks its First name/Child # row above the "Child Not Found"
   badge (when present) instead of running all three inline -- the 2-column
   grid's .grid-2col fix (minmax(0, 1fr), see that rule's comment) capped
   this fieldset at a hard 50% of the page instead of letting it push the
   Source PDF column narrower, so there's less width here now than the
   fields' old fixed sizes assumed; column-stacking the badge separately
   keeps the fields row itself uncluttered and free to use all of that
   narrower width. */
.siblings-grid .inline-form {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
}
/* Vertical rule between the two columns -- on the left column's cells
   (odd positions: 1st, 3rd, ...) rather than a pseudo-element on the grid
   container, so it only spans each row's own height instead of running
   the full fieldset height through the row-gap too. */
.siblings-grid .inline-form:nth-child(odd) {
  border-right: 1px solid var(--color-border);
  padding-right: 0.6rem;
}
/* nowrap plus letting First name flex to fill whatever's left after
   Child #'s fixed width keeps both fields on one line at any cell width,
   same reasoning as the fields-vs-badge stacking above. */
.siblings-grid .sibling-cell__fields {
  display: flex;
  flex-wrap: nowrap;
  gap: 0.5rem;
  width: 100%;
}
.siblings-grid .sibling-cell__fields input[name*="firstName"] {
  flex: 1 1 auto;
  /* Floor, not a fixed size: grows past this whenever the column has
     room, but won't shrink tighter than 15 characters of typing room
     even under the tightest width the 2-column layout allows just above
     the container query's 1-column collapse. */
  min-width: 15ch;
}
/* Placed after every unconditional .siblings-grid rule above (same
   selector specificity throughout this section, so source order decides
   the winner) -- an earlier draft of this query block sat above the
   :nth-child(odd) divider rule and silently lost to it at every
   container width, divider included, since @container doesn't add
   specificity of its own. */
@container (max-width: 500px) {
  .siblings-grid {
    grid-template-columns: 1fr;
  }
  .siblings-grid .inline-form:nth-child(odd) {
    border-right: none;
    padding-right: 0;
  }
}
legend {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0 0.4rem;
  color: var(--color-fg-muted);
}

/* ---------- Layout utilities ---------- */

/* minmax(0, 1fr), not bare 1fr: a bare 1fr track still won't shrink below
   its content's min-content width, so when the Siblings matrix (2 columns
   of First name + Child #) made the left column's own min-content wider
   than a true 50%, the grid quietly gave it more than its 1fr share and
   shrank the Source PDF column (right, audit page only) below its
   original width to compensate. minmax(0, ...) makes the 1fr split a hard
   50/50 regardless of either side's content, restoring (and guaranteeing
   a floor on) the PDF column's original width. */
.grid-2col {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
}

.inline-form {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  flex-wrap: wrap;
}
.inline-form--end {
  align-items: end;
}
/* Form controls default to width:100% (fills a stacked .field); inside a
   flex row that fights the layout, so each control gets its own
   content-sized width here, and the input-* utilities set an explicit one. */
.inline-form select,
.inline-form input {
  width: auto;
  flex: 0 0 auto;
}
.inline-form .field {
  margin-bottom: 0;
}

/* Admin > Users action buttons (Edit/Password/Lock/Delete): kept on one line
   rather than wrapping to a second row -- the Actions column doesn't have
   room for all 4 at once, so without this the last button (Delete) drops
   below the others. .table-wrap already scrolls horizontally, so letting
   this row grow past the column's fitted width just triggers that instead
   of wrapping. Scoped to this table only -- .inline-form's wrap behavior is
   still wanted everywhere else it's used (actual multi-field forms). */
#users-tbl .inline-form {
  flex-wrap: nowrap;
}
#users-tbl .inline-form > * {
  flex-shrink: 0;
}

/* Donors action buttons (Edit/Lists/Delete): same fix, same reason -- see
   #users-tbl above. */
#donors-tbl .inline-form {
  flex-wrap: nowrap;
}
#donors-tbl .inline-form > * {
  flex-shrink: 0;
}

/* Donors grid: fixed, percentage-sized columns (widths chosen from each
   field's typical content -- Phone/State/ZIP are short and fixed-format,
   Company/Email/Name are open text and can run long) so a single oversized
   value -- e.g. a long company name -- wraps within its own cell instead of
   stretching the table past its container. Without table-layout: fixed a
   nowrap cell just grows the column to fit its content, which pushed the
   Actions column off the visible edge of .table-wrap's scroll area. Actions
   itself stays unwrapped (see .inline-form above) and gets a wide enough
   share of the row for all three buttons on one line. */
#donors-tbl {
  table-layout: fixed;
  /* Without this, table-layout: fixed resolves the table's own width to
     .table-wrap's available width first and only *then* divides it among
     the columns -- when the declared column widths add up to more than
     that (a certainty once several are viewport-independent ch/px
     floors), the browser doesn't preserve each one's floor and let the
     table overflow instead; it shrinks every column, including the
     "fixed" ones, and starves the plain-percentage columns (Select/
     State/Actions) toward 0 first. Confirmed by measurement -- computed
     from each floor's actual measured px value in this table's font
     (1ch = 8.78px here) plus a real minimum for each percentage column
     (Select ~34px for the checkbox, State ~62px for a 2-letter code,
     Actions ~330px for Lists+Edit+Delete, both from
     getBoundingClientRect() on the rendered cells; it was ~234px until v0.6.5
     gave Edit and Delete their words) -- not a round guess.
     Forcing the table to never resolve narrower than this is what
     actually makes .table-wrap's overflow-x: auto engage below it,
     instead of the column collapse above. */
  min-width: 1300px;
}
#donors-tbl col.col-select { width: 3%; }
/* Name/Company/Email/City are ch (character-count) widths, not
   percentages, so each holds its own floor no matter the viewport.
   table-layout: fixed only sizes a column from its own <col> width --
   min-width on the td/th (or a CSS max()/min() expression mixing % with
   ch inside the <col> width itself, both tried and confirmed NOT to
   work: at a narrow viewport the percentage side won and Name/Company
   collapsed to 0, the opposite of a floor) is not honored for column
   sizing in fixed layout, unlike table-layout: auto -- so the floor has
   to be this column's *entire* width, not a minimum alongside room to
   grow. The remaining columns (Select/State/Actions) stay percentage-
   based; if the fixed-width columns plus those percentages add up to
   more than the container's width, the table grows past .table-wrap's
   100% and scrolls horizontally (already how this table handles
   overflow -- see the comment above) instead of any of them ever
   shrinking below their floor.

   2026-08-29: Name/Company were plain 12%/14% -- narrowed to nothing at
   a small enough viewport, same collapse-to-0 problem described above,
   just via the viewport shrinking rather than a failed max() -- given a
   10-character floor to match, the same way Email/City already had one.
   Actions used to need 32% for 3 full-width text buttons (Edit/Lists/
   Delete); now that Edit/Delete are icon-only (donors.php), its actual
   content -- measured via getBoundingClientRect() on the rendered
   buttons, not guessed -- is ~230px including padding at a 1280px
   viewport, well under even the new, smaller share below. Half of the
   ~90px this freed up went to Email's floor instead, the column real
   addresses most reliably overflow (25ch wrapped a plain
   first.last@example.org onto 2 lines).

   Phone is a measured px width, not ch: ch is defined by the width of the
   "0" glyph specifically, but Inter's digits aren't tabular -- measured
   via canvas.measureText() against this cell's actual computed font
   (13.92px Inter), "4" is the widest digit at 129.18px for
   "+1 (444) 444-4444" (vs. "0" at 127.07px), not the ~2px gap that alone
   would suggest: across the other columns' rounding, plain 17ch already
   wrapped a number containing several wide digits (steadily 4/6/8/9 all
   sit within ~2px of each other and above "0"). 129.18px content + 12.8px
   padding each side = 154.78px minimum border-box width; 160px leaves a
   ~5px buffer. Re-measure and adjust if the font ever changes. */
#donors-tbl col.col-name { width: 16ch; }
#donors-tbl col.col-company { width: 16ch; }
#donors-tbl col.col-phone { width: 160px; }
#donors-tbl col.col-email { width: 30ch; }
#donors-tbl col.col-city { width: 17ch; }
#donors-tbl col.col-state { width: 6%; }
#donors-tbl col.col-actions { width: 26%; }
#donors-tbl td[data-col] {
  white-space: normal;
  overflow-wrap: break-word;
}

/* Donor picker modal (Section 10.9's donor-assignment page): same
   fixed-column/wrap treatment as the Donors grid above -- a long company
   name wraps instead of forcing the narrower modal to scroll horizontally. */
#donor-picker-search {
  margin-bottom: 0.6rem;
  max-width: none;
}
#donor-picker-tbl {
  table-layout: fixed;
}
#donor-picker-tbl col.col-name { width: 42%; }
#donor-picker-tbl col.col-company { width: 38%; }
#donor-picker-tbl col.col-actions { width: 20%; }
#donor-picker-tbl td[data-col] {
  white-space: normal;
  overflow-wrap: break-word;
}

/* Staging page (templates/reporting/picking-list.php): columns pinned to a
   tight width so the row -- Child #, Name, Sex, Age, Ethnicity, Color, and
   all 3 clothing size/category pairs -- stays compact, matching the same
   "fixed columns, don't let one field's content push the row wider than
   intended" approach as the Donors grid/donor picker above. Pant/Shirt/
   Shoe are sized in ch (character count) rather than px, since their
   widths are specified in characters, not a measured pixel value.
   Name and Sex moved to measured px (2026-08-30): 7ch/36px were both
   clipping real values -- "Christopher M" (this dataset's longest name)
   and "M->F"/"F->M" (canvas.measureText()'d against this table's actual
   font, not guessed). Action's button also shrank (to the then-small
   row-button size), but that alone wasn't enough to avoid the same
   button+input-stack-onto-two-lines bug this table's Update column hit
   before -- confirmed via getBoundingClientRect() that the button and
   the Shelf/bin input shared the same `top` only once col-staging-action
   grew enough for both plus .inline-form's gap to fit on one line. */
#staging-tbl {
  table-layout: fixed;
  /* Without this, table-layout: fixed resolves the table's width against
     .table-wrap's available space before dividing among columns -- fine
     (columns just stretch proportionally) when the container is wider
     than the declared column widths sum to, as it is at a typical desktop
     width, but at a narrower one the same mechanism SQUEEZES every column
     instead, re-clipping the Name/Sex values this same change just fixed.
     Measured via getBoundingClientRect() on every <col>, not calculated:
     90+125+60+34+130+90+(3*13ch measured at 125.06px each)+245 = 1223px. */
  min-width: 1313px;
}
/* v0.6.5: 90px -> 150px. "WASH-5002" with the Siblings button and the Residential icon needs 152.5px
   (getBoundingClientRect()), and this table clips (overflow: hidden) instead of overflowing, so the Residential icon was
   cut off, not just crowded. A flat px like its neighbours here (a ch value on a <col> resolves against the body font, not
   the cells', and came out 23px too wide). min-width above grew by 60px with it, which keeps Name, Sex, Age, Ethnicity, Color and Update within 2px of the widths they had. */
#staging-tbl col.col-child-number { width: 150px; }
#staging-tbl col.col-name { width: 125px; }
#staging-tbl col.col-gender { width: 60px; }
/* 2026-10-26: 34px -> 60px. An age reads as "5M", "1.5" or "11.25" now (32px at most, measured), but the cell's content box was only 11.4px
   (the column minus 25.6px of padding): it clipped "1.5" and even "15" and the "Age" heading, and an infant's "5M" cut down to "5" would read as five
   years old -- this table clips (overflow: hidden) rather than overflowing. min-width above grew by 28px with it (the same proportion as the column), which
   keeps every other column within a pixel of the width it had. */
#staging-tbl col.col-age { width: 60px; }
#staging-tbl col.col-ethnicity { width: 130px; }
#staging-tbl col.col-fav-color { width: 90px; }
#staging-tbl col.col-size-category { width: 13ch; }
#staging-tbl col.col-staging-action { width: 245px; }
#staging-tbl th, #staging-tbl td {
  white-space: nowrap;
  overflow: hidden;
}
#staging-tbl td.truncate {
  text-overflow: ellipsis;
}
/* Pant/Shirt/Shoe (.size-category) and Ethnicity (.ethnicity-cell) are
   explicitly allowed to wrap, unlike every other column here -- a long
   "12 to 18 Months Infant/Toddler"-style size or a multi-ethnicity child
   (e.g. "African American, Hispanic") was otherwise silently clipped with
   no ellipsis at all (plain overflow: hidden, not .truncate) instead of
   fully shown. Named .ethnicity-cell rather than the generic .wrap --
   .wrap already exists above (page-level sticky-footer flex wrapper) and
   the two collided when this cell briefly used that name: the unscoped
   global rule's display:flex/min-height:100vh applied right alongside
   this scoped rule's white-space/overflow-wrap, blowing the row up to
   viewport height with the ethnicity text pinned to the top. */
#staging-tbl td.size-category,
#staging-tbl td.ethnicity-cell {
  white-space: normal;
  overflow-wrap: break-word;
}

.actions-row {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-top: 1rem;
}

/* Tag-qualified (not just .input-narrow) so this beats the generic
   input[type="date"]/select { width: 100% } rule above on specificity --
   a bare class selector would lose that fight and silently render at 100%. */
input.input-tiny, select.input-tiny      { width: 50px; }
input.input-narrow, select.input-narrow  { width: 80px; }
input.input-medium, select.input-medium  { width: 140px; }
input.input-wide, select.input-wide      { width: 190px; }
/* An age is at most two digits: the assignment filter's Age box shows three characters and no spinner arrows (owner, 2026-10-16). The width is the
   three characters plus the box's own left and right padding. */
input.input-age {
  width: calc(3ch + 1.8rem);
  min-width: 0;
  appearance: textfield;
  -moz-appearance: textfield;
}
input.input-age::-webkit-outer-spin-button,
input.input-age::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
/* A measured px width, not ch -- ch is pinned to the "0" glyph's width,
   but Inter's digits aren't tabular, so a ch value doesn't actually
   guarantee every digit combination fits (confirmed by real wrapping:
   see #donors-tbl col.col-phone's longer comment above for the
   measurement method). Measured against this input's own computed font
   (16px Inter): "4" is the widest digit at 148.48px for
   "+1 (444) 444-4444". 148.48px content + 9.6px padding each side + 1px
   border each side = 169.68px minimum border-box width; 180px leaves a
   ~10px buffer. Used by the donor Add/Edit modal's Phone field. */
input.input-phone, select.input-phone    { width: 180px; }
/* Sized for the Siblings fieldset's Child # field -- real values run 4-5
   digits (e.g. "3011"); reduced from 120px so it and First name (which
   flexes to fill the rest -- see .sibling-cell__fields) both fit within
   the Siblings grid's now-halved column width without either being
   clipped or pushed under the Source PDF panel. */
input.input-childnum                     { width: 70px; }

/* PreStage page's (renamed from Intake 2026-09-07) wish item fulfillment
   rows: only the detail fields matching the row's currently-selected
   Method show at once -- Tracking # (optional) for Waiting to Arrive/On
   Giving Tree, Shelf # for Received at Center. .hidden's !important wins
   over this when wish-fulfillment.js (or the server-rendered initial
   state) toggles it off. */
.wish-method-fields {
  display: inline-flex;
  gap: 0.6rem;
  align-items: center;
}

/* !important is intentional: a true utility class -- "this element is
   hidden" must win regardless of what other classes (e.g. .tbl-toolbar's
   own display: flex) happen to be on the same element or how CSS source
   order between them falls out. Caught by the donor bulk-action bar
   reusing .tbl-toolbar + .hidden together, where .tbl-toolbar's later
   source position was silently winning without this. */
.hidden { display: none !important; }
.inline { display: inline; }
.tbl-empty { padding: 1rem; text-align: center; }
.text-center { text-align: center; }

.mt-1 { margin-top: 1rem; }
.mb-1 { margin-bottom: 1rem; }
.ml-auto { margin-left: auto; }

/* ---------- Cards & panels ---------- */

.card {
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 1.25rem;
  margin-bottom: 1.25rem;
}

/* A plain-form card that shouldn't stretch full-width (e.g. change-password) -- same idea as .login-card's own max-width, for a card in normal page flow rather than the bare login layout. */
.card-narrow {
  max-width: 420px;
}

.card-title {
  margin: 0 0 0.85rem;
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-fg-muted);
  border-bottom: 2px solid var(--color-accent);
  padding-bottom: 0.45rem;
}

/* ---------- PDF-style child form (review/show.php) ---------- */
/* Mirrors the layout of the actual intake PDF (sample-forms/*.pdf) --
   bordered sections in the paper form's own order, and fields drawn as an
   underline with a small caption below rather than a boxed input, matching
   how the paper form presents "write on the line, label under it." The
   read-only Auditor equivalent (templates/review/audit.php) intentionally
   does NOT use these classes -- it keeps the older boxed-field look, since
   it's a data-vs-source comparison tool, not a form facsimile. */

.pdf-sheet {
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 1.1rem 1.75rem;
  /* Wide enough for Name/P-number/Gender/Age/checkboxes to share one row
     even after tightening Gender and Age to their real content widths --
     see .pdf-field--wide and .pdf-field--tiny below. */
  max-width: 960px;
}

/* No divider line between sections -- each section's own title underline
   (below) is enough separation, and skipping it lets sections sit closer
   together so more of the record shows without scrolling. */
.pdf-section {
  margin-bottom: 0.75rem;
}
.pdf-section:last-of-type {
  margin-bottom: 0;
}

/* Title text on the left, the parenthetical/context <small> (e.g. "(at the
   time of Christmas)") right after it on the same line, one space over,
   instead of wrapping to its own line below or being pushed to the full
   right -- one line per section title instead of two. Layout/border only
   here -- the actual title look (font, weight, size, case, spacing,
   color) lives on .pdf-section-title__text below, not here, so that
   class is self-contained and reusable anywhere a "title" is needed
   (e.g. the Donor field's own title), not just as this bar's direct
   child. */
.pdf-section-title {
  display: flex;
  align-items: baseline;
  justify-content: flex-start;
  gap: 0.4rem;
  border-bottom: 2px solid var(--brand-teal-color);
  padding-bottom: 0.25rem;
  margin-bottom: 0.6rem;
}
.pdf-section-title__text {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.05rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-fg);
}
.pdf-section-title small {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 0.72rem;
  text-transform: none;
  letter-spacing: normal;
  color: var(--color-fg-subtle);
  white-space: nowrap;
}
/* .pdf-section-title's own gap (0.4rem) is sized for a title butted up
   against a short <small> annotation right after it -- too small to
   match .pdf-row's 1.5rem column-gap below. With unequal gaps, two
   flex:1 columns splitting evenly would still land at different x
   positions between the title bar and the fields row. Overrides the
   gap to match exactly, only in this two-column state. */
.pdf-section-title--split {
  gap: 1.5rem;
}
/* Case Worker Information / Donor Information split the title bar and
   the fields row into two columns -- both rows use these same two
   widths (not a plain 50/50) so the two stay pixel-aligned, and Donor
   is 1in (96px) wider than an even split (Case Worker 96px narrower),
   moving the whole Donor column -- its title above and its field below
   -- 1in to the left at the shared boundary. flex: 0 0 <width> (no
   grow/shrink) keeps both exactly this width rather than the default
   1 1 0% redistributing under differing bases; min-width matches
   .pdf-field--grow's own 180px floor. Applied to both the title span
   and the field div on each side (see _pdf-sheet.php) only when Donor
   is actually present with Case Worker's field also showing --
   Donor's field falls back to .pdf-field--grow (full row width) when
   it's the only field in the row (Case Worker identity hidden from the
   current role), and Case Worker Information's title elsewhere (no
   donor at all) keeps its normal content-sized width. */
.pdf-donor-split-case-worker {
  flex: 0 0 calc(50% - 0.75rem - 96px);
  min-width: 180px;
}
.pdf-donor-split-donor {
  flex: 0 0 calc(50% - 0.75rem + 96px);
  min-width: 180px;
}

.pdf-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.65rem 1.5rem;
  align-items: end;
  margin-bottom: 0.6rem;
}
.pdf-row:last-child {
  margin-bottom: 0;
}

.pdf-field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 110px;
  margin-bottom: 0.5rem;
}
.pdf-row > .pdf-field {
  margin-bottom: 0;
}
.pdf-field input,
.pdf-field select {
  border: none;
  border-bottom: 1px solid var(--color-fg);
  border-radius: 0;
  background: transparent;
  padding: 0.15rem 0.1rem;
  min-height: 28px;
}
.pdf-field input:focus,
.pdf-field select:focus {
  border-bottom-color: var(--color-accent);
  box-shadow: none;
  outline: none;
}
.pdf-field input:disabled {
  color: var(--color-fg-muted);
  border-bottom-style: dotted;
}
/* Read-only display for a value that can run long (interests fields) --
   same dotted-underline look as a disabled input, but a plain block
   wraps to fit any amount of text with no scrollbar and no fixed row
   count to clip against, which matters here since this same markup feeds
   the print view (Section 13.3) and a printed page can't scroll. */
.pdf-static-text {
  min-height: 28px;
  padding: 0.15rem 0.1rem;
  border-bottom: 1px dotted var(--color-fg);
  color: var(--color-fg-muted);
  overflow-wrap: break-word;
}
/* Holds one badge per selected value (e.g. Ethnicity, which can have
   several) -- wraps to as many lines as needed instead of a single-line
   input silently clipping anything past its width. */
.pdf-tag-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
  min-height: 28px;
  padding: 0.15rem 0.1rem;
  border-bottom: 1px dotted var(--color-fg);
}
.pdf-tag-list__empty {
  color: var(--color-fg-muted);
  font-size: 0.9rem;
}
.pdf-field-label {
  font-size: 0.66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-fg-subtle);
  margin: 0;
}
.pdf-field--grow {
  flex: 1;
  min-width: 180px;
}
/* Fixed width, not max-width -- .pdf-field's own min-width:110px would
   otherwise win the conflict and this would never actually shrink. Sized
   for a full 10-digit unmasked P-number, not the masked "**********" --
   digits render wider than asterisks in this font (a worst-case run of
   "0000000000" needs ~101px, vs ~80px for 10 asterisks), so sizing for the
   mask alone clipped the real value for roles with full access. */
.pdf-field--narrow {
  width: 118px;
  min-width: 0;
  flex: 0 0 auto;
}
/* The "AGE" label (uppercase, ~23px) is wider than any real age value
   ("17" is ~16px) -- this fits the label, the actual binding constraint. */
.pdf-field--tiny {
  max-width: 36px;
  min-width: 0;
}
/* Sized so "Toiletries: fragrance/dye-free?" (the longest .pdf-field-label
   on the read-only child page, ~207px on one line) never wraps, while still
   fitting alongside County of placement/Ethnicity/Favorite color in the
   same row -- see review/show.php. */
.pdf-field--medium {
  flex: 1;
  min-width: 220px;
}
/* Sized for the longest genders.label on file ("Male that identifies as
   female" / "Female that identifies as male", 30 chars, measures ~226px
   rendered) -- same read-only rationale as .pdf-field--medium above. */
.pdf-field--wide {
  flex: 1;
  min-width: 236px;
}

.pdf-checkbox-group {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.1rem;
  align-items: center;
}
.pdf-checkbox-group--stacked {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
}

.pdf-siblings-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.5rem 1.5rem;
}
@media (max-width: 820px) {
  .pdf-siblings-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
.pdf-wish-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.55rem;
}
.pdf-wish-row:last-child {
  margin-bottom: 0;
}
.pdf-wish-number {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.15rem;
  color: var(--brand-teal-color);
  flex-shrink: 0;
}
.pdf-wish-row .pdf-field {
  flex: 1;
  margin-bottom: 0;
}
.pdf-wish-row .checkbox-field {
  flex-shrink: 0;
}
/* 2026-09-19: this row's OR SIMILAR checkbox became the app's standard
   inline wish icons (plus the Requires Attention wrench), sitting
   immediately after the description text inside .pdf-static-text rather
   than at the row's right edge. Only the leading gap is set here -- the
   icons' own size/alignment comes from .wish-or-similar-icon /
   .wish-assembly-icon, shared with every other wish display. */
.pdf-wish-row .pdf-static-text .wish-or-similar-icon,
.pdf-wish-row .pdf-static-text .wish-assembly-icon {
  margin-left: 0.35rem;
}
/* PreStage's per-item receipt data, on its own line under the wish it
   belongs to -- secondary to the wish text itself, and indented past the
   pdf-wish-number gutter so it reads as belonging to the row above. */
.pdf-wish-fulfillment {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.25rem;
  margin: -0.25rem 0 0.55rem 2rem;
  font-size: 0.85rem;
  color: var(--color-fg-muted);
}

/* Phase 3 enhancement (site-wide Child # -> modal conversion, see
   PHASE-LOG.md): the project owner's own instruction -- review/show.php's
   content (via review/_pdf-sheet.php) should look the same inside
   #child-detail-modal as it does on its own full page, just with tighter
   VERTICAL spacing, so more of the record is visible without scrolling in
   the modal's own more limited height. Deliberately only the vertical
   half of each rule below (margin-bottom, row-gap, padding's own vertical
   component) -- horizontal spacing (column-gap, left/right padding) is
   untouched, matching that instruction exactly (an earlier, since-
   corrected version of it asked for tighter horizontal spacing instead).
   .child-detail-modal is the modal's own wrapper class (templates/review/
   _child-detail-modal.php's .modal), so these overrides never reach the
   full-page render (review/show.php) or the print view (reporting/
   print.php), both of which keep the normal, more generous spacing. */
.child-detail-modal .pdf-sheet {
  padding-top: 0.4rem;
  padding-bottom: 0.4rem;
}
.child-detail-modal .pdf-section {
  margin-bottom: 0.25rem;
}
.child-detail-modal .pdf-section-title {
  margin-bottom: 0.2rem;
}
.child-detail-modal .pdf-row {
  row-gap: 0.2rem;
  margin-bottom: 0.2rem;
}
.child-detail-modal .pdf-field {
  margin-bottom: 0.15rem;
}
.child-detail-modal .pdf-wish-row {
  margin-bottom: 0.15rem;
}

/* Follow-up request (see PHASE-LOG.md): the read-only Wish List
   heading's own status badge (review/show.php) moved from the button
   group into its own independently-centered position -- centered across
   the WHOLE heading row (name/number left, buttons right), not just
   centered within whichever div it happens to sit in, so it stays
   centered regardless of how wide the name or the button group are on a
   given child. A plain flex `justify-content: space-between` can't do
   this for 3 unequal-width children (it distributes gaps evenly, it
   doesn't force the middle item to the row's true midpoint) -- a 3-column
   grid with the outer two columns equally flexible is the standard way to
   truly center a middle item independent of its neighbors' widths.
   `.page-heading.child-detail-heading` (not a bare `.child-detail-heading`)
   for the same reason CLAUDE.md's own paginated-listing rule already
   documents elsewhere in this file: combined-selector specificity beats
   `.page-heading` alone regardless of source order, rather than relying
   on this rule simply appearing later in the file. Every other page's
   `.page-heading` (flex, 2 children, badge inside the button div) is
   completely unaffected -- this rule only ever matches when both classes
   are present, i.e. only this one heading. */
.page-heading.child-detail-heading {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
}
.child-detail-heading__stage {
  justify-self: center;
}
.child-detail-heading__actions {
  justify-self: end;
}

/* ---------- Tables (excel-style grid) ---------- */

.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.87rem;
}

thead th {
  background: var(--color-bg-inset);
  position: sticky;
  top: 0;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-fg-muted);
  border-bottom: 2px solid var(--color-border);
}

th, td {
  text-align: left;
  padding: 0.55rem 0.8rem;
  border-bottom: 1px solid var(--color-border-muted);
  white-space: nowrap;
}

/* PreStage page (renamed from Intake 2026-09-07): the Item cell is the
   only thing left in its column now that Method/Details moved into the
   per-row form, so it gets to run larger than the table's base 0.87rem
   instead of competing for space. Capped width and wrapping (overriding
   th/td's default nowrap above) so a long description can't crowd the
   second column's Method/Details/Volunteer/Save row into wrapping onto
   its own line instead. */
.wish-item-cell {
  font-size: 1.05rem;
  max-width: 240px;
  white-space: normal;
}

/* Donors' Lists modal (donor-grid.js's renderWishesTable) -- a different
   table from .wish-item-cell above, its own Item column. table-layout:
   auto (this table's default) doesn't reliably honor max-width on a
   cell for column-sizing purposes -- confirmed by testing: it still
   rendered wider than the declared max-width even once text was
   visibly wrapping. table-layout: fixed does honor a declared width,
   the same fix already used for #donors-tbl elsewhere in this file, so
   this table gets it too, scoped to just this one (donor-picker-tbl and
   the outer donors-tbl are unaffected). Only Item needs an explicit
   width; the other 4 columns share whatever's left automatically. */
#donor-lists-body table {
  table-layout: fixed;
}
.donor-wish-rank-cell {
  width: 4ch;
}
.donor-wish-item-cell {
  width: 19.4ch;
  white-space: normal;
}
.donor-wish-method-cell {
  width: 18ch;
}
.donor-wish-tracking-cell {
  width: 15ch;
  white-space: normal;
}
.donor-wish-received-cell {
  width: 9ch;
}

/* Assembly tracking: same table-layout: fixed + every-column-declared fix
   as the two tables above -- Item had no width at all (measured 510px /
   ~58.1ch, sized purely from the longest description in real demo data)
   and was squeezing Update's own checkbox/Shelf-bin input/Save button so
   tightly that .inline-form's flex-wrap stacked all three vertically
   instead of side by side (confirmed via getBoundingClientRect(): each
   sat at a different top with the same left, one per line). Item is
   capped 10 characters narrower and allowed to wrap; Update gets a
   precise px width instead of ch, sized to what its actual content
   (checkbox label + input-medium + button + gaps) measured needing.
   Storage location column was removed (2026-08-29: the shelf/bin field
   already shows that data) -- min-width dropped accordingly, remeasured
   the same way as the rest of this block, not just arithmetic. Update is
   then widened again by the same amount plus a bit more: auto-save's
   .save-status indicator sits after the Save button on the same flex
   line, and "Saving…" (the longest routine state, measured via
   getBoundingClientRect()) didn't fit in the leftover space, wrapping
   to its own line below the button instead of reading inline. Shelf's
   own header, "PreStage shelf" (renamed from "Intake shelf" 2026-09-07),
   didn't fit in 10ch either (measured via
   canvas.measureText() against the header's own uppercase/letter-spaced
   rendering, not the raw string -- text-transform makes it noticeably
   wider than it looks in source) -- widened to a fixed px value, same
   as Update, rather than guessing a larger ch count. */
#assembly-tbl {
  table-layout: fixed;
  /* 1126px before v0.6.5. Child # grew from 83px to 159px (below), and table-layout: fixed shares any width beyond the
     declared columns out in proportion, so min-width went up to the figure at which Name, Item, Shelf and Update measure
     (getBoundingClientRect(), 2026-10-16) exactly what they did before -- 83, 401, 129 and 429px -- rather than the 5-27px
     narrower they came out at with the old value, which wrapped Item and Name onto an extra line and made every row 8px
     taller. */
  min-width: 1213px;
}
/* v0.6.5: widened from 10ch. A 9-character "WASH-5002" with the Siblings button and the Residential icon needs 152.5px
   (getBoundingClientRect(), the same measurement Donor Assignment's Child # got); 10ch was 83px, so the number itself
   already ran into Name on the second agency's numbers. Same width formula as Donor Assignment. */
.assembly-child-cell {
  width: calc(12ch + 1.6rem + var(--child-number-icons-room));
}
.assembly-name-cell {
  width: 10ch;
}
.assembly-item-cell {
  width: 33ch;
  white-space: normal;
}
.assembly-shelf-cell {
  width: 120px;
}
td.assembly-shelf-cell {
  text-align: center;
}
/* v0.6.5 (the owner: Save between Shelf/bin and the Volunteer drop-down, "all in the same row"): Completed, Shelf/bin, Save, Volunteer and the
   "Saving..." note measure 555px side by side (getBoundingClientRect(): 114 + 140 + 80 + 130 + 53 + four 9.6px gaps) plus the cell's 25.6px padding, so
   Update is 590px and the form never wraps; Item gave the room up (it wraps, and its rows are now one line of controls instead of four). */
.assembly-update-cell {
  width: 590px;
}
.assembly-form {
  flex-wrap: nowrap;
}

/* Shared Wishes column (introduced by the Phase 3 Check-In enhancement,
   see PHASE-LOG.md; reused as-is by Donor Assignment as of its own
   follow-up request) -- stacks one line per wish item, a small top margin
   between consecutive lines (not the first) is enough separation without
   needing a bulleted/bordered list. Generically named (not "checkin-...")
   specifically because it's already used by more than one page.
   white-space: normal overrides the global th, td { white-space: nowrap }
   (above) -- without it, a long wish description wouldn't wrap at all,
   it would just render past its own cell's edge instead, which matters
   far more once this column has an actually-constrained width (Donor
   Assignment's own table-layout: fixed below) than it did when Check-In's
   own table was left free to grow as wide as this column's content
   wanted. */
/* PreStage detail modal's "Additional Items Received" section
   (2026-09-19) -- sits between the wish-item table and the save bar
   inside the same form, so it needs its own top separation from the
   table it follows. The heading itself reuses .card-title, matching the
   "WISH LIST ITEM PRESTAGE" heading above it. */
.prestage-additional-items {
  margin-top: 1.25rem;
}

.wish-list-cell {
  white-space: normal;
}
.wish-list-row + .wish-list-row {
  margin-top: 0.35rem;
}

/* Wish Lists/Check In/PreStage listing (search/index.php) -- caps the
   Donor column and lets it wrap (overriding the global th, td nowrap
   above), same technique as .wish-item-cell. Added 2026-09-19 when Wish
   Lists picked up the Wishes column: a company-suffixed donor name runs
   to 42 characters in real GCP data (local demo data's longest is far
   shorter, so this only surfaced once deployed), forcing that column to
   321px and pushing the table 17px past its container -- the same
   horizontal-scrollbar problem the Donor Assignment page hit for the same
   reason. 240px comfortably fits the common case on one line and wraps
   only the genuinely long ones. */
.listing-donor-cell {
  min-width: 12rem;
  max-width: 240px;
  white-space: normal;
}

/* Donor Assignment (follow-up request, see PHASE-LOG.md): adding the
   Wishes column above widened this table past its container, forcing an
   unwanted horizontal scrollbar (.table-wrap's own overflow-x: auto).
   table-layout: fixed -- with no min-width, unlike #assembly-tbl's own
   deliberately-wider-than-container table below -- makes the table's
   existing width: 100% (base rule above) an actual hard cap instead of
   just a suggestion auto layout is free to exceed: every column below
   gets an explicit width except Wishes, which simply takes up whatever's
   left (same "only the columns that need a precise width get one, the
   rest share the remainder" convention #donor-lists-body table's own
   comment documents).

   Every ch width below is wrapped in calc(Nch + 1.6rem), not a bare Nch --
   this app's global * { box-sizing: border-box } (top of this file) means
   a declared width includes th/td's own existing 0.8rem-each-side
   padding (unmodified, per CLAUDE.md's own rule), so a bare 3ch column
   would try to fit that 1.6rem of padding AND 3 characters of content
   into a 3ch box -- impossible once 3ch alone (about 23px in this font)
   is already smaller than the 1.6rem (25.6px) of padding it has to share
   space with. Caught live: a first pass using bare Nch values rendered
   Gen/Age at exactly padding-width with zero room for "M"/"11" at all,
   silently overflowing past their own cell instead of actually fitting --
   looked like truncation, was really this. The +1.6rem keeps each Nch
   value meaning what it says (N characters of actual content), on top of
   the padding both existed before and remains.

   Age and Gender sized to the project owner's own explicit instruction
   (3ch content -- enough for "M"/"F"/"M->F"/"F->M" and any realistic
   age). Name/Ethnicity/Donor/Wishes get white-space: normal, but sized
   from the real demo data's own actual length distribution (measured
   live via getBoundingClientRect() over every row on the page, not
   guessed) specifically so wrapping only kicks in for genuinely large
   values, per the project owner's own explicit goal -- typical data
   should read on one line:
     - Name: 14ch comfortably covers the real max (13, "Christopher M") --
       wrapping here would need a longer name than exists in this dataset.
     - Ethnicity: 17ch comfortably covers the real max (16, "African
       American"/"Native American") -- same reasoning.
     - Donor: real lengths vary far more (median 19, 75th pct. 31, 90th
       pct. 39, max 42 -- a company-suffixed name, e.g. "Jennings, Kai
       (Union Square Print Shop)", is a normal quarter of real donors, not
       a rare fluke) -- 31ch (the 75th percentile) so a plain "Last,
       First" donor never wraps and only the longer company-suffixed
       quarter does, rather than sizing to the max and starving Wishes.
     - Wishes: gets whatever's left (unchanged mechanism) rather than an
       explicit width, which at this table's real column budget worked
       out to comfortably cover typical wish descriptions (median 22
       characters, 90th percentile 36) with only the longest ones (the
       69-character outlier being the extreme case) wrapping.
   Actions keeps a flat px width, not ch, matching #assembly-tbl's own
   .assembly-update-cell precedent -- its content is a button
   (padding/label driven), not prose, and isn't narrow enough for the
   border-box problem above to bite.

   Follow-up fix: Child # widened 9ch -> 11ch -- confirmed live
   (getBoundingClientRect()) that the household icon suffix
   (View::householdIconSuffix(), 🏠) was overflowing the cell's own right
   edge by about 8px into Name's space at 9ch, since "WSC-3011" (8
   characters) plus the icon together need a bit more than 9ch of content
   room. No column lost its own explicit width for this -- Wishes (the
   one column with no explicit width of its own) simply absorbs 2ch less
   of the table's total budget than before, same mechanism every other
   width change on this table already goes through.

   Widened again, 11ch -> 12ch, after deploying to GCP and finding the
   same overlap still live there specifically: the GCP database has real
   children under a second referral agency code, WASH (not just WSC), and
   "WASH-5003"-style numbers are 9 characters, one longer than "WSC-3011"
   -- checked directly against the live DB (`SELECT referral_agency_code,
   MAX(LENGTH(...))...`) rather than guessing after the fact, since local
   demo data only ever seeds WSC and could never have surfaced this on
   its own. Re-verified live on GCP itself afterward -- every WASH- row's
   icon overflow went from positive (a few px past the cell) to negative
   (comfortably inside it), same margin WSC- rows already had.

   v0.6.5: the 🏠 became two icons -- the Siblings button (household) and the 🏠 now meaning residential placement --
   and a child can carry both. Re-measured with getBoundingClientRect() on a "WASH-5002" carrying both: Child # needed
   152.8px against the 119px above, so it overflowed into Name by 33.7px. The column now adds
   --child-number-icons-room (base.css :root), the one token that also sizes the same column on Assembly and Staging,
   the other two pinned-width tables with a Child # (every auto-layout table sized itself and needed nothing). */
#donor-assignment-tbl {
  table-layout: fixed;
}
.donor-assign-child-cell {
  width: calc(12ch + 1.6rem + var(--child-number-icons-room));
}
.donor-assign-name-cell {
  width: calc(14ch + 1.6rem);
  white-space: normal;
}
.donor-assign-gender-cell {
  width: calc(3ch + 1.6rem);
}
.donor-assign-age-cell {
  width: calc(3ch + 1.6rem);
}
.donor-assign-ethnicity-cell {
  width: calc(10ch + 1.6rem);
  white-space: normal;
}
/* v0.6.5 (C-9): County of placement, on Donor Assignment and the Sponsor Requests per-donor page. Sized from the real data, measured on
   the local database (longest "Gladwin", 7) and the GCP test database (longest "Washtenaw", 9; the others Macomb, Wayne, Gladwin);
   a longer one wraps like Name does. */
.donor-assign-county-cell {
  width: calc(9ch + 1.6rem);
  white-space: normal;
}
/* v0.6.5 follow-up (owner, 2026-10-16): Ethnicity (17ch -> 10ch) and County (10ch -> 9ch) were narrowed and their body cells set in a smaller
   font, so the Wishes column -- the one with no width of its own, which takes whatever is left -- got narrower than it should have when County
   arrived. Only the body cells shrink: the header's own font (thead th) is already small, and ch in a column's width is measured in the
   header's font, so the widths above are the measured ones, not a percentage of the new font. The longest single word of an ethnicity
   ("Caucasian", 9 letters) and of a county ("Washtenaw", 9) still fits the column; "African American", "Native American" and combinations of
   labels wrap onto two or three short lines, which the three-wish rows (never shorter than three lines) absorb. */
td.donor-assign-ethnicity-cell,
td.donor-assign-county-cell {
  font-size: var(--assign-compact-column-font-size);
}
.donor-assign-donor-cell {
  width: calc(31ch + 1.6rem);
  white-space: normal;
}
.donor-assign-actions-cell {
  width: 150px;
}

/* PreStage page (renamed from Intake 2026-09-07): a smaller font shrinks
   the select's own rendered width too (its longest option, "Waiting to
   Arrive (Red Tag)" -- previously "Shipped (tracked)" before the 2026-09-07
   rename/redesign -- otherwise sets a wide floor regardless of the
   field's own font-size) -- part of fitting Method alongside
   Tracking#/Shelf#/Volunteer on one row (Save moved out of the row
   entirely with the single-Save-button enhancement below). */
.wish-method-select {
  font-size: 0.8rem;
}

/* PreStage modal enhancement (2026-09-07): the one shared Save button for
   all of a child's wish items, below the table instead of one per row. */
.prestage-save-bar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 0.8rem;
}

/* PreStage heading's Donor -- a sibling of <h1> within .page-heading, not
   nested inside it, so .page-heading's own justify-content: space-between
   (above) pushes it to the right automatically. white-space: nowrap keeps
   "Donor: Name (Company)" from breaking mid-phrase; .page-heading's own
   flex-wrap still lets the whole block drop to its own line below the
   heading if the modal's narrower width leaves no room for both. */
.prestage-heading-donor {
  font-size: 1rem;
  white-space: nowrap;
}

/* Brief inline confirmation next to a Save button -- replaces the old
   page-reload's flash message for any form that saves via fetch without
   navigating away (prestage-autosave.js -- despite the filename, no
   longer auto-saves as of 2026-09-07, only on an explicit Save click --
   and assembly.js, which still does). */
.save-status {
  font-size: 0.82rem;
}
.save-status.is-success {
  color: var(--color-success);
}
.save-status.is-error {
  color: var(--color-danger);
}

tbody tr:last-child td {
  border-bottom: none;
}

tbody tr:hover {
  background: var(--color-bg-inset);
}

/* Sortable column headers (excel-like grid, e.g. the Donors page) */
th.sortable {
  cursor: pointer;
  user-select: none;
}
th.sortable:hover {
  color: var(--color-fg);
}
th.sortable .sort-indicator {
  margin-left: 0.25rem;
  opacity: 0.4;
  font-size: 0.68rem;
}
th.sortable.sorted-asc .sort-indicator,
th.sortable.sorted-desc .sort-indicator {
  opacity: 1;
  color: var(--color-accent);
}

/* Shown right after a wish's description wherever that page doesn't
   already have the real OR SIMILAR checkbox in its own layout (Assembly
   tracking, PreStage fulfillment, the Donors "Lists" modal) -- review/
   _pdf-sheet.php and review/audit.php show a real checkbox instead and
   don't use this. cursor: help pairs with the title="or similar"
   attribute that drives the actual tooltip. Standing convention (see
   CLAUDE.md): any future page that lists a wish's description without
   that checkbox gets this same icon.

   2026-09-03: an <img> (public/assets/img/or-similar.png), not a text
   glyph -- sized in em so it scales with whatever font-size context it
   sits in, same as the glyph it replaced; vertical-align nudges it back
   onto the text baseline, which an inline image doesn't sit on by
   default the way a character glyph does. */
.wish-or-similar-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  margin-left: 0.3rem;
  cursor: help;
}

/* Check In page's own Wishes column (Phase 3 enhancement, see
   PHASE-LOG.md): replaces the original plain-text "Assembly" badge with a
   dedicated icon, same sizing/vertical-align/cursor recipe as
   .wish-or-similar-icon just above (public/assets/img/assembly-wrench.svg)
   -- a distinct amber color (not teal, unlike OR SIMILAR) so the two read
   as clearly different at a glance when both sit on the same wish line. */
.wish-assembly-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  margin-left: 0.3rem;
  cursor: help;
}

/* v0.6.5 child icons (View::childIconSuffix(), after a Child # on the main workflow/listing pages).

   Household (2+ children this program year): the Siblings artwork, public/assets/img/siblings-icon.png, as a <button>
   that opens the household modal (icon-modals.js). The PNG is a CSS mask, so the figure is painted in
   --color-household-icon (the artwork's own navy on the light themes, the theme's text colour on dark and
   high-contrast; themes.css) and stays legible on every background without a second image. --household-icon-size is
   the one number that resizes it everywhere (templates never carry a size).

   Residential placement: the 🏠 glyph, which used to mean "household" and now means "residential placement" -- static
   (cursor: help pairs with the title tooltip, same reasoning as .wish-or-similar-icon just above).

   Raised hand 🙋 after a donor's name (View::sponsorRequestIconSuffix()): a button inside the same
   .sponsor-request-icon-suffix span as ever, opening the sponsor-request modal. */
:root {
  --household-icon-size: 1.25em;
  /* Room, in a column whose width is pinned (table-layout: fixed), for what follows a Child #: the Siblings button, the
     Residential icon and the two spaces between. Measured 2026-10-16 with getBoundingClientRect() on a 9-character
     "WASH-5002" carrying both icons: the content needed 2.12rem more than the same number with no icons; 2.5rem leaves a
     little air. Change it together with --household-icon-size. Auto-layout tables (Wish Lists, Check In, PreStage,
     Fulfillment, Review, Households, the Admin pricing queue) size themselves and need nothing. */
  --child-number-icons-room: 2.5rem;
  /* The narrow read-only modal (.modal-sm): the sponsor-request modal, six short facts and a line or two of free text. */
  --modal-sm-max-width: 24rem;

  /* Sponsor Requests per-donor assignment page (v0.6.5, C-1 .. C-8). */
  /* C-7: the status badge at the top, larger than the app-wide .badge (scoped: .sponsor-request-card .badge below). */
  --sponsor-badge-font-size: 0.9rem;
  --sponsor-badge-padding: 0.25rem 1rem;
  --sponsor-badge-min-height: 1.8rem;
  /* C-1/C-2: the five columns of the request's head -- requested, gender, age, special requests, status (under Done). One definition, so the
     title row (which holds Done in the last column) and the two fact rows line up exactly. */
  --sponsor-head-columns: auto minmax(7rem, 14rem) minmax(8rem, 16rem) minmax(10rem, 1fr) max-content;
  /* C-5: the cell at the left of the filter row reserved for Assign selected (moved from the right, 2026-10-16), so showing or hiding it moves nothing. */
  --sponsor-assign-cell-width: 9.5rem;
  /* C-6: the gap between the donor's name and its Remove Assignment button. */
  --sponsor-donor-action-gap: 0.6rem;
  /* C-8: the frame's thickness. Its colour is --color-assigned-frame (themes.css). */
  --sponsor-frame-width: 3px;
  /* C-9 follow-up: the font of the body cells of the Ethnicity and County columns on Donor Assignment and the per-donor page. Both hold short
     labels, so a smaller font lets their columns be narrower and gives the Wishes column the room back (owner, 2026-10-16). */
  --assign-compact-column-font-size: 0.85rem;
  /* Sponsor Requests listing (v0.6.5, B-2 .. B-4): the Actions column holds Assign / Edit / Delete side by side. */
  --sponsor-row-actions-gap: 0.35rem;
  /* v0.6.5: the height of every button (see .btn). One value, so a button in a table row, a toolbar and a window can never differ. */
  --button-height: 36px;
  --sponsor-row-actions-width: 17rem;
}
.household-icon {
  display: inline-block;
  width: var(--household-icon-size);
  height: var(--household-icon-size);
  vertical-align: -0.3em;
  background-color: var(--color-household-icon);
  -webkit-mask: url('../img/siblings-icon.png') center / contain no-repeat;
  mask: url('../img/siblings-icon.png') center / contain no-repeat;
}
.household-modal-btn {
  display: inline-block;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  line-height: 1;
  color: inherit;
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.household-modal-btn:hover .household-icon {
  opacity: 0.75;
}
.residential-icon-suffix {
  cursor: help;
}
.sponsor-request-modal-btn {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  line-height: 1;
  color: inherit;
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.sponsor-request-modal-btn:hover {
  opacity: 0.75;
}
/* The group headers' own Siblings image (Households report, Review phase tabs): not a button -- the rows under the
   header are already the household. */
.household-group-header .household-icon {
  margin-right: 0.25rem;
}
/* Windows forced-colors (its own high-contrast mode) repaints backgrounds with system colours, which would turn the
   mask's fill into the page colour; opt the icon out and give it the text colour instead. */
@media (forced-colors: active) {
  .household-icon {
    forced-color-adjust: none;
    background-color: CanvasText;
  }
}

/* The sponsor-request modal's facts (templates/workflow/_sponsor-request-summary.php): the shared .sponsor-summary list,
   without the bottom margin it carries on a page, since the modal body already pads it. */
.sponsor-request-modal-summary {
  margin-bottom: 0;
}

/* The wish-lists modal on the Sponsor Requests listing (templates/workflow/_sponsor-request-wish-lists.php): the Wishes column
   wraps like it does on Donor Assignment (.wish-list-cell), and a Child # with its icons stays on one line. */
.sponsor-wish-lists-table td:first-child {
  white-space: nowrap;
}

/* The household modal's own table (templates/review/_household-modal-body.php). */
.household-modal-table td {
  white-space: normal;
}
.household-modal-row--this td {
  background: var(--color-accent-bg);
}
.household-modal-this-child {
  margin-left: 0.4rem;
  font-size: 0.85em;
  font-weight: 600;
  color: var(--color-fg-muted);
}

/* Phase 2 enhancement (Q29): the Staging page's manual Check-Out/Check-In --
   "a light red color", removed the moment the row is checked back in.
   --color-danger-bg is the same token every alert/badge already uses for
   "danger", so this stays correct across every theme (light, dark, high-
   contrast, etc.) instead of a hardcoded hex value that would only look
   right in one of them. */
.row-checked-out {
  background-color: var(--color-danger-bg);
}

/* Toolbar above a grid: free-text search + primary action */
.tbl-toolbar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.6rem;
  flex-wrap: wrap;
}
.tbl-search {
  flex: 1;
  min-width: 200px;
  max-width: 340px;
}

/* Per-column filter row rendered as a second row inside <thead> */
.filter-row th {
  padding: 0.35rem 0.5rem;
  background: var(--color-bg-elevated);
  border-bottom: 2px solid var(--color-border);
  position: sticky;
  top: 34px;
}
/* type="text" only -- the Donors filter row also holds a .checkbox-inline
   checkbox (Has a list assigned), which must keep .checkbox-inline input's
   own 20px width instead of stretching to fill the cell like these do. */
/* The global label rule (uppercase, 600 weight, letter-spacing) is meant
   for a field's own label above it, not a checkbox sharing this row with
   plain filter inputs -- match those instead, so "Has a list assigned"
   reads like the rest of the filter row, not a section heading. */
.filter-row .checkbox-inline {
  font-size: 0.8rem;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
}
.filter-row input[type="text"] {
  width: 100%;
  min-height: 0;
  padding: 0.3rem 0.5rem;
  font-size: 0.8rem;
  font-weight: 400;
  text-transform: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}

/* ---------- Badges ---------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.badge-success { background: var(--color-success-bg); color: var(--color-success); }
.badge-warning { background: var(--color-warning-bg); color: var(--color-warning); }
.badge-danger  { background: var(--color-danger-bg);  color: var(--color-danger);  }
.badge-info    { background: var(--color-info-bg);    color: var(--color-info);    }
.badge-neutral { background: var(--color-bg-inset);   color: var(--color-fg-muted); }

/* Admin > Users role badges (Section 14's 5 roles): fixed colors, not
   theme-driven like the badges above -- the point is a color that reliably
   means "this role" no matter which of the 8 themes is active, the same
   way a legend wouldn't change color depending on who's looking at it. */
.badge-role-staging       { background: #dcfce7; color: #166534; }
.badge-role-manager       { background: #ccfbf1; color: #115e59; }
.badge-role-administrator { background: #fee2e2; color: #991b1b; }
.badge-role-auditor       { background: #fef9c3; color: #854d0e; }
.badge-role-case-worker   { background: #dbeafe; color: #1e40af; }

/* Audit Status (Section 6.1's 7 values) and Fulfillment Stage (Section
   7.1's 9 values) each get their own fixed color, same "not theme-driven,
   a legend wouldn't change color" reasoning as the role badges above --
   added 2026-09-06 because the previous 3-bucket success/warning/neutral
   scheme left most of the 16 values sharing one indistinguishable gray.
   Colors are deliberately drawn from 16 different hue families (no two
   share a background) so no two states are easy to mistake for each
   other even across the two columns, which sit side by side on Wish
   Lists/PreStage. View::auditStatusBadgeClass()/fulfillmentStageBadgeClass()
   map each value to exactly one of these. */
.badge-audit-status-ready-for-audit-phase-1               { background: #dbeafe; color: #1e40af; }
.badge-audit-status-audit-1-requires-case-worker-attention { background: #fef3c7; color: #92400e; }
.badge-audit-status-ready-for-audit-phase-2               { background: #ccfbf1; color: #115e59; }
.badge-audit-status-audit-2-requires-case-worker-attention { background: #ffedd5; color: #9a3412; }
.badge-audit-status-ready-for-final-audit                 { background: #e0e7ff; color: #3730a3; }
.badge-audit-status-final-audit-requires-case-worker-attention { background: #fee2e2; color: #991b1b; }
.badge-audit-status-verified                              { background: #dcfce7; color: #166534; }

.badge-fulfillment-stage-awaiting-audit-completion { background: #e2e8f0; color: #334155; }
.badge-fulfillment-stage-audit-complete            { background: #e0f2fe; color: #075985; }
.badge-fulfillment-stage-check-in-complete         { background: #fae8ff; color: #86198f; }
.badge-fulfillment-stage-prestage-complete         { background: #ecfccb; color: #3f6212; }
.badge-fulfillment-stage-staging-complete          { background: #fce7f3; color: #9d174d; }
.badge-fulfillment-stage-wrapping-complete         { background: #fef9c3; color: #854d0e; }
.badge-fulfillment-stage-awaiting-assembly          { background: #ede9fe; color: #5b21b6; }
.badge-fulfillment-stage-ready-for-pickup          { background: #d1fae5; color: #065f46; }
.badge-fulfillment-stage-received-by-case-worker   { background: #ffe4e6; color: #9f1239; }

/* ---------- Tabs ---------- */

.tab-bar {
  display: flex;
  gap: 1px;
  border-bottom: 2px solid var(--color-border);
  margin-bottom: 1.1rem;
  flex-wrap: wrap;
}
.tab-btn {
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.55rem 1.1rem;
  border: none;
  background: none;
  color: var(--color-fg-muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
}
.tab-btn:hover {
  color: var(--color-fg);
}
.tab-btn.on {
  color: var(--color-accent);
  border-bottom-color: var(--color-accent);
}
/* Admin's tabs and the Review listing's (v0.6.5, option C, the owner's choice): the active tab is a dark chip with orange text -- the same cue the header
   gives the current page (.app-nav a[aria-current="page"]: charcoal, --color-nav-highlight) -- instead of orange text over a thin line, which was hard to
   tell from the others. The household tabs on a Review detail page have their own styling below and keep it. */
.tab-bar--chips {
  gap: 4px;
  padding-bottom: 6px;
}
.tab-bar--chips .tab-btn {
  border-bottom: none;
  margin-bottom: 0;
  border-radius: var(--radius-sm);
}
.tab-bar--chips .tab-btn:hover {
  background: var(--color-bg-inset);
}
.tab-bar--chips .tab-btn.on {
  background: var(--color-tab-active-bg);
  color: var(--color-nav-highlight);
}
/* The emoji in Admin's tab labels are colour pictures drawn in dark blues, greys and browns, which vanish on the charcoal chip (the owner: "the icons do
   not stand out"), so the active tab's icon sits on a small white plate. */
.tab-bar--chips .tab-btn.on .tab-icon {
  background: var(--color-tab-icon-plate);
  border-radius: 4px;
  padding: 0 0.25rem;
}
.tab-pane {
  display: none;
}
.tab-pane.on {
  display: block;
}

/* Wish-list tabs enhancement (2026-09-07, Audit household detail pages):
   richer styling layered on top of the generic .tab-bar/.tab-btn rules
   above via combined selectors (higher specificity than either class
   alone, so the override holds regardless of source order) -- scoped to
   household-tab-bar/household-tab-btn specifically so Admin's and the
   Audit listing's own unrelated tab bars (same generic classes, same
   tabs.js, confirmed via grep to be the only other 2 .tab-bar consumers
   in the app) render exactly as they already did, completely unaffected. */
.tab-bar.household-tab-bar {
  position: sticky;
  /* Stacks below the site header exactly like .audit-sticky-heading
     already did before this -- that heading's own offset (below) now
     adds --tab-bar-height on top of this same value, so the two sit
     directly one after another with no gap or overlap. */
  top: var(--sticky-header-height, 58px);
  /* Above .audit-sticky-heading's z-index: 800 -- this bar sits higher in
     the sticky stack (closer to the top of the viewport) and needs to
     stay visually in front of it as both become pinned during scroll. */
  z-index: 900;
  /* Design change (2026-09-07): removed the distinct "track" tint
     (--tab-track-bg) per an explicit later request for no fill on this
     row -- reverses this rule's own original reasoning (a recessed
     track color, distinct from the page, is what used to sell "the tabs
     sit inside a container"). Matches --color-bg (the page background)
     rather than a literal `none`/transparent: this row is position:
     sticky, and a truly transparent fill lets page content scrolling
     past underneath show through it while it's pinned -- confirmed live
     before this was settled (scrolled the page, saw the Case Worker
     section's own box bleed through the row). Opaque and page-colored
     avoids that while still reading as "no distinct fill," which was
     the actual goal. --tab-track-bg (themes.css) is left defined and
     still used elsewhere on this same row (the high_contrast-only
     --tab-track-outline border and its own comment both still apply). */
  background: var(--color-bg);
  /* --tab-track-outline is none in every theme except high_contrast,
     which adds a full border here on top of the color fill and the
     height difference below (themes.css's own comment on
     --tab-track-outline has the full reasoning) -- bottom is left to the
     generic .tab-bar's own border-bottom above, unchanged; this only adds
     the 3 sides that border doesn't already cover. */
  border-top: var(--tab-track-outline, none);
  border-left: var(--tab-track-outline, none);
  border-right: var(--tab-track-outline, none);
  /* Bottom-aligns tabs instead of the flex default (stretch), which would
     otherwise force every tab to the same height and erase the
     active/inactive height difference below before it could ever render.
     Bottom, not top or center, so the height difference reads as the
     active tab growing upward out of the track while every tab's bottom
     edge -- and so the seamless connection into its content panel below
     -- stays exactly where it already was. */
  align-items: flex-end;
  /* Design correction (2026-09-07): the active tab previously sat flush
     with the track's own top edge (zero track color visible above it) --
     revised so a sliver of track shows above the active tab too, just
     less of one than above the inactive tabs, the same way the track
     already reads above every inactive tab. Since align-items: flex-end
     anchors every tab to the *bottom*, this padding-top is the only
     dimension that can add that headroom -- it pushes the whole flex
     content area down by a fixed amount uniformly, so the active tab
     (still the tallest, still poking furthest up) and every inactive tab
     each end up with their existing headroom above them plus this same
     fixed amount, preserving the difference between them while making
     neither one flush with the track's own top anymore. */
  padding-top: 0.35rem;
  /* Up to 8 sibling tabs (Section 13.1's sibling slots) -- wrap would let
     a sticky bar grow to 2-3 lines tall and permanently eat that much
     more of a tablet's limited vertical space; a single scrollable row
     keeps its height constant (which the sticky offset math above also
     depends on) and reads as a completely natural touch gesture. */
  flex-wrap: nowrap;
  overflow-x: auto;
  /* Explicit, not left to default -- the CSS overflow spec computes
     overflow-y as auto (not visible) the moment overflow-x is anything
     but visible, *even though this axis is never meant to scroll at all*.
     Confirmed live this was firing for real, not just a theoretical risk:
     scrollHeight (48px) vs. clientHeight (46px) on a plain 2-tab
     household -- a 1-2px sub-pixel mismatch between the flex-computed
     container height and its tallest (active) child's own rendered
     height, well within normal browser rounding -- was enough to trigger
     a visible vertical scrollbar with no genuine vertical overflow to
     scroll. hidden (not auto, not visible) is exempt from that coupling
     rule entirely, so it suppresses the scrollbar without reintroducing
     it; the cost is that any future sub-pixel mismatch clips by a
     imperceptible pixel or two instead of scrolling, which is the far
     better failure mode for a bar that was never supposed to scroll
     vertically in the first place. */
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  /* Bug fix (2026-09-07): found while re-checking the seamless-connection
     fix below against a genuinely-overflowing 8-tab household -- a real
     (not phantom, unlike the overflow-y coupling above) horizontal
     scrollbar reserves space for its own widget *inside* this box, which
     shrinks the area align-items: flex-end has to work with, so the
     active tab's own bottom edge lands short of this box's own bottom
     edge by roughly the scrollbar's own thickness (confirmed live: a
     14px gap reappeared specifically on an 8-tab household at tablet
     width, absent on a 2-tab household with nothing to scroll). Hiding
     the scrollbar *widget* -- not the scrolling itself, which stays
     fully working via mouse wheel, trackpad, touch drag, and keyboard --
     removes that reservation entirely rather than trying to predict or
     compensate for a thickness that varies by OS and browser. The 3
     rules below are the standard cross-browser way to do that (Firefox,
     then legacy Edge/IE, then everything WebKit/Blink); no vendor combo
     covers 100% of browsers alone. Accepted tradeoff: a mouse/trackpad
     user loses the native scrollbar as a "there's more here" hint on an
     overflowing household -- the last visible tab still renders partly
     cut off at the bar's edge in that case, which was already this bar's
     only such hint even before this change and is standard enough
     practice for this exact pattern (horizontally-scrolling tab/chip
     rows) elsewhere on the web that it wasn't treated as needing a
     separate, purpose-built affordance here. */
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* Closes the gap the generic .tab-bar's own margin-bottom: 1.1rem would
     otherwise leave between the track and the content panel below --
     harmless for the other two .tab-bar consumers (Admin, Audit listing),
     which have no seamless-connection illusion to preserve, but here it
     defeated that illusion outright: the active tab's border/background
     trick (.on below) erases the seam at the *tab bar's own* bottom edge,
     which only reads as "flowing into the panel" when that edge is
     actually adjacent to the panel, not floating 17.6px above it. */
  margin-bottom: 0;
  /* Bug fix (2026-09-07): the generic .tab-bar's own border-bottom is a
     single 2px line across this *entire* bar's width, but the active
     tab's own erasure trick (.on below) only paints over that line at
     the active tab's own horizontal position -- a household can have up
     to 8 tabs (Section 13.1), so the active tab typically covers only a
     small fraction of the bar's full width. Reported (with a screenshot)
     as "the header row below has a top border" -- confirmed live this
     was exactly it: the black line was still fully intact everywhere
     *except* directly under the active tab, immediately above the
     content panel below, which spans the bar's full width and so sits
     directly under most of that still-visible line. Removed outright
     rather than patched per-tab, since the track's own background-color
     difference from the page (--tab-track-bg) already communicates
     "this is a track with tabs in it" on its own -- confirmed by
     inspection this border was never actually load-bearing for that.
     high_contrast's own separate --tab-track-outline (border-top/left/
     right above) is unaffected and still gives that theme a real,
     non-color-dependent border on the 3 sides that were never part of
     this seamless-connection problem in the first place -- only the
     bottom, which by definition has to connect to content, not fully
     enclose the track. */
  border-bottom: none;
  /* Rounded top corners read as an actual container the tabs sit inside,
     not just a flat colored band -- bottom stays square (default),
     matching the flat bottom edge the seamless-connection trick and the
     border-bottom-mirrors-.tab-bar's-line trick both depend on. Small
     horizontal padding keeps the first/last tab's own top-corner
     rounding (below) from colliding with the track's own rounding at
     the same corner. */
  border-top-left-radius: var(--radius-lg);
  border-top-right-radius: var(--radius-lg);
  padding-left: 0.4rem;
  padding-right: 0.4rem;
}
/* Third piece of the scrollbar-widget-hiding fix above (WebKit/Blink --
   Chrome, Edge, Safari -- have no CSS property for this, only this
   pseudo-element, which is why it can't just be another declaration in
   the rule above). */
.tab-bar.household-tab-bar::-webkit-scrollbar {
  display: none;
}
.tab-btn.household-tab-btn {
  font-size: 1.2rem;
  /* Inactive tabs read as visibly lighter than the active one (below) --
     the generic .tab-btn's own font-weight: 700 applies to every tab
     unconditionally, with no such distinction. */
  font-weight: 500;
  flex-shrink: 0;
  /* The per-child accent color (tab-accent-0 through -7 further down set
     --tab-accent-color; transparent is the fallback for any tab that
     somehow lacks one, rather than an invalid/missing color). Widened
     4px -> 8px (2026-09-07) so it reads as a clear color band rather
     than a thin line -- sits on a different edge than the top/right
     border added to inactive tabs in a later round (below), so the two
     don't overlap; where they meet at the top-left corner, the browser
     miters/curves between the two widths (8px left, 1px top) the same
     way it always does for any two differing border widths at a
     rounded corner (live-verified in requirements-audit.md). */
  border-left: 8px solid var(--tab-accent-color, transparent);
  /* "Physical tab" refinement (2026-09-07): shorter than the active tab's
     own padding-top below, so inactive tabs sit visibly recessed within
     the track (.household-tab-bar's align-items: flex-end above bottom-
     anchors every tab, so this is the only dimension that needs to
     differ). padding-BOTTOM deliberately stays the generic .tab-btn's
     unmodified 0.55rem in both this rule and .on below -- with a shared
     bottom padding and a shared bottom edge, each tab's text lands at the
     same height regardless of padding-top, so only the empty headroom
     above the label grows or shrinks, never the label's own position.
     Bug fix (2026-09-07): 0.35rem read as too little headroom above the
     label -- reported as the active tab's own top looking cut off
     (below, 0.9rem -> 1.2rem keeps that same relationship). Both values
     moved up by the same 0.3rem, so the row is taller everywhere but the
     *difference* between active and inactive -- the actual "how much
     taller is the active tab" signal -- is unchanged from what was
     already tuned and verified. */
  padding-top: 0.65rem;
  /* Bug fix (2026-09-07): an inactive tab's own fill was transparent
     (it was never given a background -- the track shows through), so
     legibility depended entirely on the track-vs-tab color contrast being
     strong enough on its own, and it wasn't -- adjacent inactive tabs
     were blending into each other and into the track, with nothing
     marking where one tab's box actually ends. Two independent fixes
     rather than one, so legibility doesn't ride on a single signal:
     a plain 1px border defines the shape directly on 3 sides -- top and
     right (left is already the 4px accent stripe above, which already
     reads as a strong edge in its own right; adding a second,
     differently-colored border on top of it there would just compete
     with it, not clarify anything) -- and an actual fill color, below,
     replaces the transparent default so the tab's own body reads as
     distinct from the track around it, not just its outline. Bottom
     border is deliberately left alone (still the generic .tab-btn's
     transparent border-bottom) -- that edge stays open toward the track,
     since an inactive tab never claims to "flow into" anything below it
     the way the active one does. --tab-inactive-border aliases
     --color-border per theme (themes.css has the full reasoning),
     already this app's own established "strong, unambiguous structural
     line" color rather than a new, potentially-too-subtle one -- and in
     high_contrast specifically, --color-border's own maximum contrast
     there is exactly what makes this unmistakable, with no separate
     override needed. Must be cancelled on .on below -- this rule and the
     active-tab rule share this same class, so without an explicit
     override the active tab would inherit this border too. */
  border-top: 1px solid var(--tab-inactive-border);
  border-right: 1px solid var(--tab-inactive-border);
  /* --color-bg-inset, not a new custom property -- already this app's
     own established "this needs to read as a distinct, recessed surface"
     token (sticky table headers, hover rows, the household-group row
     tint), so it's already tuned per theme to contrast against a plain
     background rather than being a fresh, untested value. Deliberately
     not --color-bg-elevated (the app's "raised card" token, usually
     pure white) -- elevated is *lighter* than --color-bg in every theme
     but dark, which would make an inactive tab look brighter/more
     prominent than the active one sitting right next to it, backwards
     from the intended hierarchy (track, darkest/most recessed <
     inactive tab, partway < active tab, fully risen to page-bg). */
  background: var(--color-bg-inset);
  /* Bug fix (2026-09-07): matches this rule's own background above, not
     left at the generic .tab-btn's transparent -- with the track's own
     border-bottom now removed (further up), a transparent border-bottom
     here would show the *track's* background through it instead
     (--tab-track-bg, a different color from this tab's own
     --color-bg-inset fill), a visible 2px band in the wrong shade right
     at this tab's own bottom edge. Matching it to this tab's own fill
     keeps its whole body one consistent color, top to bottom. */
  border-bottom-color: var(--color-bg-inset);
  /* Rounded top corners on every tab, active or inactive -- reads as an
     actual tab shape rather than a plain rectangle, matching the
     track's own top-corner rounding above (and, since this rule now also
     gives inactive tabs their own --color-bg-inset fill above, curves
     that fill's own top corners too, not just the border-left stripe).
     Bottom stays square (default) on every tab: rounding it
     would put a gap in the flat bottom edge the seamless-connection
     trick (.on below) and the shared-border-line trick both depend on. */
  border-top-left-radius: var(--radius-md);
  border-top-right-radius: var(--radius-md);
}
.tab-btn.household-tab-btn.on {
  /* Full-strength text color, not the generic .tab-btn.on's accent color
     -- the accent already lives in this tab's own left-edge stripe above,
     not the text, so re-using it here too would be redundant emphasis on
     top of everything else this rule already does to mark it active. */
  color: var(--color-fg);
  font-weight: 700;
  /* Seamless connection to the content panel below: matches
     .audit-sticky-heading's own background (base.css, further down) --
     so only the active tab's own body reads as page-bg-colored, matching
     the panel below it exactly, rather than the track color every other
     tab sits against. This tab's own border-bottom-color is set to match
     too, purely for consistency with the rest of its own fill (the bar's
     own border-bottom that this used to paint over is gone now -- see
     the bug-fix comment on .tab-bar.household-tab-bar's own border-bottom
     further up, which covers why removing it there was the actual fix,
     not this rule). */
  background: var(--color-bg);
  border-bottom-color: var(--color-bg);
  /* Design change (2026-09-07): the active tab now gets the same
     top/right border treatment as inactive tabs (the base rule above,
     --tab-inactive-border), just thicker -- 2px instead of 1px, per an
     explicit later request reversing this rule's own original "no
     border on the active tab, redundant against the shadow" stance.
     Overrides the base rule's 1px rather than merely not-cancelling it,
     since this class is additive on top of that rule and the two
     widths would otherwise conflict (last one in the cascade would
     silently win over the other, not a real "both apply" outcome). */
  border-top: 2px solid var(--tab-inactive-border);
  border-right: 2px solid var(--tab-inactive-border);
  background: var(--color-bg);
  border-bottom-color: var(--color-bg);
  /* Bug fix (2026-09-07): -3px, not the generic .tab-btn's own -2px --
     -2px was calibrated to exactly cancel .tab-bar's 2px bottom border,
     landing this tab's own bottom edge exactly flush with the panel
     below rather than overlapping it. Exactly-flush isn't quite enough
     in practice: the panel and the tab bar are positioned independently
     (both position: sticky, off two separately-measured JS heights), so
     their edges can each round to a slightly different sub-pixel value
     at paint time even when the layout math says they're identical,
     showing as a hairline gap -- confirmed as the project owner's own
     live report, not reproduced by this session's own testing but
     plausible on a real display/zoom this session's browser tool
     doesn't reproduce, and cheap to guard against either way. One extra
     pixel of overlap (this tab's own background/border-bottom color
     already matches the panel's exactly, so the overlap paints over
     itself invisibly) absorbs that without visibly shortening the tab --
     the difference is one page-bg-colored pixel sitting on top of more
     of the same color. */
  margin-bottom: -3px;
  /* Subtle elevation -- every theme's own --shadow-sm (themes.css) is
     already tuned for that theme's background (a soft shadow for light
     themes, a stronger one for dark ones to stay visible), so no
     additional per-theme tuning is needed here specifically for this use.
     high_contrast's own --shadow-sm may not read as strongly as the
     others -- expected and accepted there; bold text and the seamless
     connection above still carry the active-state signal on their own.
     Deliberately no full border/outline on top of this -- font-weight,
     the seamless connection, and this shadow together are the complete
     active-tab treatment. */
  box-shadow: var(--shadow-sm);
  /* Bug fix (2026-09-07): box-shadow with no `inset` keyword casts in
     every direction, including straight down onto whatever sits right
     below this tab -- exactly the seam the rule above works to erase.
     Confirmed live (screenshot from the project owner, then reproduced
     and isolated here by toggling box-shadow off and back on) that the
     shadow itself, not a color or border mismatch, was the visible line
     at the seam: matching border/background colors alone don't stop a
     drop shadow from still being painted on top of that matching color,
     which reads as a seam even when nothing is technically
     mismatched. clip-path trims only the bottom of this tab's own
     paintable area back to its exact border edge -- generously
     negative (-20px, well past any theme's actual --shadow-sm extent,
     the largest being high_contrast's 2px offset + 6px blur) on the
     other 3 sides so the shadow stays fully visible everywhere the
     seamless-connection rule above doesn't apply. Margin isn't part of
     an element's own paintable box, so this has no effect on the
     margin-bottom: -3px overlap above -- that still repositions the
     whole box exactly as before; this only ever trims paint that would
     land outside the box's own bottom edge. */
  clip-path: inset(-20px -20px 0 -20px);
  /* "Physical tab" refinement (2026-09-07): 0.55rem (~8.8px) taller than
     the inactive .household-tab-btn's own 0.65rem above (both bumped up
     from an originally-tighter 0.35rem/0.9rem pair by the same +0.3rem,
     which read as too little headroom -- reported as this tab's own top
     looking cut off -- without changing the 0.55rem difference between
     them, already tuned and verified). Combined with align-items:
     flex-end on the container and the shared, unmodified padding-bottom
     both rules inherit from the generic .tab-btn, this is the whole
     mechanism -- the container's own height auto-sizes to fit this
     (tallest) tab, so THIS tab's top edge sits just below the track's own
     top edge by the container's own padding-top (above) -- a smaller
     gap than every inactive tab's own, shorter box leaves above itself,
     but deliberately no longer zero (a design correction: the active
     tab used to sit fully flush with the track's top, and per the
     project owner's own follow-up, some track should read above the
     active tab too, just less than above the inactive ones). That's
     what reads as "this tab pulled forward out of the track, but still
     sitting in it," not the shadow above on its own. The border-left
     accent stripe (above) is a plain border, so it automatically
     extends to match this tab's own taller box with no separate rule
     needed. */
  padding-top: 1.2rem;
}
/* This child's own content-panel heading (_audit-phase-child.php) gets
   the exact same accent color as its tab button, via the same
   tab-accent-N class and the --tab-accent-color indirection below --
   the visual link between "this tab" and "this is that tab's content"
   that a plain matching-hue stripe alone wouldn't make explicit without
   the tab and the header being adjacent, which they aren't once the tab
   bar is scrolled off after the page loads. */
.household-accent-header {
  /* Widened 4px -> 8px alongside the tab's own stripe (base rule above)
     (2026-09-07) -- the request that widened the tab's stripe didn't
     explicitly name this header, but this rule's whole point (comment
     above) is matching the tab's own stripe exactly to link "this tab"
     with "this is that tab's content"; leaving this one at the old
     width would have left the two visibly mismatched despite sharing
     the same color. */
  border-left: 8px solid var(--tab-accent-color, transparent);
  /* Bug fix (2026-09-07): the child's name/number was rendering flush
     against the stripe with zero gap -- .page-heading (the base class
     this is layered on top of) has no padding of its own to fall back
     on, since it was never designed with a left border in mind. */
  padding-left: 0.6rem;
}

/* One of 8 per-theme accent colors (--tab-accent-0 through -7, themes.css)
   per household member, index wrapped past 8 (Section 13.1's sibling
   slots) -- one indirection variable (--tab-accent-color) rather than
   writing border-left-color separately for .household-tab-btn and
   .household-accent-header at each of the 8 indexes, so each theme's
   actual color values are declared exactly once, in exactly one file. */
.tab-accent-0 { --tab-accent-color: var(--tab-accent-0); }
.tab-accent-1 { --tab-accent-color: var(--tab-accent-1); }
.tab-accent-2 { --tab-accent-color: var(--tab-accent-2); }
.tab-accent-3 { --tab-accent-color: var(--tab-accent-3); }
.tab-accent-4 { --tab-accent-color: var(--tab-accent-4); }
.tab-accent-5 { --tab-accent-color: var(--tab-accent-5); }
.tab-accent-6 { --tab-accent-color: var(--tab-accent-6); }
.tab-accent-7 { --tab-accent-color: var(--tab-accent-7); }

/* ---------- Modals ---------- */

.overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  z-index: 1000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 1.25rem;
}
.overlay.is-open {
  display: flex;
}
.modal {
  background: var(--color-bg-elevated);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  width: 100%;
  max-width: 540px;
  max-height: 92vh;
  display: flex;
  flex-direction: column;
  /* Without this, max-height on a flex column doesn't reliably constrain a
     flex-basis:0% child (.modal-body) in every engine -- the child's
     content can still force the column past 92vh instead of triggering
     .modal-body's own overflow-y: auto. overflow: hidden here is what
     actually makes max-height bite, so .modal-body is what ends up
     scrolling internally rather than content spilling past the modal. */
  overflow: hidden;
}
.modal-lg {
  max-width: 760px;
}
.modal-sm {
  max-width: var(--modal-sm-max-width);
}
/* The one PDF modal (templates/layout/main.php, pdf-modal.js): the file name on the batch-review page and the Source PDF button of the wish list
   modal open it. A page of a form needs height more than anything, so it is as tall as the 92vh cap of every modal allows and its body hands every
   pixel to the viewer (the iframe pdf-modal.js builds), with no padding around it. A long file name in the heading is cut with an ellipsis rather
   than pushing the X off the modal. */
.modal-pdf {
  max-width: 900px;
  height: 92vh;
}
.modal-pdf .modal-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding-right: 0.75rem;
}
.modal-pdf .modal-body {
  padding: 0;
  overflow: hidden;
  display: flex;
}
.pdf-modal-frame {
  flex: 1;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
/* PreStage's (renamed from Intake 2026-09-07) per-child modal specifically,
   50% wider than .modal-lg's usual 760px -- its rows (Item description,
   checkboxes, Method, and the method-specific detail fields + Save) need
   more than the donor-picker modal .modal-lg is otherwise sized for;
   scoped by id rather than widening .modal-lg itself, which
   #donor-picker-modal also uses. */
#prestage-modal .modal-lg {
  max-width: 1140px;
}
/* Donors' Lists modal (donor-grid.js's renderWishesTable): 5 columns of
   real content (#, Item, Method, Tracking #, Received) per assigned wish
   list, each rendered as its own card -- measured needing 733px just for
   one item's table at the standard 760px .modal-lg, 71px past what was
   actually available after the modal's own padding, forcing a second,
   inner horizontal scrollbar. Scoped by id, same reasoning as
   #prestage-modal above. */
#donor-lists-modal .modal-lg {
  max-width: 1000px;
}
/* Every form-based modal wraps .modal-body/.modal-ftr in a <form> (CSRF
   field, POST target) that sits between them and .modal -- an unstyled
   <form> is just a block box, so without this it renders at its full
   content height regardless of .modal's max-height, and .modal-body's own
   flex/overflow rules (further down) never get a constrained box to act
   on. Making the form itself a flex column (matching .modal) restores the
   chain: .modal caps the total height, the form fills whatever's left
   after .modal-hdr, and .modal-body -- the form's own flex:1 child --
   is what ends up scrolling. Read-only modals with no <form> (e.g. the
   Lists modal) already have .modal-body as a direct child of .modal and
   don't need this. */
.modal > form {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
.modal-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.9rem 1.25rem;
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}
.modal-title {
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 700;
}
.modal-x {
  background: none;
  border: none;
  color: var(--color-fg-muted);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  padding: 1px 5px;
  border-radius: var(--radius-sm);
}
.modal-x:hover {
  background: var(--color-bg-inset);
  color: var(--color-fg);
}
.modal-body {
  padding: 1.25rem;
  overflow-y: auto;
  flex: 1;
  /* Flex items default to min-height: auto, which lets a tall .modal-body
     grow past its flex share instead of shrinking to it -- without this,
     overflow-y: auto never actually engages, and .modal (which has no
     overflow of its own) lets content spill out below its rounded box
     instead of scrolling internally within max-height: 92vh. Caught by the
     Donor Add/Edit modal's field count once it grew past what fit. */
  min-height: 0;
}
.modal-ftr {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
  padding: 0.75rem 1.25rem;
  border-top: 1px solid var(--color-border);
  flex-shrink: 0;
}

/* ---------- Alerts / flash ---------- */

.alert {
  padding: 0.85rem 1.1rem;
  border-radius: var(--radius-md);
  margin-bottom: 1rem;
  border: 1px solid transparent;
  font-size: 0.9rem;
}
.alert-error   { background: var(--color-danger-bg);  color: var(--color-danger);  border-color: var(--color-danger); }
.alert-success { background: var(--color-success-bg); color: var(--color-success); border-color: var(--color-success); }
.alert-warning { background: var(--color-warning-bg); color: var(--color-warning); border-color: var(--color-warning); }

/* ---------- Dashboard stat tiles ---------- */

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 0.85rem;
  margin-bottom: 1.5rem;
}
.stat-tile {
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: 0.7rem 1rem;
  text-align: center;
}
.stat-tile__value {
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 800;
  line-height: 1.1;
}
/* Only present on tiles that show a count alongside its share of some
   total (e.g. "48% of Assemblies Completed") -- sits directly below the
   big value, under the tile's label at the top. */
.stat-tile__sublabel {
  color: var(--color-fg-muted);
  font-size: 0.78rem;
  font-weight: 600;
  margin-top: 0.15rem;
}
.stat-tile__label {
  color: var(--color-fg-muted);
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.3rem;
}

/* ---------- Upload dropzone ---------- */

.dropzone {
  border: 2px dashed var(--color-border);
  border-radius: var(--radius-md);
  padding: 2.5rem 1.5rem;
  text-align: center;
  color: var(--color-fg-muted);
  background: var(--color-bg-elevated);
  cursor: pointer;
}
.dropzone.is-dragover {
  border-color: var(--color-accent);
  background: var(--color-accent-bg);
  color: var(--color-fg);
}

/* Status column on the upload page's live file list (upload.js's
   addFileRow()) and the Review Import Batch page (templates/upload/
   batch.php) -- both can carry a long rejection reason or validation
   issue message with no natural line break. th, td's shared nowrap
   default (above) would otherwise force the row wider instead of
   taller, growing the table past the viewport and pushing the Action
   column (Resolve.../the Accept-Reject dropdown) out of view --
   confirmed by reproducing it directly: a long enough message pushed
   Action off the right edge entirely, reachable only via .table-wrap's
   own horizontal scrollbar. Wrapping here instead keeps every column,
   including Action, always on screen with no scrolling needed. */
.upload-status-cell {
  white-space: normal;
  overflow-wrap: break-word;
}

/* The overall progress of an upload ("Uploading 12 of 40 files...", upload.js's updateProgress()), so the operator can see when a large batch has
   finished. A native <progress> (no inline width to set), restyled with the theme's own tokens: the track is the inset colour, the fill the
   primary colour, and the fill turns to the success colour when the last upload has ended. */
.upload-progress {
  margin-top: 1rem;
}
.upload-progress__text {
  margin: 0 0 0.4rem;
  font-weight: 600;
}
.upload-progress__bar {
  display: block;
  width: 100%;
  height: 1rem;
  appearance: none;
  -webkit-appearance: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg-inset);
  overflow: hidden;
}
.upload-progress__bar::-webkit-progress-bar {
  background: var(--color-bg-inset);
}
.upload-progress__bar::-webkit-progress-value {
  background: var(--color-primary);
}
.upload-progress__bar::-moz-progress-bar {
  background: var(--color-primary);
}
.upload-progress__bar.is-complete::-webkit-progress-value {
  background: var(--color-success);
}
.upload-progress__bar.is-complete::-moz-progress-bar {
  background: var(--color-success);
}

/* A button that is a link with no href yet ("Review this batch" while uploads are still running): dimmed like .btn:disabled. */
.btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
}
.btn[aria-disabled="true"]:hover {
  filter: none;
}
/* ---------- Diff view (duplicate resolution) ---------- */

.diff-row {
  display: grid;
  grid-template-columns: 180px 1fr 1fr;
  gap: 0.75rem;
  padding: 0.6rem 0.2rem;
  border-bottom: 1px solid var(--color-border-muted);
  align-items: start;
}
.diff-row--head {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: var(--color-fg-muted);
  border-bottom: 2px solid var(--color-border);
}
.diff-row__field { font-weight: 600; font-size: 0.85rem; }
.diff-old { color: var(--color-danger); text-decoration: line-through; }
.diff-new { color: var(--color-success); font-weight: 600; }

/* ---------- Filter bar (server-side search forms) ---------- */

.filter-bar {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  align-items: end;
}
/* Tighter than a plain .card's default 1.25rem padding/margin -- a search
   form doesn't need as much breathing room as a content card, and every
   server-side-paginated listing puts one directly above its table, so the
   saved space directly means more rows visible without scrolling. */
.filter-bar.card {
  padding: 0.85rem 1.25rem;
  margin-bottom: 0.75rem;
}
.filter-bar .field {
  margin-bottom: 0;
  min-width: 160px;
}

/* Admin's Audit Log / Application Log filter bars, and Wish Lists/Audit/
   PreStage/Donor Assignment's: more fields than a 160px-per-field floor can
   fit on one row, but every field here already carries an explicit
   .input-narrow/.input-medium/.input-wide width, so the floor is just
   extra empty space -- drop it instead of letting fields wrap. Tighter
   gap too, so the Search/Clear buttons at the end still fit alongside a
   full row of fields at a moderate (not just wide) window size. */
.filter-bar--compact .field {
  min-width: 0;
}
.filter-bar--compact {
  gap: 0.4rem;
}
/* The one free-text field earns back the room the fixed-width fields gave up. */
.field-grow {
  flex: 1 1 200px;
}

/* Numbered page-link row under a search/listing table (Wish Lists/Audit/
   PreStage, Admin's Audit Log tab). */
.pagination-nav {
  margin-top: 1rem;
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  align-items: center;
}
.pagination-ellipsis {
  color: var(--color-fg-subtle);
  padding: 0 0.2rem;
}

/* Wraps .pagination-nav, the "Showing X - Y of Z" count, and the
   records-per-page dropdown -- every server-side-paginated listing (Wish
   Lists/Audit/PreStage/Donor Assignment/Admin Audit Log) uses this same
   layout, at both the top and bottom of the list. A 3-column grid (not
   flex + space-between) so the count sits genuinely centered between the
   nav and the dropdown regardless of how wide either one is -- the nav's
   width varies with page count, so a flex layout's "space between" would
   only even out the gaps, not actually center the middle item. Either
   outer column can be empty (a single page has no nav; the top bar's
   count may briefly be absent) without breaking alignment, since each
   template always renders exactly 3 children here, substituting an empty
   <div> for whichever slot has nothing to show. */
.pagination-bar {
  margin-top: 0.4rem;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.75rem;
}
.pagination-bar .pagination-nav {
  margin-top: 0;
}
.pagination-count {
  margin: 0;
  text-align: center;
  justify-self: center;
}
.page-size-field {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.85rem;
  color: var(--color-fg-muted);
  justify-self: end;
}

/* Admin > Application Log: message text is free-form and can be long
   (stack traces, JSON context), so it wraps within its own cell instead of
   forcing the table wider than its container -- same reasoning as the
   donors grid's wrapping columns. */
.log-message {
  white-space: normal;
  overflow-wrap: break-word;
  font-family: var(--font-mono, ui-monospace, 'Courier New', monospace);
  font-size: 0.82rem;
}

/* Admin > Audit Log's Details modal: raw before/after JSON, preformatted. */
.audit-details-json {
  background: var(--color-bg-inset);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0.6rem 0.8rem;
  margin: 0 0 0.75rem;
  font-family: var(--font-mono, ui-monospace, 'Courier New', monospace);
  font-size: 0.82rem;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 220px;
  overflow-y: auto;
}

/* Audit page: the page-heading (child name/#/status/action buttons) sticks
   below the site header as the page scrolls, and the Source PDF panel
   sticks below both of them -- top offsets come from --sticky-header-height/
   --sticky-audit-heading-height, which audit-sticky.js measures and sets on
   :root. A fixed px guess would be wrong depending on role (an
   Administrator's extra Admin nav link can wrap .app-header to a second
   line that Manager/Auditor, who can also reach this page, don't get) and
   on viewport width. The px fallback values here only matter before that
   script's first paint.
   --tab-bar-height (2026-09-07, wish-list tabs enhancement): the household
   tab bar (.household-tab-bar below) is now sticky too, stacked between
   the site header and this heading -- audit-sticky.js sets this to the
   tab bar's real measured height on a household page, or 0px when there
   is none (a singleton child's page), so this same calc() works
   unconditionally in both cases without a separate CSS branch. */
.audit-sticky-heading {
  position: sticky;
  top: calc(var(--sticky-header-height, 58px) + var(--tab-bar-height, 0px));
  z-index: 800;
  background: var(--color-bg);
  padding-top: 0.75rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--color-border);
}

/* Establishes the query container that .pdf-frame's cqw units below
   measure against -- has to be the iframe's own direct parent, not
   .grid-2col itself, since the iframe is only ~1fr of that grid (roughly
   half its width, not all of it). */
.pdf-frame-wrap {
  container-type: inline-size;
  position: sticky;
  top: calc(var(--sticky-header-height, 58px) + var(--tab-bar-height, 0px) + var(--sticky-audit-heading-height, 60px));
}
/* Every sample form's MediaBox is [0 0 612 792] -- exactly US Letter
   (8.5in x 11in @ 72pt/in). A plain aspect-ratio (width * 11/8.5) undersizes
   the frame: Chrome's built-in PDF viewer renders its own toolbar (page
   nav/zoom/download, ~48px) INSIDE the iframe, on top of the page canvas --
   that toolbar eats into the height aspect-ratio assumed was all page
   content, so the bottom of the page still needed a scrollbar. Sizing via
   cqw (the iframe's own container width, not the viewport) plus a fixed
   allowance for that toolbar fixes both: the ratio term keeps the page
   itself uncropped at any column width, and the +48px reserves room for
   the toolbar on top of it instead of stealing from the page area. */
.pdf-frame {
  width: 100%;
  height: calc(129.41cqw + 48px);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

/* ---------- Login page ---------- */

.login-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
  padding: 1.5rem;
}
.login-card {
  width: 100%;
  max-width: 400px;
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-top: 4px solid var(--brand-teal-color);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 2.1rem;
}
.login-logo {
  font-family: var(--font-display);
  font-size: 1.7rem;
  font-weight: 800;
  text-align: center;
  letter-spacing: 0.03em;
  margin-bottom: 0.2rem;
  color: var(--color-fg);
  /* Same glow as .app-header__brand's "Spirit of Christmas", in light gray instead of white. */
  text-shadow: 0 0 6px #d3d3d3, 0 0 12px #d3d3d3;
}
.login-logo span {
  color: var(--color-accent);
}
.login-sub {
  text-align: center;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--color-fg-muted);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  margin-bottom: 1.75rem;
}
.login-hint {
  text-align: center;
  margin-top: 1rem;
  margin-bottom: 0;
}
.login-version {
  text-align: center;
  margin-top: 0.75rem;
  margin-bottom: 0;
  opacity: 0.75;
}

/* ---------- Public Sponsor Request page (Sponsor Requests, Stage B) ----------
   Reuses .login-page / .login-card -- the same card as the sign-in page -- so
   only what differs is defined here. Mobile-first: the donors using this are on
   phones, so on a narrow screen the card is top-aligned (a centered one sits
   behind the on-screen keyboard) and uses nearly the full width, which also
   leaves room for Cloudflare's 300px-wide widget. From 480px it is the same
   centered card as the sign-in page. */

.sponsor-page {
  min-height: 0;
  align-items: flex-start;
  padding: 0; /* main already pads 1rem on a phone */
}
.sponsor-card {
  max-width: 440px;
  padding: 1rem;
}
@media (min-width: 480px) {
  .sponsor-page {
    min-height: 100vh;
    align-items: center;
    padding: 1.5rem;
  }
  .sponsor-card {
    padding: 2.1rem;
  }
}
.sponsor-title {
  font-family: var(--font-display);
  font-size: 1.45rem;
  font-weight: 800;
  line-height: 1.25;
  text-align: center;
  margin: 0.25rem 0 0.4rem;
  color: var(--color-fg);
}
.sponsor-step-heading {
  font-size: 1.1rem;
  text-align: center;
  margin: 0.5rem 0 0.25rem;
  color: var(--color-fg);
}
.sponsor-lead {
  text-align: center;
  color: var(--color-fg-muted);
  margin: 0 0 1.25rem;
}
.sponsor-address {
  overflow-wrap: anywhere;
  color: var(--color-fg);
}
.sponsor-closed-hint {
  text-align: center;
}
/* Reserves the widget's height so the page does not jump when Cloudflare's script
   finishes loading. The widget is a fixed 300px wide: it fits a 375px phone as is,
   is scaled down for the narrower ones, and as a last resort scrolls inside its own
   box rather than pushing the whole page sideways. */
/* The staff sign-in page's human check (v0.6.5): Cloudflare's widget is 65px tall and 300px wide, so the space is held before it loads and the
   button does not jump. */
.login-turnstile {
  min-height: 65px;
  margin: 0 0 1rem;
}
.sponsor-turnstile {
  min-height: 65px;
  max-width: 100%;
  margin: 0 0 1rem;
  overflow-x: auto;
}
@media (max-width: 374px) {
  .sponsor-turnstile {
    zoom: 0.9;
  }
}
@media (max-width: 339px) {
  .sponsor-turnstile {
    zoom: 0.8;
  }
}
.sponsor-code-input {
  width: 100%;
  min-height: 3.25rem;
  padding: 0.6rem 0.5rem;
  font-size: 1.75rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.35em;
  text-indent: 0.35em; /* balances the trailing letter-space so the digits centre visually */
  text-align: center;
}
.sponsor-actions {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: 1.25rem;
}
.sponsor-card .btn {
  min-height: 2.75rem; /* 44px: a comfortable touch target */
}
.sponsor-change-email {
  text-align: center;
}

/* ---------- Public Sponsor Request page (Stage D): the form and the steps after verification ----------
   Same mobile-first approach: everything is one column on a phone, and the wider card from 480px lets
   the name fields and the city / state / ZIP row sit side by side on a big screen. */

.sponsor-card--wide {
  max-width: 640px;
}
.sponsor-lead--tight {
  margin-bottom: 0.5rem;
}
.sponsor-confirmed {
  text-align: center;
  color: var(--color-fg-muted);
  margin: 0 0 0.15rem;
}
.sponsor-change-email--tight {
  margin: 0 0 1rem;
}
.sponsor-muted {
  color: var(--color-fg-subtle);
}
.sponsor-section {
  margin: 0 0 1.25rem;
  padding: 0.9rem 1rem 0.25rem;
}
.sponsor-section > legend,
.sponsor-choices > legend {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.95rem;
  padding: 0 0.4rem;
  color: var(--color-fg);
}
.sponsor-choices {
  margin: 0 0 1rem;
  padding: 0.5rem 0.9rem 0.6rem;
}
.sponsor-choices .checkbox-field {
  margin: 0.4rem 0;
}
.sponsor-choices .checkbox-field label {
  font-size: 0.95rem;
}
/* v0.6.5: the choices of a Sponsor Request form sit side by side (gender) or in two columns (age preference) -- on the public page and in
   the staff Add / Modify form -- instead of one under another. The list wraps a fieldset's .checkbox-field rows. */
.sponsor-choices__list {
  margin: 0.4rem 0 0.2rem;
}
.sponsor-choices__list .checkbox-field {
  margin: 0;
}
.sponsor-choices__list .checkbox-field input {
  flex: none;
}
.sponsor-choices__list--side-by-side {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 2rem;
}
.sponsor-choices__list--two-columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem 1.25rem;
}
.sponsor-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 0.75rem;
}
.sponsor-row--address {
  grid-template-columns: 1fr 1fr;
}
.sponsor-row__city {
  grid-column: 1 / -1;
}
@media (min-width: 560px) {
  .sponsor-row {
    grid-template-columns: 1fr 1fr;
  }
  .sponsor-row--address {
    grid-template-columns: 2fr 1.6fr 1fr;
  }
  .sponsor-row__city {
    grid-column: auto;
  }
}
.field-error {
  color: var(--color-danger);
  font-size: 0.85rem;
  font-weight: 600;
  margin: 0.3rem 0 0;
}
.field--error input,
.field--error select,
.field--error textarea,
.sponsor-choices.field--error {
  border-color: var(--color-danger);
}
.sponsor-errors__title {
  margin: 0;
  font-weight: 700;
}
.sponsor-errors ul {
  margin: 0.4rem 0 0 1.1rem;
  padding: 0;
}
.sponsor-errors a {
  color: inherit;
  text-decoration: underline;
}
.sponsor-errors:focus {
  outline: 2px solid var(--color-danger);
  outline-offset: 2px;
}
.sponsor-count.is-over {
  color: var(--color-danger);
  font-weight: 700;
}
.sponsor-timer {
  text-align: center;
  margin-top: 0.75rem;
}
.sponsor-donor-card {
  margin: 0 0 1.25rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--brand-teal-color);
  border-radius: var(--radius-md);
  background: var(--color-bg);
}
.sponsor-donor-name {
  margin: 0 0 0.4rem;
  font-size: 1.1rem;
  font-weight: 700;
  overflow-wrap: anywhere;
}
.sponsor-donor-line {
  margin: 0 0 0.4rem;
  overflow-wrap: anywhere;
}
.sponsor-compare {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem;
  margin: 0 0 1.25rem;
}
@media (min-width: 560px) {
  .sponsor-compare {
    grid-template-columns: 1fr 1fr;
  }
}
.sponsor-compare__side {
  padding: 0.8rem 1rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.sponsor-compare__side--suggested {
  border: 2px solid var(--brand-teal-color);
  background: var(--color-bg);
}
.sponsor-compare__title {
  margin: 0 0 0.35rem;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-fg-muted);
}
.sponsor-compare__address {
  margin: 0;
  overflow-wrap: anywhere;
}
.sponsor-summary {
  margin: 0 0 1.25rem;
}
.sponsor-summary dt {
  margin-top: 0.7rem;
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-fg-muted);
}
.sponsor-summary dd {
  margin: 0.1rem 0 0;
  overflow-wrap: anywhere;
}
.sponsor-summary__text {
  white-space: pre-wrap;
}

/* ---------- Sponsor Requests, staff pages (Stage E) ---------- */

/* The listing: a fixed layout, so Special Requests takes whatever room the other columns leave. The
   min-width is what makes a narrow screen scroll the table sideways inside .table-wrap instead of
   squeezing that last column to nothing. */
#sponsor-requests-tbl {
  table-layout: fixed;
  min-width: 64rem;
}
.sponsor-req-donor-cell {
  width: 14rem;
  white-space: normal;
  overflow-wrap: anywhere;
}
/* The donor's name is a button now (it opens the wish-lists modal, v0.6.5); a button centres its text, which showed on a name
   that wraps in this narrow column. Left-aligned like every other name here. */
.sponsor-req-donor-cell .link-button {
  text-align: left;
}
.sponsor-req-number-cell {
  width: 6.5rem;
  white-space: normal;
}
.sponsor-req-pref-cell {
  width: 8.5rem;
  white-space: normal;
}
.sponsor-req-notes-cell {
  white-space: normal;
  overflow-wrap: anywhere;
}
.sponsor-req-status-cell {
  width: 9.5rem;
  white-space: normal;
}
.sponsor-req-actions-cell {
  width: 5.5rem;
}
/* v0.6.5: a Manager or Administrator also gets Edit and Delete, so the column is wide enough for all three buttons on one line. */
.sponsor-req-actions-cell--manage {
  width: var(--sponsor-row-actions-width);
}
.sponsor-req-actions {
  display: flex;
  align-items: center;
  gap: var(--sponsor-row-actions-gap);
}

/* The donor picker's footer line (workflow/_donor-picker.php, only where a page gives it one), at the left of the footer, Cancel at the right. */
.donor-picker-footer-note {
  margin: 0 auto 0 0;
  align-self: center;
  font-size: 0.85rem;
}

/* The Add / Modify form and the delete check inside their modals (workflow/_sponsor-request-form.php, _sponsor-request-delete-check.php). */
.sponsor-request-manage-donor {
  margin: 0 0 0.75rem;
  line-height: 1.3;
}
.sponsor-request-provenance {
  margin: 0 0 0.3rem;
}
.sponsor-request-delete-form p,
.sponsor-request-delete-denied p {
  margin: 0 0 0.75rem;
  line-height: 1.5;
}
.sponsor-request-form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1rem;
}
/* [data-form-error] is hidden until the server answers with a message; a button inside it ("Show the latest") sits on its line. */
.sponsor-request-manage-form [data-form-error] .btn {
  margin-left: 0.4rem;
}
.sponsor-assigned-count {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.8rem;
  color: var(--color-fg-muted);
}
/* A donor's own words keep their line breaks. */
.sponsor-special-text {
  white-space: pre-line;
}
.sponsor-special-more summary {
  cursor: pointer;
}
.sponsor-special-more[open] summary {
  margin-bottom: 0.3rem;
}

/* The per-donor page (v0.6.5 redesign): ONE sticky block under the site header -- the donor's name and Done, the request's facts
   and the filters -- above the shared wish-list table with a tick-box column.

   Sticky only where there is room for it. A custom property cannot be read by a media query, so the breakpoint lives here, in
   exactly one place: at least 60rem wide and 38rem tall (below that a block this tall would leave no room for the table, and the
   page simply scrolls as it always did). Its top is --sticky-header-height, measured by sticky-offsets.js, never a guess; it sits
   above the table and below every modal overlay (the site header is 900, overlays are 1000). The background is opaque, so rows
   scroll away behind it instead of showing through. */
.sponsor-sticky-block {
  background: var(--color-bg);
  padding: 0.4rem 0 0.5rem;
  margin-bottom: 0.5rem;
}
@media (min-width: 60rem) and (min-height: 38rem) {
  .sponsor-sticky-block {
    position: sticky;
    top: var(--sticky-header-height, 58px);
    z-index: 800;
    border-bottom: 1px solid var(--color-border);
    box-shadow: var(--shadow-sm);
  }
}
.sponsor-request-card {
  margin-bottom: 0;
  padding: 0.4rem 0.9rem 0.5rem;
}

/* C-2/C-3/C-4: a title row (donor name, Done at the right) over a label row and a value row, five columns. The rightmost column is
   the status: its label row is the badge, its value row "N of M assigned", under the Done button. Special requests sits right of Age. */
.sponsor-request-head {
  display: grid;
  grid-template-columns: var(--sponsor-head-columns);
  column-gap: 1.25rem;
  row-gap: 0.1rem;
  align-items: start;
}
.sponsor-request-head__title {
  grid-column: 1 / 5;
  grid-row: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
}
.sponsor-request-head__title h1 {
  margin: 0;
  overflow-wrap: anywhere;
}
.sponsor-request-head__done {
  grid-column: 5;
  grid-row: 1;
  justify-self: end;
  /* Yellow (owner, 2026-10-16): tokens, defined in every theme, so the text and the outline stay readable on each background. */
  background: var(--color-done-bg);
  color: var(--color-done-fg);
  border-color: var(--color-done-border);
}
.sponsor-request-head__done:hover,
.sponsor-request-head__done:focus-visible {
  background: var(--color-done-hover-bg);
  color: var(--color-done-fg);
  border-color: var(--color-done-border);
}
/* The <dl> and each fact's <div> take no box of their own: every <dt> and <dd> is a cell of the head's grid, a label in row 2 and a value
   in row 3 of its fact's column. */
.sponsor-request-facts,
.sponsor-fact {
  display: contents;
}
.sponsor-fact--requested { --fact-column: 1; }
.sponsor-fact--gender { --fact-column: 2; }
.sponsor-fact--age { --fact-column: 3; }
.sponsor-fact--special { --fact-column: 4; }
.sponsor-fact--status { --fact-column: 5; }
.sponsor-fact dt {
  grid-column: var(--fact-column);
  grid-row: 2;
  margin: 0.2rem 0 0;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-fg-muted);
}
.sponsor-fact dd {
  grid-column: var(--fact-column);
  grid-row: 3;
  margin: 0;
  overflow-wrap: anywhere;
  line-height: 1.4;
}
/* The status column's label row is the badge itself, not a caption (C-2). */
.sponsor-fact--status dt {
  margin-top: 0;
  letter-spacing: 0;
  text-transform: none;
}
/* Special requests run to 1,000 characters: three lines show and the rest scrolls inside this box (it takes keyboard focus), so the
   sticky block can never be pushed taller. */
.sponsor-special-box {
  max-height: calc(3 * 1.4em);
  overflow-y: auto;
  padding-right: 0.25rem;
}
/* C-7: bigger than the app-wide badge, for this card only -- .badge itself is used everywhere. */
.sponsor-request-card .badge {
  font-size: var(--sponsor-badge-font-size);
  padding: var(--sponsor-badge-padding);
  min-height: var(--sponsor-badge-min-height);
}

/* C-1/C-5: the filter bar in its compact form, with the reserved Assign selected cell at its LEFT (moved from the right at the owner's request, 2026-10-16). */
.sponsor-filter-row {
  display: flex;
  align-items: flex-end;
  gap: 0.6rem;
  margin-top: 0.4rem;
}
.sponsor-filter-row .filter-bar {
  flex: 1 1 auto;
  min-width: 0;
}
/* Assign Donors' filter bar (v0.6.5): an Administrator sees eight fields, and the two labelled buttons (workflow/_filter-buttons.php) are about
   100px wider than the icon-only pair they replaced, which pushed the buttons onto a second row at 1280-1366px. The fields are all short (a child
   number, a first name, a county), so they give that room back; the Sponsor Requests per-donor page has its own widths below. */
.filter-bar--assign input.input-medium,
.filter-bar--assign select.input-medium {
  width: 120px;
}
.filter-bar--assign input#f-childNumber,
.filter-bar--assign select#f-genderCode {
  width: 110px;
}
.filter-bar--assign select.input-wide {
  width: 160px;
}
.filter-bar--assign select#f-programYearId {
  width: 125px;
}
.filter-bar--sponsor-sticky.card {
  margin-bottom: 0;
  padding: 0.35rem 0.75rem 0.45rem;
  gap: 0.35rem;
}
.filter-bar--sponsor-sticky label {
  font-size: 0.7rem;
  margin-bottom: 0.1rem;
}
/* The same 36px minimum the buttons use (--button-height), instead of the 44px every other field has: this bar is part of a block that
   has to stay short. */
.filter-bar--sponsor-sticky input[type="search"],
.filter-bar--sponsor-sticky input[type="number"],
.filter-bar--sponsor-sticky select {
  min-height: 36px;
  padding: 0.25rem 0.55rem;
  font-size: 0.9rem;
}
.sponsor-assign-cell {
  flex: 0 0 var(--sponsor-assign-cell-width);
}
.sponsor-assign-cell .btn {
  width: 100%;
  justify-content: center;
  white-space: nowrap;
}
/* .btn sets display, which would beat the hidden attribute. */
.sponsor-assign-cell .btn[hidden] {
  display: none;
}
/* One row from 1200px up: the fields give up some width (they are only inputs), and the row does not wrap. */
@media (min-width: 75rem) {
  .filter-bar--sponsor-sticky {
    flex-wrap: nowrap;
  }
  .filter-bar--sponsor-sticky input.input-narrow,
  .filter-bar--sponsor-sticky select.input-narrow {
    width: 4rem;
  }
  .filter-bar--sponsor-sticky input.input-medium,
  .filter-bar--sponsor-sticky select.input-medium {
    width: 6.5rem;
  }
  .filter-bar--sponsor-sticky input.input-wide,
  .filter-bar--sponsor-sticky select.input-wide {
    width: 8.5rem;
  }
}
#sponsor-assign-tbl {
  table-layout: fixed;
  min-width: 64rem;
}
.donor-assign-select-cell {
  width: 2.75rem;
  text-align: center;
}
/* The wish lists the request's donor already holds (listed first on the per-donor page): the green background AND (v0.6.5, C-8) one
   black frame round the whole run -- an edge only on the outside, not a box per row. The server marks the first and last row of the run
   (CSS cannot select "the first row with this class"); the first and last cell of every row carry the sides. A run of one row gets all
   four edges, and a run cut by pagination simply closes at the page edge. */
.sponsor-assign-row--mine td {
  background: var(--color-success-bg);
}
.sponsor-assign-row--mine td:first-child {
  border-left: var(--sponsor-frame-width) solid var(--color-assigned-frame);
}
.sponsor-assign-row--mine td:last-child {
  border-right: var(--sponsor-frame-width) solid var(--color-assigned-frame);
}
.sponsor-assign-row--mine-first td {
  border-top: var(--sponsor-frame-width) solid var(--color-assigned-frame);
}
/* Beats the app-wide "no bottom border on a table's last row" (tbody tr:last-child td, above), so a run that reaches the end of the page
   still closes its frame. */
#sponsor-assign-tbl tbody tr.sponsor-assign-row--mine-last td {
  border-bottom: var(--sponsor-frame-width) solid var(--color-assigned-frame);
}
/* C-6: the donor's name with its Remove Assignment button to the right (it wraps beneath a long name). */
.donor-assign-donor-flex {
  display: flex;
  align-items: center;
  gap: var(--sponsor-donor-action-gap);
}
/* The name takes what room is left and wraps inside it, so the button stays to the RIGHT of the name however long the name is. */
.donor-assign-donor-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}
.sponsor-remove-assignment-form {
  display: inline-flex;
  flex: 0 0 auto;
  margin: 0;
}
.donor-assign-select-cell input {
  width: 1.15rem;
  height: 1.15rem;
  min-height: 0;
  accent-color: var(--color-accent);
}

@media (max-width: 720px) {
  .diff-row {
    grid-template-columns: 1fr;
  }
  .grid-2col {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 768px) {
  .nav-toggle-btn { display: flex; }
  .app-nav {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    flex-wrap: nowrap;
    gap: 2px;
    background: var(--brand-teal-color);
    padding: 0.5rem;
    box-shadow: var(--shadow-md);
    z-index: 890;
  }
  .app-nav.is-open { display: flex; }
  .app-nav a { text-align: center; }
  /* The Donors menu opens inline in the collapsed panel instead of floating over the page. */
  .app-nav__group { flex-direction: column; }
  .app-nav__group-btn { text-align: center; }
  .app-nav__submenu { position: static; min-width: 0; margin-top: 2px; box-shadow: none; }
  .app-nav .app-nav__submenu a { text-align: center; }
  main { padding: 1rem; }
}

@media print {
  .app-header, .app-nav, .app-footer, .no-print {
    display: none !important;
  }
}
