/* ==========================================================================
   Draco Staff Development — v8 Components
   Reusable UI kit. Depends on tokens.css + base.css.
   ========================================================================== */

/* ------------------------------- Wordmark ------------------------------- */
.brand { display: inline-flex; align-items: center; gap: 0.7rem; text-decoration: none; }
.brand__logo { width: 44px; height: 44px; flex: none; object-fit: contain; }
.brand__text { display: flex; flex-direction: column; line-height: 1; white-space: nowrap; }
.brand__name {
  font-family: var(--font-brand);
  font-weight: var(--fw-extra);
  font-size: 1.32rem;
  letter-spacing: var(--tracking-brand);
}
.brand__tag {
  font-size: 0.72rem;
  font-weight: var(--fw-semibold);
  letter-spacing: 0.03em;
  margin-top: 4px;
  text-transform: none;
  opacity: 0.92;
}
.brand--light .brand__name { color: #fff; }
.brand--light .brand__tag  { color: var(--brand-tag-user, var(--accent)); }
.brand--dark  .brand__name { color: var(--brand-navy); }
.brand--dark  .brand__tag  { color: var(--brand-tag-user, var(--link)); }  /* AA on light surfaces (7.2:1) */
/* Theme-aware "dark" variant: swap logo + text when the surface flips dark */
.brand--dark .brand__logo--ondark { display: none; }
:root[data-theme="dark"] .brand--dark .brand__logo--onlight { display: none; }
:root[data-theme="dark"] .brand--dark .brand__logo--ondark  { display: block; }
:root[data-theme="dark"] .brand--dark .brand__name { color: var(--text); }
:root[data-theme="dark"] .brand--dark .brand__tag  { color: var(--brand-tag-user, var(--accent)); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .brand--dark .brand__logo--onlight { display: none; }
  :root:not([data-theme="light"]) .brand--dark .brand__logo--ondark  { display: block; }
  :root:not([data-theme="light"]) .brand--dark .brand__name { color: var(--text); }
  :root:not([data-theme="light"]) .brand--dark .brand__tag  { color: var(--brand-tag-user, var(--accent)); }
}
.brand--sm .brand__logo { width: 36px; height: 36px; }
.brand--sm .brand__name { font-size: 1.1rem; }
.brand--sm .brand__tag  { font-size: 0.66rem; }
.brand--lg .brand__logo { width: 60px; height: 60px; }
.brand--lg .brand__name { font-size: 1.8rem; }
.brand--lg .brand__tag  { font-size: 0.9rem; }
a.brand:hover { opacity: 0.94; }

/* ------------------------------- Buttons -------------------------------- */
/* Every button: a two-color gradient background, a subtle border + drop
   shadow, a smooth hover color change (a second gradient fades in via ::before),
   a high-contrast label/icon, and NO position or size change on interaction.
   Anchors styled as buttons never underline. */
.btn {
  --btn-from:   #2a4c74;
  --btn-to:     #183253;
  --btn-from-h: #315986;
  --btn-to-h:   #1f3d63;
  --btn-fg:     #ffffff;
  --btn-bd:     rgba(8, 18, 33, 0.32);
  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: 0.7rem 1.25rem;
  min-height: 44px;                 /* touch target */
  font-size: var(--fs-base);
  font-weight: var(--fw-semibold);
  line-height: 1;
  letter-spacing: 0.005em;
  color: var(--btn-fg);
  background-image: linear-gradient(180deg, var(--btn-from), var(--btn-to));
  border: 1px solid var(--btn-bd);
  /* BUTTONS HAVE A RADIUS FLOOR of 6px, independent of the shape style's --radius-md.
     Gothic sets 2px and Corporate 4px, and at those values a button you are meant to press reads as
     a hard-cornered block (owner, 2026-07-21). The floor softens the BUTTON without softening the
     panels and tables around it — the sharp panel edge is the point of both styles, so it stays.
     max() rather than per-style overrides so any style added later inherits the floor instead of
     re-introducing the problem; --btn-radius remains as a deliberate escape hatch. Styles already
     above the floor (default 10px, soft 16px) are untouched. */
  border-radius: var(--btn-radius, max(var(--radius-md), 6px));
  box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.16);
  text-decoration: none;
  white-space: nowrap;
  user-select: none;
  transition: box-shadow var(--transition), border-color var(--transition), color var(--transition);
}
.btn::before {                       /* hover gradient — fades in for a smooth color change */
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image: linear-gradient(180deg, var(--btn-from-h), var(--btn-to-h));
  opacity: 0;
  transition: opacity var(--transition);
  z-index: -1;
}
.btn:hover::before { opacity: 1; }
.btn:hover  { color: var(--btn-fg); box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.18); }
.btn:active { box-shadow: var(--shadow-xs); }
.btn:focus-visible { outline: none; box-shadow: var(--shadow-focus), var(--shadow-sm); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; box-shadow: none; }
.btn:disabled::before, .btn[aria-disabled="true"]::before { display: none; }
.btn:hover { text-decoration: none; }

/* PRIMARY FOLLOWS THE PALETTE, and until 8.220.1 it did not.
   themes.css carried `.btn--primary { --btn-from: var(--brand-navy-700); … }` — the rule that is
   supposed to re-tint the primary button for each of the 23 palettes. It never applied: themes.css
   loads BEFORE components.css and the two selectors have identical specificity, so this rule won
   every time and every palette rendered the same hardcoded navy. Choosing Mono, Noir, Forest or
   Plum restyled the whole chrome and left the buttons blue — which is exactly why they read as
   not belonging to the page.
   The literals below were #2a4c74 / #183253, within a couple of points of the default palette's
   --brand-navy-700 / -800, so the DEFAULT look is unchanged; every other palette is fixed.
   tools/check-btn-adjacency.py already modelled primary as palette-driven, so its numbers were
   describing a page the browser never painted. That model is now true.
   Hover LIGHTENS via color-mix rather than naming a ramp step: --brand-navy is DARKER than -700 in
   every palette, so the obvious `--btn-from-h: var(--brand-navy)` would darken on hover. */
.btn--primary {
  --btn-from: var(--brand-navy-700);
  --btn-to:   var(--brand-navy-800);
  --btn-from-h: color-mix(in srgb, var(--brand-navy-700) 86%, #ffffff);
  --btn-to-h:   color-mix(in srgb, var(--brand-navy-800) 86%, #ffffff);
  --btn-fg: #fff;
  --btn-bd: rgba(8,18,33,.34);
}
/* DARK MODE: primary = the palette's RAW navy, NOT the old blanket white-lift (8.344.0).
   The lift existed for the near-black **noir** palette, where navy IS the page colour — but as a
   BLANKET dark rule it pushed primary UP into the lifted neutral ramp (--btn-ghost / --btn-quiet) on the
   bluer palettes (slate, midnight, graphite, mono, paper, corporate…), collapsing the app-wide
   "Cancel | Save" footer to ΔE00 ≈ 2 in dark. Raw navy stays dark and saturated, so it clears
   ΔE00 ≥ 10 against the neutrals on EVERY palette (worst 10.6 @ paper), verified by
   tools/check-btn-adjacency.py. A light hairline keeps it defined on dark surfaces; the hover lifts a
   little toward white for feedback. Noir re-lifts below — it is the ONLY palette that needs it, and the
   bluer palettes must NOT get it or they reconverge with the neutrals (that was the whole bug). */
:root[data-theme="dark"] .btn--primary {
  --btn-from:   var(--brand-navy-700);
  --btn-to:     var(--brand-navy-800);
  --btn-from-h: color-mix(in srgb, var(--brand-navy-700) 84%, #ffffff);
  --btn-to-h:   color-mix(in srgb, var(--brand-navy-800) 84%, #ffffff);
  --btn-bd: rgba(255,255,255,.18);
}
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .btn--primary {
  --btn-from:   var(--brand-navy-700);
  --btn-to:     var(--brand-navy-800);
  --btn-from-h: color-mix(in srgb, var(--brand-navy-700) 84%, #ffffff);
  --btn-to-h:   color-mix(in srgb, var(--brand-navy-800) 84%, #ffffff);
  --btn-bd: rgba(255,255,255,.18);
} }
/* NOIR ONLY: raw navy is the near-black page colour here, so lift primary to a visible dark GREY (white
   text stays well over AA). Grey sits far enough from the blue-grey neutrals to keep ΔE00 ≥ 10 (measured
   ghost 15.7, quiet 11.9). Higher specificity than the rule above, so it wins for noir; every other
   palette keeps raw navy. */
:root[data-palette="noir"][data-theme="dark"] .btn--primary {
  --btn-from:   color-mix(in srgb, var(--brand-navy-700) 78%, #ffffff);
  --btn-to:     color-mix(in srgb, var(--brand-navy-800) 78%, #ffffff);
  --btn-from-h: color-mix(in srgb, var(--brand-navy-700) 66%, #ffffff);
  --btn-to-h:   color-mix(in srgb, var(--brand-navy-800) 66%, #ffffff);
}
@media (prefers-color-scheme: dark) { :root[data-palette="noir"]:not([data-theme="light"]) .btn--primary {
  --btn-from:   color-mix(in srgb, var(--brand-navy-700) 78%, #ffffff);
  --btn-to:     color-mix(in srgb, var(--brand-navy-800) 78%, #ffffff);
  --btn-from-h: color-mix(in srgb, var(--brand-navy-700) 66%, #ffffff);
  --btn-to-h:   color-mix(in srgb, var(--brand-navy-800) 66%, #ffffff);
} }

/* Accent = a deeper blue so WHITE text stays high-contrast (all stops ≥4.5:1) */
/* PALETTE-AWARE, AND THE PERCENTAGE IS NOT A TASTE CHOICE (8.220.12). Accent keeps WHITE text, so
   the mix has to be dark enough for white to clear AA on the WORST palette, not the default one.
   Measured across all 23 palettes x both modes: 78% navy leaves `sky/light` at 4.36:1 (fails);
   **84% leaves the worst palette at 5.01:1**. Anything lighter is unreadable somewhere.
   tools/check-btn-adjacency.py now asserts this every run (ACCENT CONTRAST block) so a future
   "let's brighten the accent" cannot ship a button nobody can read. */
.btn--accent  {
  --btn-from: color-mix(in srgb, var(--brand-navy-700) 84%, #ffffff);
  --btn-to:   color-mix(in srgb, var(--brand-navy-800) 84%, #ffffff);
  --btn-from-h: color-mix(in srgb, var(--brand-navy-700) 76%, #ffffff);
  --btn-to-h:   color-mix(in srgb, var(--brand-navy-800) 76%, #ffffff);
  --btn-fg:#fff; --btn-bd: rgba(8,18,33,.34);
}

/* .btn--gold deleted 2026-07-19: §2 forbids gold button backgrounds outright, while §7 listed
   --gold as an available variant — the standard contradicted itself. Zero PHP and zero JS
   referenced it, so there was nothing to migrate. Reinstating it means changing §2 first. */

.btn--danger  { --btn-from:#cf342c; --btn-to:#a81a12; --btn-from-h:#da3f34; --btn-to-h:#b52019; --btn-fg:#fff; --btn-bd: rgba(90,15,10,.34); }

/* Neutral secondary — a light SLATE gradient (never white), dark text. */
/* PALETTE-AWARE (8.220.11). These were fixed blue-slate literals, so on a monochrome or noir
   palette a "neutral" button carried a visible blue cast in an otherwise grey UI — the same
   doesn't-belong defect 8.220.1 fixed for --primary, just quieter.
   The mix percentages are taken from the old literals: #e8edf4 is almost exactly 9% of the default
   navy over white, #d6def0 about 17%.
   THE DEFAULT PALETTE DOES SHIFT SLIGHTLY, about 5 points greyer (#e8edf4 -> #e3e7ea) — measured,
   not assumed. The mix base is `var(--surface)`, and the default palette's surface is #f6f8f8, not
   pure white. That is deliberate: a neutral button should sit on the surface it is actually drawn
   on, so it follows a palette that tints its surfaces. Mixing over a hardcoded #ffffff would
   reproduce the old bytes exactly and reintroduce the bug on every palette that is not white.
   Only LIGHT mode changes here — the dark overrides below are already neutral slates that work on
   any palette, and tools/check-btn-adjacency.py measures those against 23 palettes. */
.btn--ghost {
  --btn-from: color-mix(in srgb, var(--brand-navy) 9%, var(--surface));
  --btn-to:   color-mix(in srgb, var(--brand-navy) 17%, var(--surface));
  --btn-from-h: color-mix(in srgb, var(--brand-navy) 13%, var(--surface));
  --btn-to-h:   color-mix(in srgb, var(--brand-navy) 22%, var(--surface));
  --btn-fg: var(--text);
  --btn-bd: var(--border-strong);
}
.btn--ghost:hover { color: var(--text); }

/* Low-emphasis, but still a real gradient button (no transparent backgrounds). */
/* Same treatment as --ghost above; 6%/12% reproduces #eef1f7 / #e1e7f1 on the default palette. */
.btn--quiet {
  --btn-from: color-mix(in srgb, var(--brand-navy) 6%, var(--surface));
  --btn-to:   color-mix(in srgb, var(--brand-navy) 12%, var(--surface));
  --btn-from-h: color-mix(in srgb, var(--brand-navy) 9%, var(--surface));
  --btn-to-h:   color-mix(in srgb, var(--brand-navy) 16%, var(--surface));
  --btn-fg: var(--text-2); --btn-bd: var(--border);
}
.btn--quiet:hover { color: var(--text); }

/* Secondary-destructive — a muted brick-RED gradient, white text (NOT pink/rose,
   which is a forbidden button hue). Reads serious but less loud than --danger. */
.btn--danger-ghost {
  --btn-from: #b0352c; --btn-to: #8c1d16;
  --btn-from-h: #bd3e34; --btn-to-h: #9a241c;
  --btn-fg: #ffffff;
  --btn-bd: rgba(90, 15, 10, 0.36);
}

/* Dark mode: the neutral / soft variants get MID-slate / deep-rose gradients.
   ------------------------------------------------------------------------
   The neutrals used to be DARK slate here (#2a394f / #232f45). That put them
   within ΔE00 ≈ 5 of --primary on the bluer palettes (slate, midnight,
   graphite), because --primary is itself a dark palette navy in dark mode —
   so the app-wide `Cancel · Save` modal footer read as one undifferentiated
   block, exactly what §7 forbids. Fixed 2026-07-19 by lifting the ramp:

     ghost  L*≈45  ΔE00 ≥ 15.0 vs --primary (worst: sky/dark)
     quiet  L*≈41  ΔE00 ≥ 11.4 vs --primary (worst: slate/dark)

   The lift is deliberately the SMALLEST that clears the ≥10 bar with margin —
   these are still mid-slate chips, not near-white buttons, so --primary keeps
   the chromatic emphasis while the neutrals keep the desaturated one. Three
   things move together and must stay in lockstep:
     1. --btn-fg becomes #ffffff. It used to inherit var(--text)/var(--text-2);
        on a lifted fill --text-2 (#aebacb) would drop to ~2.7:1 and fail AA.
        White measures 5.33:1 (ghost) / 6.26:1 (quiet).
     2. --btn-bd becomes an explicit LIGHT slate. A lifted neutral sits only
        ~2.1–2.4:1 above the darkest surface it lands on (ocean/--surface-3),
        which alone is under the 3:1 UI bar; the border carries the edge at
        ≥4.7:1 against every dark surface, so the button never reads as a
        floating label. Do not drop it. (Before the lift the fill was 1.11:1
        against that same surface — i.e. the edge was already border-carried,
        just invisibly so.)
     3. :hover keeps white text — the base `.btn--ghost:hover{color:var(--text)}`
        rule would otherwise repaint it #e6edf6 on an even lighter hover fill.
   Re-verify with `python3 tools/check-btn-adjacency.py` after ANY change here. */
:root[data-theme="dark"] .btn--ghost:not(.btn--on-dark) { --btn-from:#6a7385; --btn-to:#5a6376; --btn-from-h:#757e91; --btn-to-h:#656e81; --btn-fg:#ffffff; --btn-bd:#939eb0; }
/* THE DARK `--quiet` FILL WAS LIFTED (8.220.1) from #5f6878/#505968 to #6b7486/#5c6577.
   tools/check-btn-adjacency.py measured quiet|primary at dE00 7.0 on paper/dark — below the 10.0
   "two different colours" bar — for the OASYS footer's `Create OASYS Account | Submit to CDPH`
   pair. The page's variant choice is deliberate and documented there (minting a real CDPH account
   takes the quiet rung), so the token moved rather than the page. Lifted only as far as the text
   allows: this lands dE00 11.0 with white at 5.22:1, where the next step up (#737c8e) buys 13.8 but
   drops the label to 4.65:1 and the one after that fails AA outright. */
/* ACCENT KEEPS A FIXED DARK-MODE FILL, and the checker is why. Making it palette-aware in dark too
   put it on top of the lifted dark neutrals: on slate/dark it measured dE00 4.9 against --ghost and
   5.4 against --quiet, i.e. one block of colour where two shipped footers need two distinct buttons.
   The dark neutrals above are themselves fixed slates rather than palette-aware, so a fixed accent
   here is consistent with that decision — not an exception to it. LIGHT mode still follows the
   palette, which is where the "blue button in a grey UI" complaint actually came from. */
:root[data-theme="dark"] .btn--accent { --btn-from:#3a70ac; --btn-to:#27568f; --btn-from-h:#4278b4; --btn-to-h:#2c5d97; }
:root[data-theme="dark"] .btn--quiet { --btn-from:#6b7486; --btn-to:#5c6577; --btn-from-h:#767f91; --btn-to-h:#67707f; --btn-fg:#ffffff; --btn-bd:#8b96a8; }
:root[data-theme="dark"] .btn--ghost:not(.btn--on-dark):hover,
:root[data-theme="dark"] .btn--quiet:hover { color: #ffffff; }
:root[data-theme="dark"] .btn--danger-ghost { --btn-from:#8f2019; --btn-to:#6d1610; --btn-from-h:#9e2820; --btn-to-h:#7a1a13; --btn-fg:#ffffff; --btn-bd:rgba(120,20,15,0.5); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn--ghost:not(.btn--on-dark) { --btn-from:#6a7385; --btn-to:#5a6376; --btn-from-h:#757e91; --btn-to-h:#656e81; --btn-fg:#ffffff; --btn-bd:#939eb0; }
    :root:not([data-theme="light"]) .btn--accent { --btn-from:#3a70ac; --btn-to:#27568f; --btn-from-h:#4278b4; --btn-to-h:#2c5d97; }
:root:not([data-theme="light"]) .btn--quiet { --btn-from:#6b7486; --btn-to:#5c6577; --btn-from-h:#767f91; --btn-to-h:#67707f; --btn-fg:#ffffff; --btn-bd:#8b96a8; }
  :root:not([data-theme="light"]) .btn--ghost:not(.btn--on-dark):hover,
  :root:not([data-theme="light"]) .btn--quiet:hover { color: #ffffff; }
  :root:not([data-theme="light"]) .btn--danger-ghost { --btn-from:#8f2019; --btn-to:#6d1610; --btn-from-h:#9e2820; --btn-to-h:#7a1a13; --btn-fg:#ffffff; --btn-bd:rgba(120,20,15,0.5); }
}

.btn--block { width: 100%; }
.btn--lg { padding: 0.9rem 1.6rem; min-height: 52px; font-size: var(--fs-lg); }
.btn--sm { padding: 0.5rem 0.85rem; min-height: 38px; font-size: var(--fs-sm); }

/* On dark backgrounds (e.g. hero CTA band) */
.btn--on-dark { --btn-bd: rgba(255,255,255,0.36); }
.btn--ghost.btn--on-dark {
  --btn-from: rgba(255,255,255,0.16); --btn-to: rgba(255,255,255,0.07);
  --btn-from-h: rgba(255,255,255,0.28); --btn-to-h: rgba(255,255,255,0.15);
  --btn-fg: #fff; --btn-bd: rgba(255,255,255,0.42);
}
.btn--ghost.btn--on-dark:hover { color: #fff; }

/* A button's icon ALWAYS matches its text colour (design standard) and never
   inherits a container's .icon colour/size rule — `inherit !important` wins over
   any `.some-container .icon { color: … }` that would otherwise leak in. The
   third-party sign-in buttons are `.social-btn`, not `.btn`, so their multi-colour
   logos are unaffected. */
.btn .icon { width: 1.15em !important; height: 1.15em !important; flex: none; color: inherit !important; }

/* Icon-only button (topbar/header actions) — same gradient/border/shadow
   language as .btn, in a compact round chip. Pair with data-tip for a tooltip. */
.icon-btn {
  --ib-from: var(--surface);
  --ib-to:   var(--surface-2);
  --ib-from-h: var(--surface-2);
  --ib-to-h:   var(--surface-3);
  --ib-bd: var(--border);
  --ib-fg: var(--text-2);
  --ib-fg-h: var(--text);
  position: relative;
  isolation: isolate;
  display: inline-flex; align-items: center; justify-content: center;
  width: 42px; height: 42px; border-radius: var(--radius-full);
  color: var(--ib-fg);
  background-image: linear-gradient(180deg, var(--ib-from), var(--ib-to));
  border: 1px solid var(--ib-bd);
  box-shadow: var(--shadow-xs);
  transition: box-shadow var(--transition), border-color var(--transition), color var(--transition);
  flex: none;
}
.icon-btn::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  background-image: linear-gradient(180deg, var(--ib-from-h), var(--ib-to-h));
  opacity: 0; transition: opacity var(--transition); z-index: -1;
}
.icon-btn:hover::before { opacity: 1; }
.icon-btn:hover { color: var(--ib-fg-h); box-shadow: var(--shadow-sm); }
.icon-btn:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.icon-btn .icon { width: 22px; height: 22px; }
.icon-btn--on-dark {
  --ib-from: rgba(255,255,255,0.15); --ib-to: rgba(255,255,255,0.06);
  --ib-from-h: rgba(255,255,255,0.26); --ib-to-h: rgba(255,255,255,0.13);
  --ib-bd: rgba(255,255,255,0.20); --ib-fg: rgba(255,255,255,0.92); --ib-fg-h: #fff;
}

/* Desktop / mouse users get more compact buttons. Touch devices (iPhone/iPad)
   keep the larger 44px+ tap targets from the base rules above — the distinction
   is a precise pointer, not screen width, so an iPad in a desktop-width layout
   still gets finger-friendly controls. */
@media (pointer: fine) {
  .btn      { min-height: 36px; padding: 0.44rem 0.95rem; font-size: var(--fs-sm); }
  .btn--lg  { min-height: 42px; padding: 0.6rem 1.25rem; font-size: var(--fs-base); }
  .btn--sm  { min-height: 30px; padding: 0.3rem 0.7rem;   font-size: var(--fs-sm); }
  .btn .icon { width: 1.05em; height: 1.05em; }
  .icon-btn { width: 36px; height: 36px; }
  .icon-btn .icon { width: 19px; height: 19px; }
}

/* Theme toggle: show ONE icon (sun in light, moon in dark). Lives here so it
   applies on every page, not just the landing/public pages. */
.theme-icon-dark  { display: none; }
:root[data-theme="dark"] .theme-icon-light { display: none; }
:root[data-theme="dark"] .theme-icon-dark  { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-icon-light { display: none; }
  :root:not([data-theme="light"]) .theme-icon-dark  { display: block; }
}

/* Notification dot */
.icon-btn .dot {
  position: absolute; top: 6px; right: 6px;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--brand-gold); border: 2px solid var(--brand-navy-900);
  z-index: 1;
}

/* Tooltip for icon-only controls (data-tip="Label"). Accessible name still
   comes from aria-label; this is the visual popup. */
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute; top: calc(100% + 8px); left: 50%;
  transform: translateX(-50%) translateY(-3px);
  padding: 5px 9px; border-radius: var(--radius-sm);
  background: var(--n-900); color: #fff;
  font-size: var(--fs-xs); font-weight: var(--fw-medium); letter-spacing: 0.01em;
  white-space: nowrap; box-shadow: var(--shadow-md);
  opacity: 0; visibility: hidden; pointer-events: none;
  /* display:none while hidden — `visibility:hidden`/`opacity:0` still occupy layout, so a nowrap
     tooltip on a control near the right edge (e.g. the stats "Customize" gear) pushed the page
     scrollWidth a few px past the viewport → a phantom horizontal scrollbar (which then forced a
     vertical one). display:none takes it out of the scroll region entirely. */
  display: none;
  transition: opacity var(--transition), transform var(--transition), visibility var(--transition);
  z-index: var(--z-dropdown);
}
[data-tip]:hover::after, [data-tip]:focus-visible::after {
  display: block; opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0);
}
[data-tip].tip-left::after  { left: auto; right: 0; transform: translateX(0) translateY(-3px); }
[data-tip].tip-left:hover::after, [data-tip].tip-left:focus-visible::after { transform: translateX(0) translateY(0); }
:root[data-theme="dark"] [data-tip]::after { background: #000; border: 1px solid var(--border); }
/* Don't show the tooltip while its menu is open (avoids overlapping the panel) */
.menu.open [data-tip]::after { display: none !important; opacity: 0 !important; visibility: hidden !important; }
/* Touch devices: suppress hover tooltips (they linger after tap) */
@media (hover: none) { [data-tip]::after { display: none; } }

/* -------------------------------- Cards --------------------------------- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.card--pad { padding: var(--space-6); }
.card--hover { transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition); }
.card--hover:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); border-color: var(--border-strong); }
.card__title { font-size: var(--fs-lg); font-weight: var(--fw-semibold); }

/* ------------------------------- Forms ---------------------------------- */
.field { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-5); }
.label { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-2); }
.label .req { color: var(--danger); }

.input, .select, .textarea {
  width: 100%;
  padding: 0.72rem 0.9rem;
  min-height: 46px;
  font-size: var(--fs-base);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  /* Subtle resting drop shadow so the white field well lifts off its (slightly
     darker) form container — the global form-field standard (DESIGN-STANDARDS §7). */
  box-shadow: var(--shadow-xs);
  transition: border-color var(--transition), box-shadow var(--transition), background var(--transition);
  appearance: none;
}
.textarea { min-height: 130px; resize: vertical; line-height: var(--lh-base); }
/* Single-line fields share ONE height with every toolbar control (--control-h,
   38px). The base rule above set min-height:46px, so a plain form field towered
   over the search/select in a report toolbar (both --control-h) sitting beside
   it. Vertical padding is zeroed here because the base 0.72rem padding clips the
   value at 38px; the min-height + line-height centres single-line text with no
   clipping.

   Deliberately min-height (NOT a fixed height): every intentionally-compact
   control shrinks itself by lowering THIS SAME property — .select--sm resets it
   to 0 (30px pager), .input--sm to 32px (inline table edit), .cnar-recpager to
   34px. A fixed `height` here would leak past all of those (they never reset
   height) and silently re-inflate them to 38px. Sharing min-height keeps the
   single-height standard for full-size fields while letting the compact
   variants win by cascade, exactly as before. .textarea keeps its own
   min-height:130px + base padding (multi-line, never pinned to a control). */
.input, .select { min-height: var(--control-h); padding-top: 0; padding-bottom: 0; }
/* Native date/time controls render ~2px taller than a plain text line at the SAME box model
   (their inner value box carries platform padding + a taller intrinsic line), so a date field
   towered over the text/select fields beside it in a form (measured: 44.8px date vs 42.8px text
   in the Add-Employee modal). Pin the value box to the shared line-height and strip the platform
   padding so every single-line field in a form is the same height. Applies in every context the
   base .input min-height does — toolbar (38px), modal (40px+pad) and plain form alike. */
.input[type="date"], .input[type="time"], .input[type="datetime-local"],
.input[type="month"], .input[type="week"] { line-height: var(--lh-base); }
.input::-webkit-datetime-edit { padding: 0; }
.input::-webkit-datetime-edit-fields-wrapper { padding: 0; }
.input::placeholder, .textarea::placeholder { color: var(--text-3); }
.input:focus, .select:focus, .textarea:focus {
  outline: none; border-color: var(--brand-sky); box-shadow: var(--shadow-focus); background: var(--surface);
}
.input:disabled, .select:disabled, .textarea:disabled { background: var(--surface-3); color: var(--text-3); cursor: not-allowed; box-shadow: none; }
.field--error .input, .field--error .select, .field--error .textarea { border-color: var(--danger); }
.field__hint { font-size: var(--fs-xs); color: var(--text-3); }
.field__error { font-size: var(--fs-xs); color: var(--danger); font-weight: var(--fw-medium); }

.select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%235b6b82' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.75rem center;
  padding-right: 2.5rem;
}
/* Lighter caret on dark surfaces (matches --text-3 dark) — with a no-JS mirror. */
:root[data-theme="dark"] .select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%238494ab' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%238494ab' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  }
}

/* Input with a leading icon */
.input-group { position: relative; }
.input-group .input-icon {
  position: absolute; left: 0.85rem; top: 50%; transform: translateY(-50%);
  width: 20px; height: 20px; color: var(--text-3); pointer-events: none;
}
.input-group .input { padding-left: 2.7rem; }
.input-group .input-affix {
  position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%);
  color: var(--text-3);
}

/* Checkbox / switch row */
.check { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; font-size: var(--fs-sm); color: var(--text-2); }
.check input { width: 18px; height: 18px; accent-color: var(--brand-navy); }

/* Toggle switch (settings / remember me) */
.switch { position: relative; display: inline-block; width: 46px; height: 26px; flex: none; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .track {
  position: absolute; inset: 0; background: var(--n-500); border-radius: var(--radius-full);
  transition: background var(--transition);
}
.switch .track::before {
  content: ""; position: absolute; left: 3px; top: 3px; width: 20px; height: 20px;
  background: #fff; border-radius: 50%; box-shadow: var(--shadow-sm);
  transition: transform var(--transition);
}
.switch input:checked + .track { background: var(--brand-navy); }
.switch input:checked + .track::before { transform: translateX(20px); }
.switch input:focus-visible + .track { box-shadow: var(--shadow-focus); }

/* Password reveal */
.pw-toggle { position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%);
  width: 38px; height: 38px; display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-3); border-radius: var(--radius-full); }
.pw-toggle:hover { color: var(--text); background: var(--surface-3); }

/* Stat card (KPI tile) — shared by the dashboard and the Staff Directory. */
.stat {
  background: var(--surface); border: 1px solid var(--border); border-top: 3px solid var(--accent);
  border-radius: var(--radius-lg);
  padding: var(--space-5); box-shadow: var(--shadow-sm); display: flex; gap: var(--space-4); align-items: center;
}
.stat__icon { width: 48px; height: 48px; border-radius: var(--radius-md); display: inline-flex; align-items: center; justify-content: center; flex: none; }
.stat__icon .icon { width: 24px; height: 24px; }
/* Brand-family chips (navy/sky/amber) all wear the controllable --icon-accent — which is BRIGHTER than
   the raw accent on dark, so a dark accent (e.g. a deep red) stays a legible glyph instead of vanishing
   into the chip. Green/red stay semantic. The theme studio sets --icon-accent independently of the band
   and link colours. (amber/yellow backgrounds are forbidden — DESIGN-STANDARDS §2, so it is NOT amber.) */
.stat__icon--navy,
.stat__icon--sky,
.stat__icon--amber { background: color-mix(in srgb, var(--icon-accent) 15%, transparent); color: var(--icon-accent); }
.stat__icon--green { background: var(--success-bg); color: var(--success); }
.stat__icon--red   { background: var(--danger-bg); color: var(--danger); }
.stat__n { font-size: var(--fs-3xl); font-weight: var(--fw-bold); line-height: 1; color: var(--text); font-variant-numeric: tabular-nums; }
.stat__l { font-size: var(--fs-sm); color: var(--text-3); margin-top: 4px; }
/* Clickable KPI card → drills into the matching detail view. */
a.stat { text-decoration: none; color: inherit; }
.stat--link { cursor: pointer; transition: box-shadow var(--transition), border-color var(--transition), transform var(--transition); }
.stat--link:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); transform: translateY(-1px); }
.stat--link:focus-visible { outline: none; box-shadow: var(--shadow-focus), var(--shadow-sm); }
.stat__go { margin-left: auto; flex: none; color: var(--text-3); opacity: 0; transform: translateX(-4px); transition: opacity var(--transition), transform var(--transition); }
.stat__go .icon { width: 18px; height: 18px; display: block; }
.stat--link:hover .stat__go, .stat--link:focus-visible .stat__go { opacity: 1; transform: none; }

/* Password strength meter (shared by the change-password page + Add-User modal). */
.pw-meter { display: flex; align-items: center; gap: var(--space-2); margin-top: 8px; }
.pw-meter__bar { flex: 1; height: 6px; border-radius: var(--radius-full); background: var(--surface-3); overflow: hidden; }
.pw-meter__bar span { display: block; height: 100%; width: 0; border-radius: inherit; background: #d61f16;
  transition: width var(--transition), background var(--transition); }
.pw-meter__label { font-size: var(--fs-xs); font-weight: var(--fw-semibold); min-width: 5.5em; text-align: right; }

/* ------------------------------- Badges --------------------------------- */
.badge {
  display: inline-flex; align-items: center; gap: 0.35em;
  padding: 0.2rem 0.6rem; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  border-radius: var(--radius-full); line-height: 1.4;
  background: var(--surface-3); color: var(--text-2);
}
/* Vivid, saturated status fills + dark bold text (light mode) — never a pale tint
   as the sole status carrier. "warning" is a neutral slate (amber is a forbidden fill). */
.badge--success { background: #a9edc0; color: #0d5231; font-weight: var(--fw-bold); }
.badge--warning { background: #dbe2ec; color: #33435c; font-weight: var(--fw-bold); }
.badge--danger  { background: #ffc9c7; color: #8a120c; font-weight: var(--fw-bold); }
.badge--info    { background: #cfe0fd; color: #16357a; font-weight: var(--fw-bold); }
:root[data-theme="dark"] .badge--success { background: #123a26; color: #86efac; }
:root[data-theme="dark"] .badge--warning { background: #263243; color: #b9c4d6; }
:root[data-theme="dark"] .badge--danger  { background: #4a1512; color: #ff5c5c; }
:root[data-theme="dark"] .badge--info     { background: #16305c; color: #b7cdfb; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .badge--success { background: #123a26; color: #86efac; }
  :root:not([data-theme="light"]) .badge--warning { background: #263243; color: #b9c4d6; }
  :root:not([data-theme="light"]) .badge--danger  { background: #4a1512; color: #ff5c5c; }
  :root:not([data-theme="light"]) .badge--info     { background: #16305c; color: #b7cdfb; }
}

/* ------------------------------- Alerts --------------------------------- */
.alert {
  display: flex; gap: var(--space-3); align-items: flex-start;
  padding: var(--space-4); border-radius: var(--radius-md);
  border: 1px solid var(--border); background: var(--surface-2);
  font-size: var(--fs-sm); color: var(--text);
}
.alert .icon { width: 20px; height: 20px; flex: none; margin-top: 1px; }
.alert + .alert { margin-top: var(--space-3); }
/* Status text via tokens — adapts to light/dark automatically (no hardcoded hex) */
.alert--success { background: var(--success-bg); border-color: var(--success-border); color: var(--success); }
.alert--warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning); }
.alert--danger  { background: var(--danger-bg);  border-color: var(--danger-border);  color: var(--danger); }
.alert--info    { background: var(--info-bg);    border-color: var(--info-border);    color: var(--info); }
.alert strong { color: inherit; }

/* ------------------------------- Avatar --------------------------------- */
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 50%;
  background: linear-gradient(135deg, var(--brand-blue), var(--brand-navy));
  color: #fff; font-weight: var(--fw-semibold); font-size: var(--fs-sm);
  overflow: hidden; flex: none; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12);
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar--lg { width: 56px; height: 56px; font-size: var(--fs-lg); }

/* ------------------------- Dropdown menu (popover) ---------------------- */
.menu { position: relative; }
.menu__panel {
  position: absolute; right: 0; top: calc(100% + 10px);
  min-width: 244px;
  max-width: calc(100vw - var(--space-4) * 2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-2);
  opacity: 0; visibility: hidden; transform: translateY(-6px);
  transition: opacity var(--transition-fast), transform var(--transition-fast), visibility var(--transition-fast);
  z-index: var(--z-dropdown);
}
.menu.open .menu__panel { opacity: 1; visibility: visible; transform: translateY(0); }
/* A CLOSED panel is still position:absolute and still has height, so inside a .dsd-tablewrap it
   inflated the scrollable area — the Check-Offs panel carried ~190px of empty vertical scroll that
   pushed real rows out of view. visibility:hidden hides it but does not remove its box. Zero height
   does, and because opacity still transitions the fade-in survives. */
.menu:not(.open) .menu__panel { height: 0; overflow: hidden; padding-top: 0; padding-bottom: 0; }
.menu__header { padding: var(--space-3) var(--space-3) var(--space-3); border-bottom: 1px solid var(--border); margin-bottom: var(--space-2); }
.menu__item {
  display: flex; align-items: center; gap: var(--space-3);
  padding: 0.6rem 0.7rem; border-radius: var(--radius-md);
  color: var(--text); font-size: var(--fs-sm); font-weight: var(--fw-medium);
  width: 100%; text-align: left; transition: background var(--transition-fast), color var(--transition-fast);
}
.menu__item:hover { background: var(--surface-3); color: var(--text); }
.menu__item .icon { width: 18px; height: 18px; color: var(--text-3); flex: none; }
.menu__item--danger { color: var(--danger); }
.menu__item--danger:hover { background: var(--danger-bg); }
.menu__item--danger .icon { color: var(--danger); }
.menu__sep { height: 1px; background: var(--border); margin: var(--space-2) 0; }

/* -------------------------------- Tables -------------------------------- */
/* Standard data table: zebra striping + a consistent pagination footer.
   Structure:
     <div class="table-card">
       <div class="table-wrap"><table class="dsd-table">…</table></div>
       <?= dsd_pagination($page, $per, $total, $baseUrl) ?>
     </div>  */
.table-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.table-wrap { overflow-x: auto; }
.dsd-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
/* THEMED header row — a BOLD accent gradient across the whole row (on thead, so it is ONE gradient,
   not one per cell) with white labels, so every table on the platform is unmistakably themed in any
   palette and in dark mode (owner: the colour change must reach "everything", not as a faint tint). */
.dsd-table thead { background: var(--sect-band); }
.dsd-table thead th {
  text-align: left; white-space: nowrap;
  font-size: var(--fs-xs); font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: #fff; text-shadow: 0 1px 1px rgba(0,0,0,.25);
  padding: var(--space-3) var(--space-4);
  background: transparent;
  border-bottom: none;
}
.dsd-table tbody td {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  color: var(--text); vertical-align: middle;
}
.dsd-table tbody tr:nth-child(even) { background: var(--surface-2); }   /* zebra */
.dsd-table tbody tr:hover { background: var(--surface-3); }
.dsd-table tbody tr:last-child td { border-bottom: 0; }
.dsd-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.dsd-table .muted { color: var(--text-3); }
.dsd-tablewrap { overflow-x: auto; }
/* A kebab inside a table is clipped by TWO ancestors: this wrapper (overflow-x:auto computes overflow-y
   to auto as well) and .panel, which uses overflow:hidden for its rounded corners. Rather than switch
   either to visible while a menu is open — which discarded horizontal scroll position and burst wide
   tables out of the page on a phone — app.js lifts the open panel to position:fixed and pins it to the
   button. Nothing about the table's own overflow changes. See dsdMenuEscape in assets/js/app.js. */
.menu__panel.is-escaped { position: fixed; z-index: 1200; }

/* Clickable rows (open a details modal) */
.dsd-table .row-click { cursor: pointer; }
.dsd-table .row-click:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--brand-blue); }
.u-cell { display: inline-flex; align-items: center; gap: var(--space-3); min-width: 0; }
.u-cell .avatar { width: 34px; height: 34px; font-size: var(--fs-xs); flex: none; }
.u-cell__nm { font-weight: var(--fw-semibold); display: inline-flex; align-items: center; gap: var(--space-2); }
.dsd-table__chev { text-align: right; color: var(--text-3); width: 1%; white-space: nowrap; }
.dsd-table__chev .icon { width: 18px; height: 18px; }

/* -------------------------------- Modal --------------------------------- */
.modal { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: var(--space-4); }
.modal[hidden] { display: none; }
.modal__backdrop { position: absolute; inset: 0; background: var(--overlay); backdrop-filter: blur(2px); }
.modal__dialog {
  position: relative; z-index: 1; width: 100%; max-width: 560px;
  max-height: calc(100dvh - 2 * var(--space-4)); overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-xl); box-shadow: var(--shadow-lg);
  animation: modal-in 160ms ease;
}
@keyframes modal-in { from { opacity: 0; transform: translateY(8px) scale(0.99); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .modal__dialog { animation: none; } }
/* Form modals sit on the tinted form surface so their white input wells pop (global
   form standard). The `form` vs `div` distinction on .modal__body separates data-entry
   dialogs from read-only detail dialogs automatically: read-only modals (notification,
   event/renewal/AFL/stat views) use a <div class="modal__body"> and stay on --surface.
   The second selector catches wizards whose <div> body wraps a nested <form> (Import). */
.modal__dialog:has(> form.modal__body:not([hidden])),
.modal__dialog:has(> .modal__body form:not([hidden])) { background: var(--form-surface); }
.modal__close { position: absolute; top: var(--space-3); right: var(--space-3); }
/* Right padding reserves room for the absolutely-positioned .modal__close (top-right)
   so a long title wraps before the X instead of running underneath it. */
.modal__head { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-4) calc(var(--space-3) + 44px) var(--space-3) var(--space-5); border-bottom: 1px solid var(--border); }
/* A "title + subtitle" head (a .modal__sub DIRECTLY under the head) lays out block, so the never-wrapping
   title keeps its own full-width line and is never ellipsed by a long subtitle beside it. Avatar-layout
   heads nest their sub inside .modal__head-meta, so they stay flex (unaffected). */
.modal__head:has(> .modal__sub) { display: block; }
.modal__head:has(> .modal__sub) > .modal__sub { max-width: 66ch; margin-top: var(--space-1); }
/* Modal titles never wrap (design standard §modals) — one line, ellipsis if somehow too long. */
.modal__head h2 { font-size: var(--fs-lg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.modal__head .avatar--lg { width: 42px; height: 42px; font-size: var(--fs-lg); }
.modal__head-meta { min-width: 0; }
.modal__sub { color: var(--text-2); font-size: var(--fs-sm); margin-top: 1px; display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.modal__body { padding: var(--space-4) var(--space-5); display: flex; flex-direction: column; gap: var(--space-3); }
.modal__foot { display: flex; justify-content: flex-end; gap: var(--space-2); padding: var(--space-3) var(--space-5); border-top: 1px solid var(--border); flex-wrap: wrap; }

/* Compact form density inside modals — corporate, tighter vertical rhythm. */
.modal .field { gap: 3px; margin-bottom: var(--space-3); }
/* ONE RHYTHM, NOT TWO (8.220.10). `.modal__body` is a flex column WITH a row-gap, and `.field` also
   carries a bottom margin, so the space between two fields was gap + margin — measured 12 + 16 =
   **28px** on the Training Calendar event form, which is why these modals read as mostly air. The
   flex gap alone is the rhythm; the margin is a leftover from page forms, where .field is not in a
   flex parent and the margin IS the rhythm.
   Scoped to DIRECT children of the body so a .field nested inside a non-flex wrapper keeps its
   margin and does not collapse onto its neighbour. Specificity (0,3,0) deliberately beats the
   per-module `.cnar-form .field` / `.tr-form .field` overrides at (0,2,0), which is where the 16px
   was actually coming from. */
.modal .modal__body > .field { margin-bottom: 0; }
/* The head and foot were paying page-level padding inside a dialog that is already a small surface.
   Trimmed a little; the body keeps a comfortable side gutter so nothing touches the edge. */
.modal .modal__body { padding: var(--space-3) var(--space-4) var(--space-3); gap: var(--space-2); }
.modal .modal__head { padding-top: var(--space-3); padding-bottom: var(--space-2); }
.modal .modal__foot { padding: var(--space-3) var(--space-4); }
.modal .form-grid { gap: var(--space-3) var(--space-4); }
/* 36px, not the page's 38px --control-h and not the old 40px: a modal is a dense form on a small
   surface. Every single-line control in a modal shares this number, which is what the
   consistent-field-height rule actually asks for — uniformity WITHIN a form, not one global
   height. Edited here at source rather than layered as an override: a previous attempt added a
   later, more-scoped rule and it never took effect. */
.modal .input, .modal .textarea { min-height: 36px; padding: 0.4rem 0.7rem; }
/* Selects need right-side room for the native dropdown arrow — sharing the input padding
   overlapped the arrow with the value (e.g. the "Per page" control). */
.modal .select { min-height: 36px; padding: 0.4rem 2rem 0.4rem 0.7rem; }
.modal .input-group .input-icon { width: 2.4rem; }
.modal .input-group .input { padding-left: 2.4rem; }
.modal .field__hint { line-height: 1.35; }

.um-section h3 { display: flex; align-items: center; gap: var(--space-2); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--tracking-wider); color: var(--text-3); margin-bottom: var(--space-3); font-weight: var(--fw-semibold); }
.um-section h3 .icon { width: 15px; height: 15px; }
.um-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3) var(--space-4); }
.um-grid > div { min-width: 0; }
.um-grid dt { font-size: var(--fs-xs); color: var(--text-3); margin-bottom: 2px; }
.um-grid dd { color: var(--text); font-weight: var(--fw-medium); word-break: break-word; }
@media (max-width: 480px) { .um-grid { grid-template-columns: 1fr; } }
.modal__dialog--lg { max-width: 760px; }
.modal__dialog--xl { max-width: min(1040px, 94vw); }

/* ------------------------- Row actions dropdown ------------------------- */
/* Fixed-positioned so it escapes table overflow clipping. Shared by the admin
   Facilities and tenant Staff Directory tables. */
.row-menu { position: fixed; z-index: 1100; min-width: 210px; max-height: min(80vh, 460px); overflow-y: auto; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); padding: 6px; }
.row-menu[hidden] { display: none; }
.row-menu__item { display: flex; align-items: center; gap: var(--space-2); width: 100%; padding: 9px 10px; border-radius: var(--radius-sm); font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--text); text-align: left; transition: background var(--transition-fast); }
.row-menu__item:hover, .row-menu__item:focus-visible { background: var(--surface-2); outline: none; }
.row-menu__item .icon { width: 16px; height: 16px; flex: none; color: var(--text-3); }
.row-menu__item[aria-checked="true"] { color: var(--link); font-weight: var(--fw-semibold); }
.row-menu__item[aria-checked="true"] .icon { color: var(--link); }
/* The tick marks the CURRENT choice and nothing else. Every option used to carry one, so all six
   statuses read as selected at once. Hidden (not removed) on the unselected options so their labels
   stay on the same left edge as the ticked one. */
.row-menu__tick { display: inline-flex; flex: none; }
.row-menu__item[aria-checked="false"] .row-menu__tick { visibility: hidden; }
.row-menu__item--danger { color: var(--danger); }
.row-menu__item--danger .icon { color: var(--danger); }
.row-menu__item--danger:hover { background: var(--danger-bg); }
.row-menu__sep { height: 1px; background: var(--border); margin: 4px 2px; }
.row-menu__label { padding: 8px 10px 4px; font-size: var(--fs-xs); font-weight: var(--fw-semibold); letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-3); }

/* ---------------- Table footer: count + view controls + pager -------------
   The shared standard for EVERY paginated table (Staff, User Accounts, dashboard
   Upcoming Events, …). Lives here (global) so it styles identically on every page,
   not only where staff.css happens to load. */
.staff-foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-4); margin-top: var(--space-4); }
.staff-foot__count { color: var(--text-2); font-size: var(--fs-sm); }
.staff-foot__view { display: flex; align-items: center; gap: var(--space-2); margin-left: auto; font-size: var(--fs-sm); color: var(--text-2); }
/* Design standard: the pager always sits at the far RIGHT of a table footer, count on the left.
   (A pager wrapped in .staff-foot__view is already right-aligned; this covers a bare pager.) */
.staff-foot > .pager { margin-left: auto; }
.select--sm { width: auto; padding: 6px 30px 6px 10px; min-height: 0; font-size: var(--fs-sm); }
.pager { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; }
.pager__btn { display: inline-flex; align-items: center; justify-content: center; min-width: 36px; height: 36px; padding: 0 8px; border: 1px solid var(--border-strong); border-radius: var(--radius-md); background-image: linear-gradient(180deg, #e8edf4, #d6def0); color: var(--text-2); font-size: var(--fs-sm); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; box-shadow: var(--shadow-xs); transition: background var(--transition-fast), box-shadow var(--transition-fast); }
.pager__btn:hover { background-image: linear-gradient(180deg, #dde4ef, #c7d2e6); color: var(--text); box-shadow: var(--shadow-sm); }
.pager__btn .icon { width: 16px; height: 16px; }
.pager__btn.is-current { background-image: linear-gradient(180deg, var(--brand-navy-700), var(--brand-navy-800)); border-color: rgba(8,18,33,.34); color: #fff; }
:root[data-theme="dark"] .pager__btn { background-image: linear-gradient(180deg, #2a394f, #1e2c46); }
:root[data-theme="dark"] .pager__btn:hover { background-image: linear-gradient(180deg, #33455f, #263857); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .pager__btn { background-image: linear-gradient(180deg, #2a394f, #1e2c46); }
  :root:not([data-theme="light"]) .pager__btn:hover { background-image: linear-gradient(180deg, #33455f, #263857); }
}
.pager__btn.is-disabled { opacity: 0.4; pointer-events: none; }
.pager__gap { padding: 0 4px; color: var(--text-3); }

/* Responsive table-column utilities (used by every data table) */
@media (max-width: 820px) { .col-hide-md { display: none !important; } }
@media (max-width: 640px) { .col-hide-sm { display: none !important; } }
@media (max-width: 560px) { .col-hide-xs { display: none !important; } }

/* --------------------------------- Tabs --------------------------------- */
/* File-folder tabs: each tab is a bordered, top-rounded card sitting on the
   divider line; the active tab shares the panel's surface so it reads as the
   front folder connected to the content below. */
.tabs { display: flex; gap: var(--space-1); padding: var(--space-3) var(--space-5) 0; border-bottom: 1px solid var(--border); overflow-x: auto; scrollbar-width: none; }
.tabs::-webkit-scrollbar { display: none; }
.tab { display: inline-flex; align-items: center; gap: 6px; padding: var(--space-3) var(--space-4); font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-3); background: var(--surface-2); border: 1px solid var(--border); border-bottom-color: transparent; border-radius: var(--radius-md) var(--radius-md) 0 0; margin-bottom: -1px; white-space: nowrap; transition: color var(--transition), background var(--transition), border-color var(--transition); }
.tab .icon { width: 15px; height: 15px; }
.tab:hover { color: var(--text); background: var(--surface-3); }
.tab.is-active { color: var(--link); background: var(--surface); border-color: var(--border); border-bottom-color: var(--surface); box-shadow: inset 0 2px 0 var(--brand-blue); }
.tab:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
/* A scrolled tab strip must never sit flush against the clipped edge — reserve the strip's own padding
   as scroll padding so the active tab stays fully visible after auto-scroll-into-view. */
.tabs { scroll-padding-inline: var(--space-5); }
/* Logs channel tabs sit flush with the left edge of the content below them (owner) — the shared .tabs
   left padding indented them past the search box and table. */
.logv-tabs { padding-left: 0; scroll-padding-inline: 0; }
.tabpanel { display: none; }
.tabpanel.is-active { display: block; animation: tabfade 140ms ease; }
@keyframes tabfade { from { opacity: 0; } to { opacity: 1; } }

/* Employee details modal carries 8 tabs (Personal … Notes). Give it more room than the stock --xl
   dialog and compact the tab strip a touch so the whole set fits on one line at desktop widths and
   scrolls cleanly (never clipped) on narrow ones. */
#employeeModal .modal__dialog { max-width: min(1200px, 96vw); }
#employeeModal .tabs { padding-inline: var(--space-4); flex-wrap: wrap; row-gap: var(--space-1); }
#employeeModal .tab { padding: var(--space-2) var(--space-3); }
@media (max-width: 720px) { #employeeModal .tabs { flex-wrap: nowrap; } }
@media (prefers-reduced-motion: reduce) { .tabpanel.is-active { animation: none; } }

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
.form-grid .field { margin-bottom: 0; }
@media (max-width: 520px) { .form-grid { grid-template-columns: 1fr; } }

/* ----------------------------- Form section (card group) -----------------------------
   A titled group of fields inside a form. A shared form primitive, so it lives here next
   to .form-grid rather than in a page stylesheet.

   THE DEFECT IT FIXES (owner: "they all blend together like one big section"). The old
   `.form-section` (was staff.css:103) carried the BYTE-IDENTICAL type spec to `.label`
   (:277 above — --fs-sm / --fw-semibold / --text-2). A section title printed in exactly
   the same ink as every field label beneath it cannot start a section; in a form where
   every other line IS a .label, 13 of them read as one continuous form. It was also a
   sibling of its own fields, with a full-width rule UNDER the title — a rule DIVIDES the
   heading from the content it names, and never shows where a section ends.

   A left rail alone (8.120.4) was not enough — owner, 8.120.9: "the sections still look
   like they all run together ... put each section in its own separate border, a subtle
   border with a drop shadow for a depth effect, so it's obvious where each section starts,
   ends and what is within each section." A rail draws a section's extent but only on ONE
   edge; it never closes the box, so two stacked sections still read as one column of
   fields. A full border closes it and the shadow lifts it off the page.

   WHY NO FILL — this is load-bearing, do not "improve" it by adding a background.
   The dialog ITSELF is already --form-surface (.modal__dialog:has(> form.modal__body)
   above), so a card fill here would either equal its own parent (invisible — measured:
   --surface-3 on --form-surface = 1.045:1) or, if set to --surface, put the section and
   its input wells at the SAME value and collapse §7's three-tier form standard: the white
   wells only pop BECAUSE the dialog is tinted. So the card is drawn with border + shadow
   and inherits the dialog's tint — the section reads as a distinct panel and the fields
   inside it keep their well. (v8's documented section card .ffill-sec (forms.css:75) does
   fill, but reads only because dashboard/form-entry.php sits on --bg; it is not portable
   into a form-surface dialog.)

   Two independent signals per §8 (colour is never the sole carrier): the border+shadow
   geometry, plus --fw-bold/--text heading type at ~15:1. --border on --form-surface is a
   quiet 1.18:1, which is correct for a container edge sitting under a shadow — the shadow,
   not the hairline, is what separates. */
.form-sec { display: block; margin-top: var(--space-4); padding: var(--space-4);
            border: 1px solid var(--border); border-radius: var(--radius-lg);
            box-shadow: var(--shadow-sm); }
/* NOT redundant with margin collapsing: .modal__body is a flex column, so each .tabpanel
   is a flex item and a BFC — a first child's margin-top cannot collapse out and would
   render as leading space. Left unscoped (not `.tabpanel > .form-sec`) so it also fires
   inside [data-docs-body], whose parent is not .tabpanel — the case the old
   `.tabpanel > .form-section:first-child` silently never matched. */
.form-sec:first-child { margin-top: 0; }
/* The card's padding IS the section's bottom rhythm. Under the old rail there was no bottom edge, so
   a trailing child's margin-bottom collapsed straight out of the box and cost nothing; the 8.120.9
   padding + border BLOCK that collapse and trap it as dead interior space — measured on prod before
   this rule: Licenses & Certifications 33px to the border vs 17px on its siblings (16px padding +
   16px stranded .lic-editor margin), CDPH OASYS 25px. Exactly the "same rhythm" legibility the cards
   were added to deliver.
   `:last-child` ALONE DOES NOT FIX IT — do not simplify this to one selector. The Licenses card's
   real last children are a <datalist> and three <input type="hidden"> (all display:none), so the CSS
   last child is not the last RENDERED box; .lic-editor needs naming outright. CSS has no
   ":last-visible-child". Children of .form-grid are already margin-bottom:0 (:634), which is why the
   grid-terminated sections were never affected. Verified on prod after this rule: all 13 sections
   across all six tabs measure 17px from last visible child to bottom border. */
.form-sec > :last-child,
.form-sec > .lic-editor { margin-bottom: 0; }
/* Section title — a BOLD THEMED GRADIENT BAND (owner: "the theme should change the section title
   background colours … colour gradients, not flat static colours" — and tints are too faint on the
   dark/noir palette). A saturated accent gradient (dark accent → bright accent) fills the whole band
   with white text, so the theme is unmistakable in every palette AND dark mode. --link and --accent
   both re-tint with the palette AND the custom accent (dsd_accent_css drives them). */
.form-sec__h { margin: 0 0 var(--space-4); font-size: var(--fs-sm); font-weight: var(--fw-bold);
               line-height: var(--lh-snug); letter-spacing: var(--tracking-wider);
               text-transform: uppercase; color: #fff; text-shadow: 0 1px 1px rgba(0,0,0,.28);
               display: flex; align-items: center; gap: .55rem;
               padding: .6rem .95rem; border-radius: var(--radius-md);
               background: var(--sect-band);
               box-shadow: var(--shadow-xs); }
.form-sec__h .icon { opacity: .92; }

/* ── UNIFORM SECTION HEADERS ─────────────────────────────────────────────────────────────────────
   One themed look for EVERY section/card header across the platform (owner: "the design needs to be
   uniform across all pages"). .panel__head and .settings-card__head now wear the SAME bold accent
   gradient band + white text as .form-sec__h and the table headers, so a section title looks
   identical whether it is on Settings, the Dashboard, Backups, or a report. They keep their own
   flex/padding, so titles, subtitles and right-aligned controls stay put. */
.panel__head,
.settings-card__head {
  background: var(--sect-band) !important;
  border-bottom: 0 !important;
}
.panel__head > *, .panel__head h2, .panel__head p,
.settings-card__head > *, .settings-card__head h2, .settings-card__head p {
  color: #fff !important;
}
.panel__head h2, .settings-card__head h2 { text-shadow: 0 1px 1px rgba(0,0,0,.28); }
/* #347 — the subtitle that sits under a band title must read on the dark gradient too. The white
   override above only reaches direct children and <p>/.panel__sub, so a subtitle nested in a wrapper
   with its own muted colour (e.g. .nh-result__sub on NHPPD's "Staffing Trend" / "Result" bands, the
   daybar date) stayed near-black on navy. Catch EVERY subtitle/muted text descendant of a header band
   — any class containing "__sub", plus the common muted helpers — and lift it to legible light. */
.panel__head p, .panel__head .panel__sub, .panel__head [class*="__sub"],
.panel__head .text-muted, .panel__head .text-3, .panel__head small, .panel__head time,
.settings-card__head p, .settings-card__head [class*="__sub"] { color: rgba(255,255,255,.88) !important; }
.panel__head .icon, .settings-card__head .icon { color: #fff !important; opacity: .95; }
/* #347 — the NHPPD daybar date + "Back to today" link also sit inside the header band; the date is the
   prominent label so it goes full white, the link a touch dimmer + underlined. */
.panel__head .nh-date__d { color: #fff !important; }
.panel__head .nh-date__today { color: rgba(255,255,255,.9) !important; text-decoration: underline; }
/* Controls that sit inside a header band read on the dark gradient: translucent-white chrome. */
.panel__head .btn--ghost, .panel__head .btn--quiet, .panel__head .icon-btn,
.settings-card__head .btn--ghost, .settings-card__head .btn--quiet, .settings-card__head .icon-btn {
  color: #fff !important; border-color: rgba(255,255,255,.4) !important;
  background: rgba(255,255,255,.12) !important;
}
/* Sentence-case aside inside an eyebrow heading, e.g. "Mailing Address (if different)". */
.form-sec__hint { text-transform: none; letter-spacing: var(--tracking-normal);
                  font-weight: var(--fw-regular); color: var(--text-3); }
/* On a dark section BAND (.form-sec__h) the hint sat at near-black --text-3 — "barely visible" (owner).
   Lift it to legible translucent white, matching the band subtitles above. Scoped to the band so the same
   class used inside a white field label (e.g. "Employment Type (as recorded)") keeps its muted dark. */
.form-sec__h .form-sec__hint { color: rgba(255,255,255,.82); }

/* Setting row — a labelled value with an optional trailing control (settings + modal). */
.setting-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: var(--space-4) 0; border-bottom: 1px solid var(--border); }
.setting-row:first-child { padding-top: 0; }
.setting-row:last-child { border-bottom: 0; padding-bottom: 0; }
.setting-row__label { font-weight: var(--fw-semibold); font-size: var(--fs-sm); color: var(--text); }
.setting-row__desc { font-size: var(--fs-xs); color: var(--text-3); margin-top: 2px; max-width: 44ch; }

/* Pagination / page-size / nav footer — identical on every table */
.dsd-tablebar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border);
  background: var(--surface);
  font-size: var(--fs-sm); color: var(--text-2);
}
.dsd-tablebar__count { white-space: nowrap; }
.dsd-tablebar__count strong { color: var(--text); font-weight: var(--fw-semibold); }
.dsd-tablebar__size { display: inline-flex; align-items: center; gap: var(--space-2); }
.dsd-tablebar__size .select { min-height: 36px; height: 36px; width: auto; padding: 0.25rem 2rem 0.25rem 0.6rem; font-size: max(16px, var(--fs-sm)); }
.dsd-tablebar__nav { display: inline-flex; align-items: center; gap: 4px; }
.dsd-pagebtn {
  min-width: 36px; height: 36px; padding: 0 0.5rem;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border-strong); border-radius: var(--radius-md);
  background-color: var(--surface);
  background-image: linear-gradient(180deg, var(--surface), var(--surface-2));
  color: var(--text-2); font-weight: var(--fw-semibold); font-size: var(--fs-sm);
  box-shadow: var(--shadow-xs);
  /* Rendered as <a> by dsd_pagination() and as <button> by the AFL client pager —
     normalize the native button look so both are pixel-identical. */
  appearance: none; -webkit-appearance: none; font-family: inherit; cursor: pointer;
  transition: color var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.dsd-pagebtn:hover { color: var(--text); border-color: var(--n-400); box-shadow: var(--shadow-sm); }
.dsd-pagebtn.is-active {
  background-image: linear-gradient(180deg, var(--brand-navy-700), var(--brand-navy-800));
  color: #fff; border-color: rgba(8,18,33,.34);
}
.dsd-pagebtn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; box-shadow: none; }
.dsd-pagebtn .icon { width: 16px; height: 16px; }
.dsd-pagebtn--ellipsis { border: 0; background: none; box-shadow: none; color: var(--text-3); pointer-events: none; min-width: 22px; }
@media (max-width: 600px) {
  .dsd-tablebar { justify-content: center; }
  .dsd-tablebar__count { order: 3; width: 100%; text-align: center; }
}

/* -------------------------------- Divider ------------------------------- */
.or-divider { display: flex; align-items: center; gap: var(--space-3); color: var(--text-3); font-size: var(--fs-sm); margin: var(--space-5) 0; }
.or-divider::before, .or-divider::after { content: ""; height: 1px; background: var(--border); flex: 1; }

/* ------------------------------ Section head ---------------------------- */
.section-head { text-align: center; max-width: 44rem; margin: 0 auto var(--space-10); }
.section-head h2 { margin-top: var(--space-3); }
.section-head p { color: var(--text-2); font-size: var(--fs-lg); margin-top: var(--space-3); }

/* ------------------------------- Spinner -------------------------------- */
.spinner {
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent; opacity: 0.9;
  animation: spin 0.7s linear infinite; display: inline-block; vertical-align: -2px;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 1.6s; } }

/* Busy submit button (set by app.js on submit; blocks duplicate POSTs). */
.btn.is-busy, .btn[aria-busy="true"] { pointer-events: none; opacity: 0.85; cursor: progress; }

/* ------------------------------- Toasts --------------------------------- */
/* Standard user-feedback toasts: top-right, self-closing after 3s. Use
   window.dsdToast(message, type) — type: success|error|warning|info. */
.toast-region {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 16px);
  right: calc(env(safe-area-inset-right, 0px) + 16px);
  z-index: var(--z-toast);
  display: flex; flex-direction: column; gap: var(--space-3);
  width: min(360px, calc(100vw - 32px));
  pointer-events: none;
}
.toast {
  display: flex; align-items: flex-start; gap: var(--space-3);
  background: var(--surface);                                   /* fallback */
  background: color-mix(in srgb, var(--surface) 80%, transparent);
  backdrop-filter: blur(12px) saturate(150%);
  -webkit-backdrop-filter: blur(12px) saturate(150%);
  border: 1px solid var(--border);
  border-left: 4px solid var(--brand-sky);
  box-shadow: var(--shadow-lg);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  font-size: var(--fs-sm); color: var(--text);
  pointer-events: auto;
  opacity: 0; transform: translateX(24px) scale(0.98);
  transition: opacity var(--transition), transform var(--transition);
}
.toast.is-in { opacity: 1; transform: translateX(0) scale(1); }
.toast.is-out { opacity: 0; transform: translateX(24px) scale(0.98); }
.toast__icon { width: 20px; height: 20px; flex: none; margin-top: 1px; }
.toast__body { flex: 1 1 auto; min-width: 0; }
.toast__title { font-weight: var(--fw-semibold); }
.toast__msg { color: var(--text-2); line-height: 1.4; }
.toast__close {
  flex: none; width: 24px; height: 24px; border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-3); margin: -2px -4px 0 0;
}
.toast__close:hover { background: var(--surface-3); color: var(--text); }
.toast__close .icon { width: 16px; height: 16px; }
.toast--success { border-left-color: var(--success); } .toast--success .toast__icon { color: var(--success); }
.toast--error   { border-left-color: var(--danger); }  .toast--error   .toast__icon { color: var(--danger); }
.toast--warning { border-left-color: var(--warning); } .toast--warning .toast__icon { color: var(--warning); }
.toast--info    { border-left-color: var(--brand-sky); } .toast--info   .toast__icon { color: var(--brand-sky); }
@media (max-width: 767px) {
  .toast-region { top: calc(env(safe-area-inset-top, 0px) + 12px); left: 12px; right: 12px; width: auto; }
}

/* ---- Table column chooser (design standard) ---- */
.tbl-cols__title { padding: 9px 14px 4px; font-size: var(--fs-xs); font-weight: var(--fw-semibold); text-transform: uppercase; letter-spacing: .04em; color: var(--text-3); }
.tbl-cols__opt { display: flex; align-items: center; gap: 10px; padding: 7px 14px; font-size: var(--fs-sm); color: var(--text); cursor: pointer; white-space: nowrap; user-select: none; }
.tbl-cols__opt:hover { background: var(--surface-2); }
.tbl-cols__opt input { width: 15px; height: 15px; accent-color: var(--brand-blue); flex: none; margin: 0; }

/* ── Branded confirm / alert / prompt dialog (replaces native confirm()/alert()) ── */
.dsd-dialog { position: fixed; inset: 0; z-index: 1200; display: flex; align-items: center; justify-content: center; padding: var(--space-4); }
.dsd-dialog[hidden] { display: none; }
.dsd-dialog__backdrop { position: absolute; inset: 0; background: var(--overlay); backdrop-filter: blur(2px); }
.dsd-dialog__card { position: relative; width: min(440px, 100%); background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-xl); overflow: hidden; animation: dsd-dialog-in .16s ease; }
@keyframes dsd-dialog-in { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .dsd-dialog__card { animation: none; } }
.dsd-dialog__body { display: flex; gap: var(--space-3); padding: var(--space-5) var(--space-5) var(--space-4); }
.dsd-dialog__icon { flex: none; width: 44px; height: 44px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; }
.dsd-dialog__icon svg { width: 22px; height: 22px; }
.dsd-dialog__icon--info   { background: color-mix(in srgb, var(--brand-blue) 14%, var(--surface)); color: var(--brand-blue); }
.dsd-dialog__icon--danger { background: color-mix(in srgb, var(--danger) 14%, var(--surface)); color: var(--danger); }
.dsd-dialog__main { min-width: 0; flex: 1 1 auto; }
.dsd-dialog__title { font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--text); line-height: 1.3; }
.dsd-dialog__msg { margin-top: var(--space-1); color: var(--text-2); font-size: var(--fs-sm); line-height: 1.5; overflow-wrap: break-word; }
.dsd-dialog__input { margin-top: var(--space-3); width: 100%; }
.dsd-dialog__foot { display: flex; justify-content: flex-end; gap: var(--space-2); padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--border); background: color-mix(in srgb, var(--border) 12%, var(--surface)); }


/* ---- Report sheets are PAPER (8.119.8) ---------------------------------------------------------
   A report "sheet" is a screen replica of the printed/PDF document, so its renderer hardcodes
   color:#0f172a — it must, because the same HTML is fed to mPDF. Inheriting the themed .panel
   background therefore rendered the title and summary numbers dark-on-dark in dark mode: on the OIG
   report the contrast measured ~1:1, i.e. invisible. A document replica should look like paper in every
   theme (17.85:1 after). Applied to every sheet via one class rather than six copies of the rule.
   The live app TABLES beside them are NOT paper and stay theme-aware. */
.panel.report-sheet { background: #fff; color: #0f172a; border-color: var(--n-300); }
/* NOTE the .panel.report-sheet compound (0,2,0), not a bare .report-sheet (0,1,0). `.panel` is declared in
   dashboard.css (:422) with `background: var(--surface)` — the SAME specificity — and dashboard.css loads
   AFTER components.css (partials/head.php), so it wins the source-order tie and the bare selector is inert.
   Measured before this fix: the BLS sheet stayed rgb(19,28,46) in dark, contrast 1.48:1. Do not "simplify"
   this back to .report-sheet. */

/* ---- SHEETS FOLLOW THE THEME ON SCREEN (8.220.7, owner decision 2026-07-21) -------------------
   The rule above made every sheet paper-white in BOTH themes, which fixed invisible ink but left a
   white slab in a dark app. The owner asked for the screen view to be dark; the PRINT and PDF output
   must not move a pixel.
   HOW THAT GUARANTEE IS MADE, and it is structural rather than careful:
     1. Everything here is inside `@media screen`, so the browser's own print path never sees it.
     2. mPDF never loads this file at all — it builds its own inline CSS from the renderer — so the
        PDF cannot be affected by anything in assets/css/.
     3. app/reports.php is NOT touched. The renderers still emit the same bytes, which is the real
        invariant behind "the PDF is unchanged".
   A .badge is EXEMPT from the ink remap: it carries a matched colour+background PAIR inline
   (e.g. #991b1b on #fee2e2), so recolouring only its text left light red on pale pink at 2.48:1 —
   measured after the first attempt at this. A self-consistent pill is already readable on any
   sheet; leave the pair alone.
   WHY VARIABLES rather than a dark-mode override block: in LIGHT mode each variable below IS the
   literal the renderer already hardcodes, so the light sheet is byte-for-byte the same design and
   this section cannot regress it. Only the variable values change per theme — the rules are
   unconditional, which also avoids writing every selector twice for the explicit-dark and
   OS-preference paths (the trap that left auto-dark half-fixed elsewhere).
   `!important` IS REQUIRED: the renderers emit colours as INLINE style attributes (they must —
   mPDF needs them), and inline beats any selector without it. The `[style*=…]` matching is the same
   technique print.css already uses to flatten these sheets for paper.
   NOT APPLIED TO THE OIG REPORT: `.oig-report` is a parity-locked reproduction of a federal
   document and is deliberately excluded — see the :not() below. */
:root {
  --sheet-bg:    #ffffff;
  --sheet-ink:   #0f172a;
  --sheet-ink-2: #334155;
  --sheet-ink-3: #64748b;
  --sheet-line:  #e2e8f0;
  --sheet-rule:  #d7dee8;
  --sheet-card:  #f8fafc;
  --sheet-note:  #f1f5f9;
  --sheet-band:  #1e293b;
  --sheet-link:  #1e56c9;
  --sheet-past:  #fef4f4;
  --sheet-soon:  #f4f7fe;
}
:root[data-theme="dark"] {
  --sheet-bg:    var(--surface);
  --sheet-ink:   var(--text);
  --sheet-ink-2: var(--text-2);
  --sheet-ink-3: var(--text-3);
  --sheet-line:  var(--border);
  --sheet-rule:  var(--border-strong);
  --sheet-card:  var(--surface-2);
  --sheet-note:  var(--surface-2);
  --sheet-band:  var(--surface-3);
  --sheet-link:  var(--link);
  --sheet-past:  var(--danger-bg);
  --sheet-soon:  var(--info-bg);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="dark"]) {
    --sheet-bg: var(--surface);      --sheet-ink: var(--text);
    --sheet-ink-2: var(--text-2);    --sheet-ink-3: var(--text-3);
    --sheet-line: var(--border);     --sheet-rule: var(--border-strong);
    --sheet-card: var(--surface-2);  --sheet-note: var(--surface-2);
    --sheet-band: var(--surface-3);  --sheet-link: var(--link);
    --sheet-past: var(--danger-bg);  --sheet-soon: var(--info-bg);
  }
}

@media screen {
  .panel.report-sheet:not(:has(.oig-report)) { background: var(--sheet-bg); color: var(--sheet-ink); border-color: var(--sheet-line); }

  /* THE BASE INK, and the one that actually matters. Each renderer opens with
     `.skl-report{color:#0f172a}` (a scoped <style> rule, not an inline attribute), and every table
     cell that sets no colour of its own INHERITS it. Overriding only the inline literals left the
     body text — "CNA", "HCC", every date, every day-count — near-black on a dark sheet. Targeting
     the sheet's direct child re-roots that inheritance for the whole document in one rule. */
  .report-sheet:not(:has(.oig-report)) > * { color: var(--sheet-ink); }

  /* the renderers' own scoped rules (skills + BLS share this vocabulary).
     #16: the header band is ALWAYS dark (light mode --sheet-band #1e293b; dark mode a dark surface), so
     its text must be light — colouring it var(--sheet-ink) rendered near-black text on the navy band on
     the BLS report (unreadable). It now uses the THEMED section band (--sect-band, matching the dashboard
     bands + the skills-due report's own override) with white text + a shadow for contrast, so every
     report header follows the selected theme AND stays legible. Screen-only; print is unaffected. */
  .report-sheet:not(:has(.oig-report)) table.rt th { background: var(--sect-band, var(--sheet-band)) !important; color: #fff !important; text-shadow: 0 1px 2px rgba(0,0,0,.35); }
  .report-sheet:not(:has(.oig-report)) table.rt td { border-bottom-color: var(--sheet-line) !important; }
  /* #16 uniformity — give the report tables the SAME rounded container the platform's standard
     .dsd-table has (it rounds via its wrapper). These tables have no wrapper, so switch to
     border-collapse:separate (spacing 0, so it still reads like a collapsed grid) and clip with
     border-radius + overflow:hidden — this rounds the header's top corners and the last row's
     bottom corners, matching every other data table on the platform. Screen-only; print untouched. */
  .report-sheet:not(:has(.oig-report)) table.rt {
    border-collapse: separate !important; border-spacing: 0 !important;
    border: 1px solid var(--sheet-line) !important; border-radius: 10px !important; overflow: hidden;
  }
  .report-sheet:not(:has(.oig-report)) table.rt th:first-child { border-top-left-radius: 10px; }
  .report-sheet:not(:has(.oig-report)) table.rt th:last-child  { border-top-right-radius: 10px; }
  .report-sheet:not(:has(.oig-report)) td.sc       { background: var(--sheet-card) !important; border-color: var(--sheet-rule) !important; }
  .report-sheet:not(:has(.oig-report)) td.sc-total { border-top-color: var(--sheet-ink-2) !important; }
  /* sc-past / sc-soon carry `sc` too, so the generic card rule above would flatten their tint and
     lose the only thing that separates "past due" from "due soon" at a glance. Restore it in the
     theme's own status tints rather than the paper pinks. */
  .report-sheet:not(:has(.oig-report)) td.sc-past { background: var(--sheet-past) !important; }
  .report-sheet:not(:has(.oig-report)) td.sc-soon { background: var(--sheet-soon) !important; }

  /* the inline literals the renderer writes as style attributes */
  .report-sheet:not(:has(.oig-report)) [style*="color:#0f172a"]:not(.badge) { color: var(--sheet-ink) !important; }
  .report-sheet:not(:has(.oig-report)) [style*="color:#1e293b"]:not(.badge) { color: var(--sheet-ink) !important; }
  .report-sheet:not(:has(.oig-report)) [style*="color:#334155"]:not(.badge) { color: var(--sheet-ink-2) !important; }
  .report-sheet:not(:has(.oig-report)) [style*="color:#64748b"]:not(.badge) { color: var(--sheet-ink-3) !important; }
  .report-sheet:not(:has(.oig-report)) [style*="color:#1b1b1b"]:not(.badge) { color: var(--sheet-ink) !important; }
  .report-sheet:not(:has(.oig-report)) [style*="color:#1e56c9"]:not(.badge) { color: var(--sheet-link) !important; }
  /* Status ink. The paper reds/greens are chosen for white and go nearly black on a dark sheet —
     the past-due "Next Due" date measured 2.05:1 before this. The theme's own status tokens are
     already lightened for dark backgrounds, so they are the right substitute. */
  .report-sheet:not(:has(.oig-report)) [style*="color:#991b1b"]:not(.badge) { color: var(--danger) !important; }
  .report-sheet:not(:has(.oig-report)) [style*="color:#b91c1c"]:not(.badge) { color: var(--danger) !important; }
  .report-sheet:not(:has(.oig-report)) [style*="color:#059669"]:not(.badge) { color: var(--success) !important; }
  .report-sheet:not(:has(.oig-report)) [style*="background:#f8fafc"] { background: var(--sheet-card) !important; }
  .report-sheet:not(:has(.oig-report)) [style*="background:#f1f5f9"] { background: var(--sheet-note) !important; color: var(--sheet-ink) !important; }
}

/* ── Branded dialogs (8.161.0) ─────────────────────────────────────────────
   Replaces the browser's confirm()/alert()/prompt(), which are unbranded, say
   "dracosd.com says", cannot show a destructive-red action, ignore the platform
   themes, and on a compliance product read like a phishing prompt at the moment
   a user is about to destroy a signed regulatory record. See dsdConfirm in app.js. */
.dsd-dialog .modal__dialog { max-width: 460px; }
.dsd-dialog__head { align-items: flex-start; }
.dsd-dialog__head h2 { white-space: normal; }          /* a question may need two lines */
.dsd-dialog__icon { flex: none; width: 34px; height: 34px; border-radius: 50%;
                    display: flex; align-items: center; justify-content: center; }
.dsd-dialog__icon::before { content: ''; width: 17px; height: 17px; background: currentColor;
                            -webkit-mask: var(--dlg-icon) center/contain no-repeat; mask: var(--dlg-icon) center/contain no-repeat; }
.dsd-dialog__icon--danger  { background: var(--danger-bg);  color: var(--danger);
  --dlg-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z'/></svg>"); }
/* --info, NOT --brand-sky. The sky family is the USER-CONFIGURABLE accent (dsd_accent_css()
   rewrites it), so pairing it with the fixed cyan --info-bg measured 2.64:1 in light theme —
   below even the 3:1 non-text floor — and the exact ratio moved with whatever accent the user
   picked, so it could not be fixed by nudging one colour. --info (#0e7490 light / #38bdf8 dark)
   is a status token no palette overrides, which is why this holds across all 20 palettes AND any
   custom accent. Measured: 4.79:1 light, 6.90:1 dark. It also makes this icon match its own
   siblings, which already pair --success with --success-bg and --danger with --danger-bg. */
.dsd-dialog__icon--info    { background: var(--info-bg, var(--surface-3)); color: var(--info);
  --dlg-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 11v5M12 8h.01'/></svg>"); }
.dsd-dialog__icon--success { background: var(--success-bg, var(--surface-3)); color: var(--success);
  --dlg-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='m8.5 12.2 2.4 2.4 4.6-4.9'/></svg>"); }
.dsd-dialog__msg { margin: 0; color: var(--text-2); line-height: 1.6; white-space: pre-line; }
.dsd-dialog__foot { display: flex; justify-content: flex-end; gap: var(--space-2); }

/* ── Summary chips — the card variant ──────────────────────────────────────
   ONE definition. This block was copy-pasted into THREE module sheets
   (cna-renewals.css, forms.css, reports.css) because each of those modules
   loads only its own stylesheet and each needed the chips. components.css is
   core — partials/head.php:84 links it on every page — so one copy here
   reaches all three and is the only place the component is defined.

   Consumers of the card variant (verified on production by reading each page's
   own <link> list, not by assuming):
     Reports views          module.php?m=reports&r=…        (was reports.css)
     Form Submissions       dashboard/form-submissions.php  (was forms.css)
     Employee Evaluations   module.php?m=employee-evaluations&view=…
                                                            (was cna-renewals.css)
   The CNA Renewal Tracker — the page these classes are NAMED after — no longer
   renders a single .cnar-chip; its strip became the platform stat-card row, so
   cna-renewals.css was carrying this block purely for Employee Evaluations,
   which module.php:33 loads it for.

   This block was ALSO missing from training-records.css, and that is the whole
   of the ".tr-chips has no layout" defect: that markup is `class="cnar-chips
   tr-chips"` and was relying on a base .cnar-chips that never loaded on the
   page, so the strip computed to display:block with no gap and the
   `.tr-head .tr-chips { justify-content: flex-end }` rule could never take
   effect. Defining .cnar-chips here fixes it at the cause.

   The compact pill used by Training Records and Skills Evaluations is a
   DIFFERENT component that happens to reuse these class names. It lives in
   training-records.css as .dsd-chipmini and explicitly resets the properties
   this base would otherwise leak into it — do not "simplify" those resets
   away. */
.cnar-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-4); }
.cnar-chip { display: flex; flex-direction: column; min-width: 96px; padding: var(--space-3) var(--space-4);
  background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--border-strong);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-xs); text-decoration: none;
  transition: box-shadow var(--transition-fast); }
.cnar-chip:hover { box-shadow: var(--shadow-sm); }
.cnar-chip.is-on { outline: 2px solid var(--brand-blue); outline-offset: -2px; }
.cnar-chip__n { font-size: var(--fs-2xl, 1.7rem); font-weight: var(--fw-bold); color: var(--text);
  font-variant-numeric: tabular-nums; line-height: 1; }
.cnar-chip__l { font-size: var(--fs-xs); color: var(--text-3); font-weight: var(--fw-medium); margin-top: 4px; }
/* Left-border accents — the union of the sets cna-renewals.css and reports.css
   each carried. They only ever set border-left-color, so a page that never uses
   a given variant is unaffected by its presence here. */
.cnar-chip--danger  { border-left-color: var(--danger); }
.cnar-chip--success { border-left-color: var(--success); }
.cnar-chip--ok      { border-left-color: var(--success); }
.cnar-chip--info    { border-left-color: var(--brand-blue); }
.cnar-chip--accent  { border-left-color: var(--accent); }
.cnar-chip--muted   { border-left-color: var(--text-3); }
.cnar-chip--missing { border-left-color: #475569; }
.cnar-chip--soon    { border-left-color: #2563eb; }

/* ── .cmx-toolbar — a SHARED toolbar used by the Compliance Matrix, the Reports module and the
   CNA Renewal Tracker. It lived, byte-identical, in BOTH compliance.css and reports.css because
   the Reports module does not load compliance.css and the tracker does. Hoisted here (loaded on
   every page by partials/head.php) so there is one definition; heights come from --control-h so a
   row cannot drift again. 8.196.0. */
.cmx-toolbar { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin-bottom: 0; flex: 1 1 auto; }
.cmx-toolbar .cmx-search .input { height: var(--control-h); min-height: var(--control-h); padding: 0 12px 0 38px; }
.cmx-toolbar .select { height: var(--control-h); min-height: var(--control-h); padding-top: 0; padding-bottom: 0; }
.cmx-toolbar .btn { min-height: var(--control-h); }

/* .tr-toolbar — the Training Records / Skills Evaluations / Survey Results toolbar. It had NO
   control-height rules at all, so its row rendered FOUR heights side by side, measured live on
   production 2026-07-20: search input 50px (base .input is padding-only, no height), selects 36px,
   the checkbox pill 38px, and the button 30px (the .btn--sm responsive override at :241). The
   search box being 20px taller than the button beside it is the mismatch the owner reported.
   Same token, same treatment as .cmx-toolbar above. 8.196.0. */
.tr-toolbar .input, .tr-toolbar .tr-search .input { height: var(--control-h); min-height: var(--control-h); padding-top: 0; padding-bottom: 0; }
.tr-toolbar select, .tr-toolbar .select { height: var(--control-h); min-height: var(--control-h); padding-top: 0; padding-bottom: 0; }
.tr-toolbar .btn { min-height: var(--control-h); height: var(--control-h); }

/* ---------------------- Branded navigation loader ----------------------- */
/* Full-page server-rendered navigations leave the content area blank for a few
   seconds on heavy reports (e.g. CNA License Verification). app.js shows this
   indicator on a slow internal navigation and it clears when the next page
   paints (or on pageshow/pagehide). The whole element is created by app.js and
   lives directly under <body>; it starts invisible and is revealed by class.
   Brand colours only (the palette's sky accent), no forbidden-hue fills. */
.dsd-loading {
  position: fixed;
  inset: 0;
  z-index: 2147483000;          /* above every app layer incl. modal/toast */
  pointer-events: none;         /* stage 1 (bar only) never blocks the page  */
  opacity: 1;
}

/* Stage 1 — a thin brand-sky progress bar pinned to the very top. It "creeps"
   toward 90% and never completes: the real completion is the next page paint.
   Sits over the navy topbar, so it uses the bright sky accent to stay visible. */
.dsd-loading__bar {
  position: fixed;
  top: 0; left: 0;
  height: 3px;
  width: 100%;
  transform: scaleX(0);
  transform-origin: left center;
  background: linear-gradient(90deg, var(--brand-sky), var(--brand-sky-bright));
  box-shadow: 0 0 8px color-mix(in srgb, var(--brand-sky-bright) 70%, transparent);
  opacity: 0;
  transition: opacity .12s ease;
}
.dsd-loading.is-visible { }
.dsd-loading.is-visible .dsd-loading__bar {
  opacity: 1;
  animation: dsd-load-creep 8s cubic-bezier(.15,.85,.25,1) forwards;
}
@keyframes dsd-load-creep {
  0%   { transform: scaleX(0); }
  25%  { transform: scaleX(.32); }
  55%  { transform: scaleX(.62); }
  80%  { transform: scaleX(.82); }
  100% { transform: scaleX(.9); }
}

/* Stage 2 — for genuinely heavy waits only (revealed later by app.js), a soft
   scrim over the outgoing page with the Draco mark, so the user gets an
   unmistakably branded "we're loading" moment on the slowest pages. The scrim
   uses the neutral app background token (theme-aware, never a forbidden hue). */
.dsd-loading__panel {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  opacity: 0;
  visibility: hidden;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(2px);
  transition: opacity .2s ease, visibility .2s ease;
}
.dsd-loading.is-heavy { pointer-events: auto; }   /* block double-clicks now   */
.dsd-loading.is-heavy .dsd-loading__panel { opacity: 1; visibility: visible; }

/* The Draco dragon mark. --watermark-src is defined per theme in head.php
   (draco_b.png on light, draco_w.png on dark), so this is automatically
   theme-aware and never needs a second asset reference. */
.dsd-loading__mark {
  width: 72px; height: 72px;
  background-image: var(--watermark-src);
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  animation: dsd-load-pulse 1.5s ease-in-out infinite;
}
@keyframes dsd-load-pulse {
  0%, 100% { opacity: .55; transform: scale(.94); }
  50%      { opacity: 1;   transform: scale(1); }
}
.dsd-loading__cap {
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  letter-spacing: .02em;
  color: var(--text-2);
}

/* Reduced motion: no creep, no pulse. The bar fades in at a fixed partial width
   and the mark holds steady — still a clear "loading" signal, just no motion. */
@media (prefers-reduced-motion: reduce) {
  .dsd-loading.is-visible .dsd-loading__bar { animation: none; transform: scaleX(.35); }
  .dsd-loading__mark { animation: none; opacity: .85; transform: none; }
}
