/* ===============================================================
   SAHE SMS — Design System Enhancements
   Layered on top of design-system.css. This file ONLY adds and
   extends; it never renames or removes existing classes.

   Sections:
     1. Responsive 12-column grid (sm / md / lg + mobile stacking)
     2. Bootstrap-flavoured modal styling (paired with js/modal.js)
     3. Production polish (typography rhythm, shadows, micro-interactions)
     4. Accessibility (focus rings, darker muted/warning text)
     5. Comprehensive dark-mode overrides
   =============================================================== */

/* ===============================================================
   1. RESPONSIVE 12-COLUMN GRID
   The base file only defined a handful of fixed-width col-md-*.
   Generate the full 12-col set for every breakpoint templates use,
   and stack everything on small screens.
   =============================================================== */

/* Base (all widths) — full 1..12 set. These act as the "xs" defaults. */
.col-1  { width: 8.3333%; }
.col-2  { width: 16.6667%; }
.col-3  { width: 25%; }
.col-4  { width: 33.3333%; }
.col-5  { width: 41.6667%; }
.col-6  { width: 50%; }
.col-7  { width: 58.3333%; }
.col-8  { width: 66.6667%; }
.col-9  { width: 75%; }
.col-10 { width: 83.3333%; }
.col-11 { width: 91.6667%; }
.col-12 { width: 100%; }

/* >= 576px (sm) */
@media (min-width: 576px) {
    .col-sm-1  { width: 8.3333%; }
    .col-sm-2  { width: 16.6667%; }
    .col-sm-3  { width: 25%; }
    .col-sm-4  { width: 33.3333%; }
    .col-sm-5  { width: 41.6667%; }
    .col-sm-6  { width: 50%; }
    .col-sm-7  { width: 58.3333%; }
    .col-sm-8  { width: 66.6667%; }
    .col-sm-9  { width: 75%; }
    .col-sm-10 { width: 83.3333%; }
    .col-sm-11 { width: 91.6667%; }
    .col-sm-12 { width: 100%; }
}

/* >= 768px (md) — full set (base file only had 2,3,4,6) */
@media (min-width: 768px) {
    .col-md-1  { width: 8.3333%; }
    .col-md-2  { width: 16.6667%; }
    .col-md-3  { width: 25%; }
    .col-md-4  { width: 33.3333%; }
    .col-md-5  { width: 41.6667%; }
    .col-md-6  { width: 50%; }
    .col-md-7  { width: 58.3333%; }
    .col-md-8  { width: 66.6667%; }
    .col-md-9  { width: 75%; }
    .col-md-10 { width: 83.3333%; }
    .col-md-11 { width: 91.6667%; }
    .col-md-12 { width: 100%; }
}

/* >= 992px (lg) — full set (base file only had 4,6,8) */
@media (min-width: 992px) {
    .col-lg-1  { width: 8.3333%; }
    .col-lg-2  { width: 16.6667%; }
    .col-lg-3  { width: 25%; }
    .col-lg-4  { width: 33.3333%; }
    .col-lg-5  { width: 41.6667%; }
    .col-lg-6  { width: 50%; }
    .col-lg-7  { width: 58.3333%; }
    .col-lg-8  { width: 66.6667%; }
    .col-lg-9  { width: 75%; }
    .col-lg-10 { width: 83.3333%; }
    .col-lg-11 { width: 91.6667%; }
    .col-lg-12 { width: 100%; }
}

/* Mobile stacking — every column becomes full width below the md breakpoint.
   This fixes the original "fixed percent, no stacking" bug. */
@media (max-width: 767.98px) {
    [class*="col-"] {
        width: 100% !important;
        flex: 0 0 100% !important;
        max-width: 100% !important;
    }
}

/* ===============================================================
   2. BOOTSTRAP-FLAVOURED MODALS  (paired with js/modal.js)
   Scoped to `.modal.fade` so it never clashes with the in-house
   `.modal.active` dialogs styled in design-system.css.
   =============================================================== */
.modal.fade {
    display: none;
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    width: 100%;
    height: 100%;
    overflow-x: hidden;
    overflow-y: auto;
    padding: var(--space-6) var(--space-4);
    /* reset the centred-transform the base .modal applies */
    top: auto;
    left: auto;
    transform: none;
    max-width: none;
    max-height: none;
    background: transparent;
    box-shadow: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--dur-rise) var(--ease-out-editorial);
}

.modal.fade.show {
    display: block;
    opacity: 1;
    visibility: visible;
}

.modal.fade .modal-dialog {
    width: 100%;
    max-width: 520px;
    margin: 5vh auto;
    transform: translateY(-12px);
    transition: transform var(--dur-rise) var(--ease-out-editorial);
}

.modal.fade.show .modal-dialog { transform: translateY(0); }

.modal.fade .modal-dialog.modal-lg { max-width: 800px; }
.modal.fade .modal-dialog.modal-sm { max-width: 360px; }

.modal.fade .modal-content {
    background: var(--white);
    border-radius: var(--radius-xl);
    border: 1px solid var(--gray-200);
    box-shadow: var(--shadow-xl);
    overflow: hidden;
}

/* The shared backdrop sits behind the dialog. */
.modal-backdrop.js-modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.5);
    z-index: var(--z-modal-backdrop);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
}

body.modal-open { overflow: hidden; }

/* Bootstrap close-button glyph (no Bootstrap CSS present to draw it). */
.btn-close {
    width: 32px;
    height: 32px;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--gray-500);
    border-radius: var(--radius-md);
    cursor: pointer;
    font-size: 0;
    line-height: 1;
    transition: background-color var(--dur-colour) var(--ease-out-editorial),
                border-color var(--dur-colour) var(--ease-out-editorial),
                box-shadow var(--dur-colour) var(--ease-out-editorial),
                color var(--dur-colour) var(--ease-out-editorial);
    flex-shrink: 0;
}
.btn-close::before {
    content: "\00d7";          /* multiplication sign — a clean × */
    font-size: 22px;
    font-weight: 400;
    line-height: 1;
}
.btn-close:hover { background: var(--gray-100); color: var(--gray-800); }
.btn-close:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(30, 58, 95, 0.25);
}

/* ===============================================================
   3. PRODUCTION POLISH
   =============================================================== */

/* Smoother page-content entrance.
   ---------------------------------------------------------------------------
   FILL MODE IS LOad-BEARING — do not change `backwards` back to `both`.

   With `both`, the final keyframe's `transform: translateY(0)` is retained on
   the element FOREVER after the animation finishes. A non-`none` transform
   creates a stacking context, which trapped every `.modal` inside `.container`:
   the dialog's `z-index: 1050` was then resolved *within* that context, so the
   body-level `.modal-backdrop` (z-index 1040, `backdrop-filter: blur(2px)`)
   painted OVER the dialog — every modal in the app rendered dimmed and blurred.

   `backwards` keeps the identical entrance (the `from` state is applied before
   the animation starts, so there is no first-frame flash) but lets the element
   revert to `transform: none` once it finishes, leaving no stacking context. */
.main-content > .container { animation: saheFadeUp .32s ease backwards; }
@keyframes saheFadeUp {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
}
/* Belt-and-braces: while any modal is open the container must never hold a
   transform, even during the 0.32s entrance window. Both modal systems
   (js/modal.js `.modal.fade` and js/modern.js `.modal.active`) set
   `body.modal-open`. */
body.modal-open .main-content > .container {
    animation: none !important;
    transform: none !important;
}
@media (prefers-reduced-motion: reduce) {
    .main-content > .container,
    .modal.fade,
    .modal.fade .modal-dialog { animation: none !important; transition: none !important var(--dur-colour) var(--ease-out-editorial); }
}

/* Cards: a touch more lift on hover for interactive feel, consistent radius. */
.card,
.content-card,
.panel {
    transition: box-shadow var(--dur-rise) var(--ease-out-editorial),
                border-color var(--dur-rise) var(--ease-out-editorial),
                transform var(--dur-rise) var(--ease-out-editorial);
}

/* .section-title now lives in design-system.css as part of the Section
   pattern (§8.5): the 10px caps label, the fading rule and an optional
   action. The copy here was a 14px 0.06em variant that loaded LAST and so
   silently beat it, which would have left the section headers on some pages
   a size and a tracking apart from the ones on the rest. */

/* Subtle gradient sheen on primary buttons for a premium feel. */
.btn-primary {
    background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(0, 0, 0, 0.04));
}

/* Tables: zebra-free but with crisper hover and rounded container. */
.table-container,
.table-wrapper,
.table-responsive {
    border: 1px solid var(--gray-200);
    background: var(--white);
}

/* Status / badge pills get a hairline for definition on white. */
.badge { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.02); }

/* Metric tile accent — a discreet brand cue.
   Previously a left-edge bar (`.metric-card::before` at left:0): the first
   tile in a metrics row has zero inline-start padding, so its label sat ON
   the bar, and for later tiles the bar landed on the column hairline — on
   hover the status text visibly collided with "the border". The accent now
   underlines the tile's own content, which is aligned with the text in every
   column and can never overlap it. */
.metric-card .metric-content { position: relative; }
.metric-card .metric-content::after {
    content: "";
    position: absolute;
    left: 0; bottom: -6px;
    height: 2px; width: 28px;
    border-radius: var(--radius-full);
    background: transparent;
    transition: background var(--dur-colour) var(--ease-out-editorial);
}
.metric-card:hover .metric-content::after { background: var(--primary); }

/* Inputs: slightly softer rest state, clear focus already defined upstream. */
input, select, textarea, .form-control {
    box-shadow: var(--shadow-xs);
}

/* The empty-state illustration is a hairline diagram in currentColor, not
   a glyph on a tinted chip (§9.15). The tinted circle is reserved for the
   full-page status medallion (§9.24); using it here made "nothing here"
   look like an error. The recipe now lives in design-system.css. */

/* ===============================================================
   4. ACCESSIBILITY
   =============================================================== */

/* Always-visible keyboard focus ring across interactive elements. */
a:focus-visible,
button:focus-visible,
.btn:focus-visible,
.nav-link:focus-visible,
.action-btn:focus-visible,
.dropdown-item:focus-visible,
[tabindex]:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* Darken muted + warning text for AA contrast on white. */
.text-muted,
.page-subtitle,
.card-subtitle,
.metric-label,
.stat-label { color: var(--gray-600) !important; }

.text-warning { color: var(--warning-dark) !important; }

/* Skip-to-content affordance (progressive — used if a template adds it). */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 10000;
    background: var(--primary);
    color: #fff;
    padding: var(--space-2) var(--space-4);
    border-radius: 0 0 var(--radius-md) 0;
}
.skip-link:focus { left: 0; }

/* =====================================================================
   FOUNDATION ADD-ONS (Session 3) — fill remaining utility/component gaps
   so no page renders unstyled, and standardise tables/cards/charts.
   ===================================================================== */

/* --- Missing button variants/aliases used across templates --- */
.btn-xs { padding: 2px 8px; font-size: 0.72rem; line-height: 1.4; border-radius: 6px; }
.btn-save  { background: var(--primary); color:#fff; border:1px solid var(--primary); }
.btn-save:hover { background: var(--primary-dark, var(--ink)); }
.btn-submit { background: var(--primary); color:#fff; }
.btn-cancel, .btn-back { background:var(--surface); color: var(--gray-700,var(--ink-soft)); border:1px solid var(--gray-300,var(--rule-strong)); }
.btn-cancel:hover, .btn-back:hover { background: var(--gray-50,var(--paper)); }
.btn-delete { background: var(--danger,var(--crimson-strong)); color:#fff; border:1px solid var(--danger,var(--crimson-strong)); }
.btn-delete:hover { filter: brightness(0.93); }

/* --- Table polish: hover + responsive scroll + zebra option --- */
.table-hover tbody tr:hover,
.data-table.table-hover tbody tr:hover { background: var(--primary-50, var(--surface-alt)); }
.table-responsive { width:100%; overflow-x:auto; -webkit-overflow-scrolling:touch; }

/* Opt-in sticky column headers for long tables.
   `.data-table th` already carries position:sticky, but it never engages inside
   a plain .table-responsive: overflow-x:auto forces overflow-y to auto, so the
   wrapper is the scroll container, and with no height cap it grows to fit and
   never scrolls. Capping the height gives the header something to stick to. */
.table-responsive.table-sticky,
.table-wrapper.table-sticky { max-height: 70vh; overflow-y: auto; }
.data-table.striped tbody tr:nth-child(even),
.table.striped tbody tr:nth-child(even) { background: var(--gray-50, var(--paper)); }

/* Row action buttons cluster (consistent across all list pages) */
.row-actions { display:inline-flex; gap:6px; align-items:center; white-space:nowrap; }
.row-actions .btn { padding:4px 9px; font-size:0.78rem; }

/* --- Float/spacing utilities still referenced by templates --- */
.float-end { float:right !important; }
.float-start { float:left !important; }
.ms-auto { margin-left:auto !important; }
.me-auto { margin-right:auto !important; }
.w-100 { width:100% !important; }

/* --- Chart container: one consistent wrapper for all graphs --- */
.chart-card { background:var(--surface); border:1px solid var(--gray-200,var(--rule)); border-radius:14px; padding:20px 22px; box-shadow:0 1px 3px rgba(16,24,40,.06); }
.chart-card .chart-title { font-size:0.95rem; font-weight:700; color:var(--gray-800,var(--ink)); margin:0 0 14px; display:flex; align-items:center; gap:8px; }
/* MIN-height, never a fixed height. A chart is a plot PLUS a legend, and a
   legend's height is a function of how many series it names and how wide the
   card is -- a 13-slice doughnut wraps its key onto three lines. Pinned to
   `height`, anything past the 300px spilled straight out of the card and
   printed itself over the panel below (a category key landed on top of the
   Recent Tickets table). The wrapper now sets the floor and the card grows
   to whatever the key needs. */
.chart-wrap { position:relative; width:100%; min-height:300px; }
.chart-wrap.sm { min-height:220px; }
.chart-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(340px,1fr)); gap:20px; }

/* --- Empty state (standardised) --- */
.empty-state { text-align:center; padding:48px 24px; color:var(--gray-500,var(--ink-mute)); }
.empty-state > i { font-size:42px; color:var(--gray-300,var(--rule-strong)); margin-bottom:14px; display:block; }
.empty-state .btn i { font-size:0.8em; color:inherit; margin-bottom:0; display:inline-block; }
.empty-state h3 { color:var(--gray-600,var(--ink-soft)); margin:0 0 6px; font-size:1.05rem; }

/* =====================================================================
   FORM LAYOUT STANDARD (Session 3) — one polished, consistent form
   system. Fixes sprawling grids, floating checkboxes, and width so every
   create/edit form reads cleanly regardless of how fields are rendered.
   ===================================================================== */

/* Keep forms to a comfortable reading width even inside full-width panels. */
.form-container { max-width: 920px; margin: 0 auto; }
.panel > .panel-body > form .form-grid,
.form-card .form-grid,
.panel-body > form .form-grid { max-width: 100%; }

/* Canonical 2-column grid with sensible stacking; never auto-fill into 4+. */
.form-grid {
    display: grid !important;
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
    gap: 18px 22px !important;
    align-items: start;
}
.form-grid .full-width,
.form-grid .form-group.full-width { grid-column: 1 / -1; }
/* A textarea field should always span the full row. */
.form-grid .form-group:has(textarea) { grid-column: 1 / -1; }

.form-group { display: flex; flex-direction: column; gap: 6px; margin-bottom: 0; }
/* THE caps label — 10px at 0.09em, the same one the section headers, panel
   headers, stat labels and table columns use. This was 11.5px at 0.4px:
   close enough to look intentional, different enough that a form and the
   table beside it read as two systems. The bare `> label` selector is kept
   because ~40 forms label their field with a plain <label>. */
.form-group > label,
.form-group > .form-label {
    margin: 0;
    font-size: var(--text-label);
    line-height: var(--text-label-lh);
    letter-spacing: var(--text-label-ls);
    text-transform: uppercase;
    font-weight: var(--font-semibold);
    color: var(--ink-mute);
}

/* --- Checkboxes / radios: render as a clean inline toggle row --- */
.form-group input[type="checkbox"],
.form-group input[type="radio"],
input[type="checkbox"], input[type="radio"] {
    /* 24px, not 18: this global (with !important) is the floor for EVERY
       checkbox in the app, and a native checkbox's box IS its hit target --
       nothing else can carry the size for the 65 that have no label. 18px
       failed WCAG 2.5.8 on 82 controls across six forms, and silently
       overrode every page-level attempt to fix one. */
    width: 24px !important; height: 24px !important; min-height: 0 !important;
    padding: 0 !important; margin: 0; flex: 0 0 auto;
    accent-color: var(--primary, var(--ink)); cursor: pointer; vertical-align: middle;
    border-radius: 5px; appearance: auto;
}
/* When a form-group is just a checkbox/radio, lay it out as a tidy row card.
   LEGACY: this infers a checkbox row from the markup rather than being asked
   for. `.checkbox-row` below is the component to write on new work; the
   `:not(:has(> .checkbox-row))` guard stops this rule drawing a second box
   around a group that already contains one. */
.form-grid .form-group:not(:has(> .checkbox-row)):has(> input[type="checkbox"]:only-of-type),
.form-grid .form-group:not(:has(> .checkbox-row)):has(> label > input[type="checkbox"]) {
    flex-direction: row-reverse; align-items: center; justify-content: flex-end;
    gap: 10px; background: var(--gray-50, var(--paper)); border: 1px solid var(--gray-200,var(--rule));
    border-radius: 10px; padding: 11px 14px; align-self: stretch;
}
.form-grid .form-group:has(> input[type="checkbox"]) > label,
.form-grid .form-group:has(> input[type="checkbox"]) > .form-label {
    text-transform: none; letter-spacing: 0; font-size: .85rem; font-weight: 500;
    color: var(--gray-700,var(--ink-soft)); margin: 0; flex: 1;
}

/* =====================================================================
   `.checkbox-row` — THE checkbox / radio row. Use this, nothing else.

   Boolean fields were drawn four different ways across ~10 forms: a bare
   checkbox with a plain label, a grey half-width box (inferred by the
   `:has()` rules above), an uppercase micro-caption inherited from
   `.form-group > label`, and a label-right variant. The casing drifted with
   it -- "Active", "Is active", "IS ACTIVE", "ACTIVE RULE" all appear for the
   same field. Same control, four appearances, so nothing scans.

   INTENDED MARKUP — one option:

     <label class="checkbox-row">
       <input type="checkbox" name="is_active"
              {% if form.is_active.value %}checked{% endif %}>
       <span class="checkbox-row-label">Active</span>
       <span class="checkbox-row-hint">Inactive records stay in reports but
         stop appearing in pickers.</span>   {# optional, omit if obvious #}
     </label>

   INTENDED MARKUP — two or more options, or one option inside a .form-grid:

     <div class="checkbox-stack full-width">
       <label class="checkbox-row"> ... </label>
       <label class="checkbox-row"> ... </label>
     </div>

   RULES the markup must follow:
     * The <label> IS the row. Do not wrap it in a .form-group -- that is what
       pulled in the uppercase caption and the second grey box.
     * The <input> is the first child; the browser's implicit label
       association makes the whole row clickable, so no `for=`/`id=` pair and
       no JS is needed.
     * Label text is sentence case and names the state that TICKING the box
       produces: "Active", "Required", "Show on transcript". Never a field
       name ("Is active"), never uppercase, never a trailing colon.
     * Radios use exactly the same markup with type="radio". For radios that
       need a description-led card, use `.radio-card` instead.
   ===================================================================== */
.checkbox-row,
.form-group > .checkbox-row {
    display: grid;
    /* Fixed first track: the input is a fixed 18px (forced !important above),
       so an explicit track keeps every label edge on the same line whether or
       not a hint wraps to a second row. */
    grid-template-columns: 18px minmax(0, 1fr);
    column-gap: var(--space-3);
    row-gap: 2px;
    align-items: start;
    /* min-height, not min-width: a min-width floor on a flex/grid item lets it
       shrink below its own content and clip the label. */
    min-height: 44px;
    align-content: center;
    margin: 0;
    padding: var(--space-3) var(--space-4);
    background: var(--gray-50);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    cursor: pointer;
    transition: border-color var(--dur-colour) var(--ease-out-editorial),
                background-color var(--dur-colour) var(--ease-out-editorial);
}
.checkbox-row:hover { border-color: var(--gray-300); }
/* The ring goes on the row, not the 18px box, so keyboard focus is visible. */
.checkbox-row:focus-within {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(30, 58, 95, 0.1);
}
/* Checked reads as selected at a glance -- the same "filled = on" language the
   filter chips use. Browsers without :has() simply show the resting row. */
.checkbox-row:has(input:checked) {
    background: var(--primary-50);
    border-color: var(--primary-100);
}
.checkbox-row > input[type="checkbox"],
.checkbox-row > input[type="radio"] {
    grid-column: 1;
    grid-row: 1;
    /* Optical alignment with the cap height of the label beside it. */
    margin-top: 2px;
}
.checkbox-row > .checkbox-row-label,
.form-group > .checkbox-row > .checkbox-row-label {
    grid-column: 2;
    grid-row: 1;
    font-size: var(--text-sm);
    font-weight: var(--font-medium);
    line-height: 1.5;
    color: var(--gray-800);
    /* Beat the uppercase micro-caption `.form-group > label` applies. */
    text-transform: none;
    letter-spacing: 0;
}
.checkbox-row > .checkbox-row-hint {
    grid-column: 2;
    grid-row: 2;
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--gray-500);
    text-transform: none;
    letter-spacing: 0;
}
.checkbox-row input:disabled { cursor: not-allowed; }
.checkbox-row:has(input:disabled) { opacity: .6; cursor: not-allowed; }

/* Stack of rows. Named `.checkbox-stack` and not `.checkbox-group` because
   `.checkbox-group` already means something else in academics.css and
   admissions.css; reusing the name would give it two definitions. */
.checkbox-stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

/* No dark-mode block: every colour above is a --gray-*/--primary-* token and
   the ramp is re-pointed wholesale for dark, so the row follows on its own. A
   hard-coded dark override here made the checked state LESS distinct than the
   token it replaced. */

/* Standard form footer/actions row. */
.form-actions, .form-footer {
    display: flex; gap: 12px; justify-content: flex-end; align-items: center;
    padding: 18px 0 0; margin-top: 22px; border-top: 1px solid var(--gray-200,var(--rule));
}
.form-card .form-actions, .form-card .form-footer { padding: 16px 24px; margin-top: 0;
    background: var(--gray-50,var(--paper)); }

/* Responsive: single column on phones. */
@media (max-width: 640px){ .form-grid { grid-template-columns: 1fr !important; } }

/* =====================================================================
   Document-template module — shared bits that replace the old per-page
   inline <style> blocks (modal preview, rendered output, document editor).
   Centralised here so the markup uses shared classes only.
   ===================================================================== */

/* Wide variant of the standard .modal for rendered-document previews. */
.modal.modal-lg { max-width: 820px; }
.modal .rendered-content { border:1px solid var(--gray-200,var(--rule)); border-radius:8px;
    padding:24px; line-height:1.7; font-size:.9rem; max-height:60vh; overflow-y:auto; }

/* Rendered preview surface (preview page). */
.rendered-doc { background:var(--surface); border:1px solid var(--gray-200,var(--rule)); border-radius:8px;
    padding:28px; line-height:1.7; color:var(--gray-800,var(--ink)); }
.preview-subject { background:var(--gray-50,var(--paper)); border:1px solid var(--gray-200,var(--rule));
    border-radius:6px; padding:10px 14px; margin-bottom:16px; }
.preview-subject strong { color:var(--primary,var(--ink)); margin-right:6px; }

/* Document-template editor: code editor + variable sidebar (no shared eq). */
.tpl-editor { display:grid; grid-template-columns:1fr 380px; gap:24px; align-items:start; }
.tpl-editor__side { position:relative; }
.tpl-code { border:1px solid var(--gray-300,var(--rule-strong)); border-radius:8px; overflow:hidden; background:var(--surface); }
.tpl-code__toolbar { display:flex; align-items:center; gap:2px; padding:6px 10px;
    background:var(--gray-50,var(--paper)); border-bottom:1px solid var(--gray-200,var(--rule)); flex-wrap:wrap; }
.tpl-code__toolbar .sep { width:1px; height:20px; background:var(--gray-300,var(--rule-strong)); margin:0 6px; }
/* Toolbar buttons sat at 22px high, just under the 24px target floor. min-height
   lifts them without touching width -- min-width on these flex items would drop
   the min-width:auto that keeps the labelled ones from clipping their text. */
.tpl-tbtn { display:inline-flex; align-items:center; gap:5px; padding:5px 10px; border:none;
    min-height:24px;
    background:transparent; border-radius:4px; font-size:.76rem; color:var(--gray-600,var(--ink-soft));
    cursor:pointer; transition: background-color var(--dur-colour) var(--ease-out-editorial),
                border-color var(--dur-colour) var(--ease-out-editorial),
                box-shadow var(--dur-colour) var(--ease-out-editorial),
                color var(--dur-colour) var(--ease-out-editorial); font-family:inherit; }
.tpl-tbtn:hover { background:var(--primary,var(--ink)); color:#fff; }
.tpl-code textarea { width:100%; min-height:320px; padding:16px; border:none; outline:none;
    font-family:'Cascadia Code','Fira Code','Consolas',monospace; font-size:.82rem; line-height:1.7;
    resize:vertical; background:var(--paper); color:var(--ink); border-radius:0; }
.tpl-mono { font-family:'Cascadia Code','Consolas',monospace !important; font-size:.82rem !important; }
.tpl-tabs { display:flex; border-bottom:1px solid var(--gray-200,var(--rule)); }
.tpl-tab { flex:1; padding:11px 0; text-align:center; font-size:.78rem; font-weight:600;
    color:var(--gray-500,var(--ink-mute)); background:transparent; border:none; cursor:pointer;
    border-bottom:2px solid transparent; transition: background-color var(--dur-colour) var(--ease-out-editorial),
                border-color var(--dur-colour) var(--ease-out-editorial),
                box-shadow var(--dur-colour) var(--ease-out-editorial),
                color var(--dur-colour) var(--ease-out-editorial); }
.tpl-tab.active { color:var(--primary,var(--ink)); border-bottom-color:var(--primary,var(--ink));
    background:var(--gray-50,var(--paper)); }
.tpl-pane { display:none; }
.tpl-pane.active { display:block; }
.tpl-var-search input { width:100%; }
.tpl-var-group-head { display:flex; align-items:center; justify-content:space-between; padding:6px 10px;
    font-size:.72rem; font-weight:700; color:var(--primary,var(--ink)); text-transform:uppercase;
    letter-spacing:.04em; cursor:pointer; border-radius:4px; }
.tpl-var-group-head:hover { background:var(--surface-alt); }
.tpl-var-chips { padding:2px 4px 10px; display:flex; flex-wrap:wrap; gap:4px; }
.tpl-chip { display:inline-block; padding:3px 8px; background:var(--surface-alt); border:1px solid var(--gray-200,var(--rule));
    border-radius:4px; font-size:.72rem; font-family:'Cascadia Code','Consolas',monospace;
    color:var(--primary,var(--ink)); cursor:pointer; transition: background-color var(--dur-colour) var(--ease-out-editorial),
                border-color var(--dur-colour) var(--ease-out-editorial),
                box-shadow var(--dur-colour) var(--ease-out-editorial),
                color var(--dur-colour) var(--ease-out-editorial); white-space:nowrap; }
.tpl-chip:hover { background:var(--primary,var(--ink)); color:#fff; border-color:var(--primary,var(--ink)); }
.tpl-preview-doc { background:var(--surface); border:1px solid var(--gray-200,var(--rule)); border-radius:6px;
    padding:24px; font-size:.85rem; line-height:1.7; color:var(--ink); min-height:200px; }
.tpl-editor__side .panel { background:var(--surface); border:1px solid var(--gray-200,var(--rule)); border-radius:12px;
    overflow:hidden; box-shadow:0 1px 3px rgba(16,24,40,.06); }
.tpl-var-group-head span { background:var(--gray-100,var(--surface-alt)); color:var(--gray-500); font-size:11px;
    padding:1px 7px; border-radius:999px; font-weight:600; }
@media(max-width:1024px){ .tpl-editor{ grid-template-columns:1fr; } .tpl-editor__side{ position:static; } }

/* Global form error + text-transform utilities (used by standard forms). */
.form-error { font-size:.75rem; color:var(--danger,var(--crimson-strong)); display:flex; align-items:center; gap:.3rem; margin-top:2px; }
.form-error i { font-size:.7rem; }
.text-uppercase { text-transform:uppercase !important; }

/* =====================================================================
   POLISH PASS (Session 4) — input comfort + graceful placeholder
   truncation + premium visual refinements toward a 10/10 finish.
   ===================================================================== */

/* --- Inputs: never clip a placeholder; degrade long text to an ellipsis. --- */
.form-control, input[type="text"], input[type="email"], input[type="password"],
input[type="number"], input[type="tel"], input[type="date"], input[type="time"],
input[type="datetime-local"], input[type="url"], input[type="search"], select {
    min-height: 42px; height: 42px; line-height: 1.4;
    text-overflow: ellipsis;            /* long placeholder/value -> "…" not a hard cut */
}
input::placeholder, textarea::placeholder { text-overflow: ellipsis; opacity: 1; color: var(--gray-400,var(--ink-faint)); }
textarea { min-height: 96px; height: auto; line-height: 1.5; }
/* Tame native number spinners so the value/placeholder isn't crowded. */
input[type="number"] { -moz-appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { margin-left: 4px; }
/* Selects: keep room for the chevron so long options don't sit under it. */
select { padding-right: 2.6rem !important; }
/* Slightly stronger, calmer focus ring. */
.form-control:focus, input:focus, select:focus, textarea:focus {
    box-shadow: 0 0 0 3px rgba(30,58,95,.12); border-color: var(--primary,var(--ink));
}

/* --- Premium card / table / header refinements --- */
.metric-card { transition: transform var(--dur-fade) var(--ease-out-editorial),
                box-shadow var(--dur-fade) var(--ease-out-editorial),
                border-color var(--dur-fade) var(--ease-out-editorial); }
/* The hover lift used to paint the shadowed box directly on the card. With
   the editorial metrics layout (first column flush at inline-start 0) the
   value/label then sat ON the box's left edge. The hover surface is now a
   behind-content layer that extends past the text on every side, so the
   figure always has breathing room inside the box and the layout never
   shifts. */
.metric-card { position: relative; }
.metric-card::after {
    content: "";
    position: absolute;
    inset: -10px -14px;
    z-index: -1;
    border-radius: var(--radius-md);
    background: transparent;
    transition: background var(--dur-colour) var(--ease-out-editorial),
                box-shadow var(--dur-colour) var(--ease-out-editorial);
}
.metric-card:hover { transform: translateY(-2px); }
.metric-card:hover::after {
    background: var(--surface);
    box-shadow: 0 10px 24px rgba(16, 24, 40, .10);
}
.panel, .card, .chart-card { box-shadow: 0 1px 2px rgba(16,24,40,.04), 0 1px 3px rgba(16,24,40,.06); }
.panel-header, .card-header { letter-spacing: .1px; }
.data-table thead th, .table thead th {
    text-transform: uppercase; font-size: .68rem; letter-spacing: .5px;
    color: var(--gray-500,var(--ink-mute)); background: var(--gray-50,var(--paper));
    border-bottom: 1px solid var(--gray-200,var(--rule)); white-space: nowrap;
}
.data-table tbody td, .table tbody td { vertical-align: middle; }
.page-header { padding-bottom: 14px; border-bottom: 1px solid var(--gray-200,var(--surface-alt)); margin-bottom: 22px; }
.btn { transition: transform var(--dur-colour) var(--ease-out-editorial),
                box-shadow var(--dur-colour) var(--ease-out-editorial),
                background var(--dur-fade) var(--ease-out-editorial),
                filter var(--dur-fade) var(--ease-out-editorial); }
.btn:active { transform: translateY(1px); }
.badge { font-weight: 600; letter-spacing: .2px; }

/* Apply-form document uploads + country doc rule inputs */
.doc-upload-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; }
.doc-upload-grid .doc-req { display:flex; flex-direction:column; gap:6px; }
.doc-req label { font-size:.78rem; font-weight:600; color:var(--gray-600,var(--ink-soft)); }
.doc-req input[type="file"] { font-size:.85rem; padding:8px 10px; border:1px dashed var(--gray-300,var(--rule-strong));
    border-radius:10px; background:var(--gray-50,var(--paper)); width:100%; }
.doc-req input[type="file"]:hover { border-color:var(--primary,var(--ink)); }
@media (max-width:640px){ .doc-upload-grid { grid-template-columns:1fr; } }

/* =====================================================================
   Custom dialogs (saheConfirm / saheAlert) — replaces native confirm/alert
   ===================================================================== */
.sahe-dialog-overlay {
    position: fixed; inset: 0; z-index: 4000; display: none;
    align-items: center; justify-content: center;
    background: rgba(15, 23, 42, .55); backdrop-filter: blur(2px);
    padding: 20px; animation: saheFade .15s ease;
}
.sahe-dialog-overlay.open { display: flex; }
@keyframes saheFade { from { opacity: 0; } to { opacity: 1; } }
.sahe-dialog {
    background: var(--surface); border-radius: 16px; width: 100%; max-width: 420px;
    padding: 28px 26px 22px; text-align: center;
    box-shadow: 0 24px 60px rgba(16, 24, 40, .28); animation: sahePop .18s ease;
}
@keyframes sahePop { from { transform: translateY(8px) scale(.98); opacity: .6; } to { transform: none; opacity: 1; } }
.sahe-dialog-icon {
    width: 56px; height: 56px; border-radius: 50%; margin: 0 auto 14px;
    display: flex; align-items: center; justify-content: center; font-size: 24px;
    background: var(--primary-50, var(--surface-alt)); color: var(--primary, var(--ink));
}
.sahe-dialog-icon.danger  { background: var(--crimson-line); color: var(--crimson-strong); }
.sahe-dialog-icon.warning { background: var(--amber-line); color: var(--amber-hover); }
.sahe-dialog-icon.success { background: var(--teal-line); color: var(--teal-strong); }
.sahe-dialog-icon.info    { background: var(--royal-soft); color: var(--royal-strong); }
.sahe-dialog-title { font-size: 1.15rem; font-weight: 700; color: var(--gray-900, var(--ink)); margin: 0 0 6px; }
.sahe-dialog-msg { font-size: .92rem; color: var(--gray-600, var(--ink-soft)); margin: 0 0 22px; line-height: 1.5; }
.sahe-dialog-actions { display: flex; gap: 10px; justify-content: center; }
.sahe-dialog-actions .btn { min-width: 110px; justify-content: center; }

/* ===== GLOBAL PAGE LOADER ===== */
.sahe-loader-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    background: color-mix(in srgb, var(--paper) 85%, transparent);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--dur-rise) var(--ease-out-editorial),
                visibility var(--dur-rise) var(--ease-out-editorial);
}
.sahe-loader-overlay.active {
    opacity: 1;
    visibility: visible;
}
.sahe-loader-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
}
/* The route loader used to be a swinging logo, which is a spinner with a
   brand on it: it loops, it says nothing about what is arriving, and it was
   the only thing in the product competing with the intake countdown for
   movement (§11, §14).

   Removing the overlay entirely is a behavioural change and is raised as a
   proposal, so it stays -- but it now shows a shimmer bar, which is the one
   loop the system allows, and it is announced to a screen reader instead of
   being silent. */
.sahe-loader-logo {
    display: block;
    width: 200px;
    max-width: 60vw;
    height: 6px;
    border-radius: var(--radius-full);
    background: linear-gradient(90deg,
        var(--surface-alt) 0%, var(--rule) 45%, var(--surface-alt) 90%);
    background-size: 200% 100%;
    animation: shimmer 1.6s linear infinite;
}
.sahe-loader-text {
    font-size: var(--text-label);
    line-height: var(--text-label-lh);
    letter-spacing: var(--text-label-ls);
    text-transform: uppercase;
    font-weight: var(--font-semibold);
    color: var(--ink-mute);
}

/* ===== DELETE CONFIRMATION PAGES ===== */
.delete-confirm-container {
    max-width: 560px;
    margin: 0 auto;
}
.delete-confirm-card {
    background: var(--white, var(--surface));
    border: 1px solid var(--gray-200, var(--rule));
    border-radius: var(--radius-xl, 0.75rem);
    overflow: hidden;
}
.delete-confirm-danger {
    padding: var(--space-5, 1.25rem) var(--space-6, 1.5rem);
    background: var(--crimson-soft);
    border-bottom: 2px solid var(--danger, var(--crimson-strong));
    display: flex;
    align-items: center;
    gap: var(--space-4, 1rem);
}
.delete-confirm-danger-icon {
    width: 52px;
    height: 52px;
    border-radius: 50%;
    background: var(--white, var(--surface));
    color: var(--danger, var(--crimson-strong));
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.4rem;
    flex-shrink: 0;
    border: 2px solid var(--danger, var(--crimson-strong));
}
.delete-confirm-danger h3 {
    font-size: 1.1rem;
    font-weight: var(--font-semibold, 600);
    color: var(--crimson-hover);
    margin: 0 0 2px;
}
.delete-confirm-danger p {
    font-size: var(--text-sm, 0.875rem);
    color: var(--crimson-strong);
    margin: 0;
}
.delete-confirm-body {
    padding: var(--space-5, 1.25rem) var(--space-6, 1.5rem);
}
.delete-confirm-info {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-3, 0.75rem) var(--space-5, 1.25rem);
    background: var(--gray-50, var(--paper));
    border-radius: var(--radius-lg, 0.5rem);
    padding: var(--space-4, 1rem) var(--space-5, 1.25rem);
}
.delete-confirm-info.single-col {
    grid-template-columns: 1fr;
}
.delete-confirm-field {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.delete-confirm-field.span-full {
    grid-column: 1 / -1;
}
.delete-confirm-field-label {
    font-size: var(--text-xs, 0.75rem);
    color: var(--gray-500, var(--ink-mute));
    font-weight: var(--font-medium, 500);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}
.delete-confirm-field-value {
    font-size: var(--text-sm, 0.875rem);
    color: var(--gray-800, var(--ink));
    font-weight: var(--font-medium, 500);
}
.delete-confirm-warning {
    margin-top: var(--space-4, 1rem);
    padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
    background: var(--amber-soft);
    border: 1px solid var(--amber);
    border-radius: var(--radius-md, 0.375rem);
    display: flex;
    align-items: flex-start;
    gap: var(--space-3, 0.75rem);
    font-size: var(--text-sm, 0.875rem);
    color: var(--amber-hover);
    line-height: 1.5;
}
.delete-confirm-warning i {
    color: var(--amber-strong);
    margin-top: 2px;
    flex-shrink: 0;
}
.delete-confirm-actions {
    padding: var(--space-4, 1rem) var(--space-6, 1.5rem);
    background: var(--gray-50, var(--paper));
    border-top: 1px solid var(--gray-100, var(--surface-alt));
    display: flex;
    justify-content: flex-end;
    gap: var(--space-3, 0.75rem);
}
@media (max-width: 600px) {
    .delete-confirm-info { grid-template-columns: 1fr; }
    .delete-confirm-actions { flex-direction: column; }
    .delete-confirm-actions .btn { width: 100%; justify-content: center; }
}

/* ==========================================================================
   Panel notes, quiet panels, and table sub-text
   Added per UI_STANDARD.md: shared components live here so every page gets
   them, rather than each template inventing its own.
   ========================================================================== */

/* Explanatory line under a panel header. For the sentence that tells someone
   WHY a panel exists or what will happen if they act -- not for status
   messages, which belong in an .alert. */
.panel-note {
    margin: 0;
    padding: var(--space-4, 1rem) var(--space-6, 1.5rem);
    border-bottom: 1px solid var(--gray-100, var(--surface-alt));
    background: var(--gray-25, var(--paper));
    color: var(--gray-600, var(--ink-soft));
    font-size: var(--text-sm, 0.875rem);
    line-height: 1.6;
}
.panel-body > .panel-note {
    padding-left: 0;
    padding-right: 0;
    padding-top: 0;
    background: transparent;
    border-bottom: 0;
}

/* A panel for a secondary or destructive action that must be available but
   should not compete with the main content -- e.g. "withdraw my application"
   at the foot of the applicant portal. */
.panel-quiet {
    background: var(--gray-25, var(--paper));
    border-style: dashed;
}
.panel-quiet .panel-title {
    color: var(--gray-600, var(--ink-soft));
    font-weight: var(--font-medium, 500);
}

/* Secondary line inside a table cell: the detail under a primary value
   (a description under a name, "verified by X on Y" under a status). */
.cell-sub {
    margin-top: 2px;
    color: var(--gray-500, var(--ink-mute));
    font-size: var(--text-xs, 0.75rem);
    line-height: 1.5;
}
.cell-sub + .cell-sub { margin-top: 1px; }

/* ==========================================================================
   Table variants
   Four app-local table components had grown their own header/cell styling:
   .mini-table (admissions), .as-table (advance standing), .tpl-table
   (templates) and .rpm-table (role matrix). Same job, four appearances.

   They are now variants OF the canonical table rather than replacements for
   it: they inherit every header, cell, hover and border rule from
   `.table, .data-table` in design-system.css, and only declare what actually
   differs -- density.
   ========================================================================== */

.mini-table,
.as-table,
.tpl-table,
.rpm-table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-size: var(--text-sm);
}

/* Compact density: the reason these variants existed. Use `.data-table
   .compact` on new work. */
.data-table.compact th,
.mini-table th,
.as-table th,
.tpl-table th,
.rpm-table th {
    padding: var(--space-2) var(--space-3);
}

.data-table.compact td,
.mini-table td,
.as-table td,
.tpl-table td,
.rpm-table td {
    padding: var(--space-2) var(--space-3);
}

/* --------------------------------------------------------------------------
   `.table-aligned` — sibling tables that repeat the same columns

   A page that renders one panel per category (checklist_category_list,
   template_variable_list) stacks several tables with an identical header row.
   Each one auto-sizes from its OWN longest cell, so the same column edge lands
   somewhere different in every panel -- measured at 120-370px of drift down a
   single page. The eye reads that as several unrelated tables rather than one
   list broken into groups.

   `table-layout: fixed` takes the width from the header row instead of the
   content, so every sibling resolves to the same geometry. Widths are declared
   once, on the <th>s.

   INTENDED MARKUP (repeat the same <th> classes in every sibling table):

     <table class="data-table table-aligned">
       <thead>
         <tr>
           <th class="col-w-30">Item</th>
           <th>Description</th>              {# no class: takes the remainder #}
           <th class="col-w-xs text-end">Order</th>
         </tr>
       </thead>
       ...
     </table>

   Fixed layout cannot grow a column to fit an unbreakable string, so cells
   wrap on any character rather than pushing the edge back out.
   -------------------------------------------------------------------------- */
.table-aligned {
    table-layout: fixed;
    width: 100%;
}
.table-aligned td {
    overflow-wrap: anywhere;
}
/* Header labels stay on one line (they are short); it is the cells that wrap. */
.table-aligned th {
    overflow-wrap: normal;
}

/* Column widths. Scoped to `.table-aligned` because a width only holds the
   line once the table is in fixed layout -- outside it they are merely hints
   and would drift again. Put them on the <th>, not the <td>. */
.table-aligned .col-w-xs { width: 80px; }
.table-aligned .col-w-sm { width: 120px; }
.table-aligned .col-w-md { width: 180px; }
.table-aligned .col-w-lg { width: 260px; }
.table-aligned .col-w-actions { width: 150px; }
.table-aligned .col-w-20 { width: 20%; }
.table-aligned .col-w-25 { width: 25%; }
.table-aligned .col-w-30 { width: 30%; }
.table-aligned .col-w-40 { width: 40%; }
.table-aligned .col-w-50 { width: 50%; }

/* ==========================================================================
   SESSION 5 — SHARED COMPONENT COMPLETION
   --------------------------------------------------------------------------
   A full-template audit found ~60 classes that templates use but no stylesheet
   defines, plus a dozen that exist in exactly ONE per-app stylesheet and so
   render unstyled on every page that does not load that sheet. Both cases show
   up as the same symptom: text and tables falling outside their card.

   Everything shared goes here, per UI_STANDARD.md. Sections:
     A. Utilities the templates already write (gaps, flex, spacing, text)
     B. Tables — scroll containers that cannot be forgotten, density, row tints
     C. Forms — the .form-card/.form-body shell, grids, checkboxes, errors
     D. Read-only detail views
     E. Empty states
     F. Lists and feeds — timeline, chat thread, leaderboard, notifications
     G. Compact stat strips and tile grids
     H. Pagination
     I. Odds and ends that were trapped in one app stylesheet
     J. Dark mode for all of the above

   ON `:where(...)`
   Some of these components still have ONE app-local definition (agents.css,
   hr.css, admissions.css, ...). Declaring the same bare class here would give
   the component two competing definitions -- exactly what
   core/test_design_system.py exists to prevent. Those rules are therefore
   wrapped in `:where()`, which carries ZERO specificity:

     * a page that loads no app stylesheet finally gets the component, and
     * a page that DOES load it keeps precisely the rendering it has today,
       because the app-local rule still wins.

   Nothing regresses. When an app-local copy is eventually deleted, drop the
   `:where()` wrapper and the rule here becomes the single definition.
   `:is(h1,...,h6)` is used where the component's root is a heading, so the
   rule still outranks the base h1..h6 styling it needs to replace.
   ========================================================================== */

/* ==========================================================================
   A. UTILITIES
   ========================================================================== */

/* --- Gap scale. Only .gap-2 and .gap-4 existed; templates writing gap-1 and
       gap-3 silently got zero spacing and their badges collided. --- */
.gap-0 { gap: 0 !important; }
.gap-1 { gap: var(--space-1) !important; }
.gap-3 { gap: var(--space-3) !important; }
.gap-5 { gap: var(--space-5) !important; }
.gap-6 { gap: var(--space-6) !important; }
.gap-xs { gap: var(--space-1) !important; }
:where(.gap-sm) { gap: var(--space-2); }
:where(.gap-md) { gap: var(--space-4); }
:where(.gap-lg) { gap: var(--space-6); }

/* --- Display + flex direction --- */
.d-grid { display: grid !important; }
.d-inline-flex { display: inline-flex !important; }
.flex-column { flex-direction: column !important; }
.flex-row { flex-direction: row !important; }
.flex-1 { flex: 1 1 auto !important; min-width: 0; }
.flex-shrink-0 { flex-shrink: 0 !important; }
:where(.flex) { display: flex; }

/* --- Cross-axis alignment. .d-flex, .row and .gap-* existed but none of the
       align-items utilities did, so `align-items-end` did nothing. --- */
.align-items-start { align-items: flex-start !important; }
.align-items-end { align-items: flex-end !important; }
.align-items-stretch { align-items: stretch !important; }
.align-items-baseline { align-items: baseline !important; }
.align-self-center { align-self: center !important; }
.align-self-end { align-self: flex-end !important; }
.justify-content-start { justify-content: flex-start !important; }
.justify-content-center { justify-content: center !important; }
.justify-content-end { justify-content: flex-end !important; }
.justify-content-around { justify-content: space-around !important; }
:where(.items-center) { align-items: center; }
:where(.items-start) { align-items: flex-start; }
:where(.justify-between) { justify-content: space-between; }
:where(.justify-center) { justify-content: center; }

/* --- Padding steps. Only .p-* / .py-* existed; pt-* / pb-* / px-* did not, so
       a `pt-3` next to a border-top produced a rule with no breathing room. --- */
.pt-1 { padding-top: 0.25rem !important; }
.pt-2 { padding-top: 0.5rem !important; }
.pt-3 { padding-top: 1rem !important; }
.pt-4 { padding-top: 1.5rem !important; }
.pb-1 { padding-bottom: 0.25rem !important; }
.pb-2 { padding-bottom: 0.5rem !important; }
.pb-3 { padding-bottom: 1rem !important; }
.pb-4 { padding-bottom: 1.5rem !important; }
.px-2 { padding-left: 0.5rem !important; padding-right: 0.5rem !important; }
.px-3 { padding-left: 1rem !important; padding-right: 1rem !important; }
.px-4 { padding-left: 1.5rem !important; padding-right: 1.5rem !important; }
.py-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; }
.mt-5 { margin-top: 1.5rem !important; }
.mt-6 { margin-top: var(--space-6) !important; }
.mb-5 { margin-bottom: 1.5rem !important; }
.mb-lg { margin-bottom: var(--space-6) !important; }
.me-4 { margin-right: 1.5rem !important; }
.ms-3 { margin-left: 1rem !important; }

/* --- Text --- */
.text-start { text-align: left !important; }
.text-nowrap { white-space: nowrap !important; }
.text-break { overflow-wrap: anywhere !important; }
.text-secondary { color: var(--gray-600) !important; }
/* One step down from the surrounding text. Used on captions and meta lines,
   as `.form-label small`, and as a size modifier on .empty-state / .progress. */
:where(.small) { font-size: var(--text-xs); }

/* --- Lists that must not show browser bullets --- */
.list-unstyled { list-style: none; padding-left: 0; margin: 0; }
:where(.plain-list) { list-style: none; margin: 0; padding: 0; }

/* --- Full-width and stacked buttons --- */
:where(.btn-block) { width: 100%; }
.btn.btn-block { justify-content: center; }
/* The "three full-width actions in a sidebar card" pattern several pages want
   and currently fake with an undefined .d-grid. Handles the <form> wrapper a
   POST action needs, so all three controls really do line up. */
.btn-stack { display: flex; flex-direction: column; gap: var(--space-2); }
.btn-stack > form { margin: 0; }
.btn-stack > .btn,
.btn-stack > form > .btn,
.btn-stack > form > button { width: 100%; justify-content: center; }

/* --- Generic hairline separator. Also aliases the vertical rule used inside
       a .quick-stats-bar, which three templates spell `.divider`. --- */
.divider { height: 1px; background: var(--gray-200); border: 0; margin: var(--space-4) 0; }
.quick-stats-bar > .divider,
.summary-bar > .divider,
.filter-bar > .divider { width: 1px; height: 32px; margin: 0; flex: none; }

/* ==========================================================================
   B. TABLES
   ========================================================================== */

/* `.table-scroll` is the name application_detail.html and the applicant portal
   already use. `.table-responsive` stays canonical -- this makes both work. */
:where(.table-scroll) { width: 100%; max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* A scroll wrapper nested inside a card must not draw a second hairline
   immediately inside the card border -- the standard .panel > .table-responsive
   nesting was showing a double rule on every list page. */
.panel > .table-responsive, .panel-body > .table-responsive,
.card > .table-responsive, .card-body > .table-responsive,
.content-card > .table-responsive, .content-card-body > .table-responsive,
.panel > .table-container, .panel-body > .table-container,
.card > .table-container, .card-body > .table-container,
.panel > .table-wrapper, .panel-body > .table-wrapper,
.card > .table-wrapper, .card-body > .table-wrapper {
    border: 0;
    background: transparent;
    border-radius: 0;
}

/* SAFETY NET -- a forgotten wrapper must degrade to a scrollbar, never to a
   clipped column. `.panel` / `.card` are `overflow: hidden`, so a table dropped
   straight into a card had its right-hand columns (usually Actions) cut off
   with no way to reach them. Making the card body the scroll container fixes
   that without touching the table's own layout, so a table narrower than its
   card still fills the width exactly as before. */
.panel:has(> table),
.card:has(> table),
.content-card:has(> table),
.panel-body:has(> table),
.card-body:has(> table),
.content-card-body:has(> table),
.panel-body:has(> form > table),
.card-body:has(> form > table) {
    overflow-x: auto;
}

/* Density plus the Bootstrap table modifiers eight templates already carry.
   `.data-table.compact` remains the name to use for new work. */
.table-sm th, .table-sm td { padding: var(--space-2) var(--space-3); }
.table-borderless th, .table-borderless td,
.data-table.borderless th, .data-table.borderless td {
    border: 0;
    background: transparent;
}
.table-bordered th, .table-bordered td { border: 1px solid var(--gray-200); }
.table-light th, thead.table-light th { background: var(--gray-50); color: var(--gray-500); }
.align-middle, .align-middle th, .align-middle td { vertical-align: middle !important; }

/* Row emphasis. Previously done with an inline <tr style="background:...">,
   which also killed the row hover. The rgba tints sit correctly on the dark
   surface too, so there is no separate dark-mode rule to keep in sync. */
.data-table tr.is-total > td,
.data-table tr.table-primary > td,
.data-table tfoot tr.is-total > td { background: rgba(var(--primary-rgb), 0.07); font-weight: var(--font-semibold); color: var(--gray-900); }
.data-table tr.row-success > td, .data-table tr.table-success > td { background: rgba(var(--success-rgb), 0.08); }
.data-table tr.row-warning > td, .data-table tr.table-warning > td { background: rgba(var(--warning-rgb), 0.10); }
.data-table tr.row-danger > td,  .data-table tr.table-danger > td  { background: rgba(var(--danger-rgb), 0.07); }
.data-table tbody tr.row-success:hover > td, .data-table tbody tr.table-success:hover > td { background: rgba(var(--success-rgb), 0.16); }
.data-table tbody tr.row-warning:hover > td, .data-table tbody tr.table-warning:hover > td { background: rgba(var(--warning-rgb), 0.20); }
.data-table tbody tr.row-danger:hover  > td, .data-table tbody tr.table-danger:hover  > td { background: rgba(var(--danger-rgb), 0.15); }

/* .pagination-bar was a third pager. The one pager (design-system.css,
   `.pager`) keeps its good idea — a pager belongs to the panel, not to the
   scroll container, or it scrolls sideways with the table — and drops the
   sunken fill, which made it read as a footer rather than as controls. */

/* ==========================================================================
   C. FORMS
   ========================================================================== */

/* The shell UI_STANDARD.md names -- .form-container > .form-card > form >
   .form-body > .form-section -- existed only in three app stylesheets, so a
   form page that linked none of them rendered with no card at all. */
:where(.form-card) {
    background: var(--white);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-xl);
    overflow: hidden;
    box-shadow: var(--shadow-xs);
}
:where(.form-body) { padding: var(--space-6); }

/* A padded header strip inside .form-card (title + optional subtitle). */
.form-header {
    padding: var(--space-4) var(--space-6);
    border-bottom: 1px solid var(--gray-100);
    background: var(--white);
}
.form-header > h1, .form-header > h2, .form-header > h3, .form-header > h4 {
    margin: 0;
    font-size: var(--text-base);
    font-weight: var(--font-semibold);
    color: var(--gray-900);
}
.form-header > p, .form-header .form-subtitle {
    margin: var(--space-1) 0 0;
    font-size: var(--text-sm);
    color: var(--gray-500);
}

/* Safety gutter: a form that forgot its .form-body still gets side padding
   instead of putting its labels flush against the card border. */
.form-card > form > .form-section,
.form-card > form > .form-grid,
.form-card > form > .alert,
.form-card > form > .error-summary {
    padding-left: var(--space-6);
    padding-right: var(--space-6);
}
.form-card > form > .form-section:first-of-type,
.form-card > form > .form-grid:first-of-type { padding-top: var(--space-6); }

/* Sanctioned column counts. .form-grid is a 2-column grid declared with
   !important, so pages wanting three columns were overriding it in a page
   <style> block and silently breaking their own .span-2 fields. */
.form-grid.cols-1 { grid-template-columns: minmax(0, 1fr) !important; }
.form-grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; }
.form-grid.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; }
.form-grid .form-group.span-2, .form-grid > .span-2 { grid-column: span 2; }
.form-grid .form-group.span-full, .form-grid > .span-full { grid-column: 1 / -1; }
@media (max-width: 900px) {
    .form-grid.cols-3, .form-grid.cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
}
@media (max-width: 640px) {
    .form-grid.cols-3, .form-grid.cols-4 { grid-template-columns: 1fr !important; }
    .form-grid .form-group.span-2, .form-grid > .span-2 { grid-column: 1 / -1; }
}

/* Self-collapsing field grids, for the ~15 places that hand-rolled
   `style="display:grid; grid-template-columns:1fr 1fr"` with no breakpoint. */
.form-grid-2, .form-grid-3 {
    display: grid;
    gap: 18px 22px;
    align-items: start;
}
.form-grid-2 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.form-grid-3 { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }

/* Split footer (Back ... Next). Wizards were using .btn-group for this, which
   is a segmented control and welded the two buttons into one pill. */
.form-actions-split, .wizard-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-top: var(--space-6);
    padding-top: var(--space-4);
    border-top: 1px solid var(--gray-200);
}
.form-actions.split { justify-content: space-between; }

/* --- Checkbox / radio rows. One component, several historical spellings. --- */
:where(.form-checkbox) {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    cursor: pointer;
}
.form-check-inline {
    display: inline-flex !important;
    flex-direction: row !important;
    align-items: center;
    gap: var(--space-2);
    margin-right: var(--space-4);
}
.checkbox-option, .checkbox-label {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin: 0;
    cursor: pointer;
}
.checkbox-text { font-size: var(--text-sm); color: var(--gray-700); line-height: 1.5; }
/* These labels are option text, not field captions -- they must escape the
   uppercase micro-caption treatment `.form-group > label` applies. */
.form-checkbox label,
.form-check-inline label,
.checkbox-option, .checkbox-option > span,
.checkbox-label, .checkbox-label > span {
    font-size: var(--text-sm);
    font-weight: var(--font-normal);
    color: var(--gray-700);
    text-transform: none;
    letter-spacing: 0;
}

/* --- Validation messages --- */
/* Django renders field errors as <ul class="errorlist">, which no stylesheet
   touched, so every validation message came out as an indented browser bullet
   hanging off to the right of its field. */
ul.errorlist, .form-error ul.errorlist {
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: var(--text-xs);
    color: var(--danger);
}
ul.errorlist li { margin: 0; line-height: 1.5; }

:where(.error-summary) {
    background: var(--danger-light);
    border: 1px solid var(--danger);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    margin-bottom: var(--space-6);
}
:is(h1, h2, h3, h4, h5, h6, div, p):where(.error-summary-title) {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
    font-weight: var(--font-semibold);
    color: var(--danger-dark);
    margin: 0 0 var(--space-2);
}
:where(.error-summary-list) {
    margin: 0;
    padding-left: var(--space-5);
    color: var(--danger-dark);
    font-size: var(--text-sm);
}

/* --- Inline help + read-only field value. `.form-hint` stays canonical; these
       are the other spellings already in the templates. --- */
:is(p, div, span, small):where(.form-help) {
    display: block;
    margin-top: var(--space-1);
    font-size: var(--text-xs);
    color: var(--gray-500);
}
.help-text {
    display: block;
    margin-top: var(--space-1);
    font-size: var(--text-xs);
    color: var(--gray-500);
}
.card-text { font-size: var(--text-sm); color: var(--gray-600); margin: 0 0 var(--space-2); }
.card-text:last-child { margin-bottom: 0; }
.form-control-static {
    display: block;
    min-height: 42px;
    padding: 9px 0;
    font-size: var(--text-sm);
    color: var(--gray-800);
    line-height: 1.5;
}

/* The search field in a filter bar should take up the slack. */
.filter-group.flex-grow, .filter-group.flex-grow-1 { flex: 1 1 auto; min-width: 180px; }

/* ==========================================================================
   D. READ-ONLY DETAIL VIEWS
   This exact block was copy-pasted verbatim into seven academics page <style>
   tags and hand-rebuilt again in admissions, agents and advance standing. One
   definition, so the detail pages cannot drift apart again.
   ========================================================================== */
.detail-card {
    background: var(--white);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-xl);
    overflow: hidden;
    margin-bottom: var(--space-6);
    box-shadow: var(--shadow-xs);
}
.detail-body { padding: var(--space-6); }
:where(.detail-section) { margin-bottom: var(--space-6); }
.detail-section:last-child { margin-bottom: 0; }
/* A section heading inside a detail page is the Section pattern (§8.5) one
   level down: the 10px caps label, then a rule that FADES to transparent on
   the right. The fading rule is what makes a section start feel editorial
   rather than administrative — a full-width border under a heading draws a
   box lid, which is exactly what this language avoids. */
:is(h1, h2, h3, h4, h5, h6, div):where(.detail-section-title) {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin: 0 0 var(--space-4);
    font-size: var(--text-label);
    line-height: var(--text-label-lh);
    letter-spacing: var(--text-label-ls);
    text-transform: uppercase;
    font-weight: var(--font-semibold);
    color: var(--ink);
}
.detail-section-title::after {
    content: '';
    flex: 1;
    min-width: 24px;
    height: 1px;
    background: linear-gradient(90deg, var(--rule) 0%, transparent 100%);
}
.detail-section-title i { color: var(--ink-mute); font-size: 12px; }
:where(.detail-grid) {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-4);
}
.detail-grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.detail-field { min-width: 0; }
:is(div, span, dt, label):where(.detail-label) {
    display: block;
    font-size: var(--text-xs);
    font-weight: var(--font-medium);
    color: var(--gray-500);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: var(--space-1);
}
:is(div, span, dd, p):where(.detail-value) {
    font-size: var(--text-sm);
    color: var(--gray-800);
    overflow-wrap: anywhere;
}
.detail-value.empty, .detail-value.is-empty { color: var(--gray-400); font-style: italic; }
/* Full-width cell in a .detail-grid or .info-grid, currently written inline as
   `style="grid-column: 1 / -1"` on several detail pages. */
.span-full,
.detail-grid .span-full, .info-grid .span-full,
.info-item.span-full, .detail-field.span-full { grid-column: 1 / -1; }
@media (max-width: 768px) {
    .detail-grid, .detail-grid.cols-3 { grid-template-columns: 1fr; }
}

/* The read-only sibling of .success-header / .warning-header. */
.info-header { border-bottom-color: var(--info-light); }
.info-header .panel-title, .info-header .card-title { color: var(--info-dark); }

/* Whichever ratio a two-column detail page wants, said out loud instead of
   inherited from whichever app stylesheet happened to load. */
.two-column-layout.main-first, .two-column-layout.sidebar-right { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
.two-column-layout.side-first, .two-column-layout.sidebar-left  { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
/* ...and it must always collapse on a narrow screen, even where a page pinned
   the ratio with an inline style (specificity 1000 beats a media query). */
@media (max-width: 992px) {
    .two-column-layout,
    .two-column-layout.main-first, .two-column-layout.sidebar-right,
    .two-column-layout.side-first, .two-column-layout.sidebar-left {
        grid-template-columns: 1fr !important;
    }
}

/* ==========================================================================
   E. EMPTY STATES
   ========================================================================== */
/* Panel-level empty state: icon chip + heading + body, centred and muted. */
:where(.empty-panel) { text-align: center; padding: var(--space-8) var(--space-4); }
.empty-panel .empty-icon,
:where(.empty-state-icon) {
    display: block;
    width: 120px;
    height: 84px;
    margin: 0 auto var(--space-4);
    background: none;
    border-radius: 0;
    color: var(--ink-faint);
}
.empty-panel .empty-icon i, .empty-state-icon i {
    display: block; line-height: 84px; font-size: 34px; font-weight: 300;
    color: inherit;
}
.empty-panel .empty-icon.success { color: var(--teal); }
.empty-panel h3 {
    margin: 0;
    font-family: var(--font-serif);
    font-size: var(--text-h2);
    line-height: var(--text-h2-lh);
    letter-spacing: var(--text-h2-ls);
    font-weight: var(--font-medium);
    color: var(--ink);
}
.empty-panel p {
    max-width: 46ch;
    margin: var(--space-2) auto 0;
    color: var(--ink-mute);
    font-size: var(--text-sm);
}

/* The element-level spellings. `.empty-title` / `.empty-description` stay
   canonical; these keep the pages already using them from rendering as plain
   body text. */
:is(h1, h2, h3, h4, h5, h6, div):where(.empty-state-title) {
    margin: 0 0 var(--space-2);
    font-size: var(--text-lg);
    font-weight: var(--font-semibold);
    color: var(--gray-600);
}
:is(p, div, span):where(.empty-state-text) {
    margin: 0 0 var(--space-4);
    color: var(--gray-500);
    font-size: var(--text-sm);
}
.empty-state-text:last-child { margin-bottom: 0; }
/* An .empty-state that wraps its icon in a div instead of using a bare <i>
   still gets a large muted glyph. */
.empty-state > .empty-state-icon > i { font-size: 28px; }
/* Compact variant, for an empty state inside a half-width panel. */
.empty-state.small { padding: var(--space-6) var(--space-4); }
.empty-state.small > i { font-size: 28px; margin-bottom: 10px; }
.empty-state.small h3 { font-size: var(--text-base); }

/* ==========================================================================
   F. LISTS AND FEEDS
   ========================================================================== */

/* --- Vertical activity feed. Lived only in admissions.css, so the students
       portal rendered its timeline as a raw bulleted <ul> on three pages. --- */
.timeline {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}
:where(.timeline-item) {
    display: flex;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-lg);
    background: var(--gray-25);
    border: 1px solid var(--gray-100);
}
.timeline-marker {
    width: 34px;
    height: 34px;
    flex: none;
    border-radius: var(--radius-full);
    background: var(--primary-50);
    color: var(--primary);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-sm);
}
.timeline-item.timeline-success .timeline-marker { background: var(--success-light); color: var(--success); }
.timeline-item.timeline-warning .timeline-marker { background: var(--warning-light); color: var(--warning-dark); }
.timeline-item.timeline-danger .timeline-marker  { background: var(--danger-light);  color: var(--danger); }
.timeline-content { flex: 1; min-width: 0; }
.timeline-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
}
.timeline-title { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--gray-900); }
.timeline-detail { margin-top: 2px; font-size: var(--text-sm); color: var(--gray-600); overflow-wrap: anywhere; }
.timeline-actor { margin-top: var(--space-2); display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/* --- Message thread. Four student templates hand-rolled this inline with four
       different hard-coded palettes. --- */
.chat-thread { display: flex; flex-direction: column; gap: var(--space-4); }
.chat-message { display: flex; flex-direction: column; gap: 4px; max-width: 80%; align-self: flex-start; }
.chat-message.chat-mine, .chat-message--student { align-self: flex-end; align-items: flex-end; }
.chat-meta, .chat-time { font-size: var(--text-xs); color: var(--gray-500); }
.chat-bubble {
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-lg);
    background: var(--gray-100);
    color: var(--gray-800);
    font-size: var(--text-sm);
    line-height: 1.6;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    text-align: left;
}
.chat-bubble--staff { background: var(--primary-50); border-top-left-radius: 2px; }
.chat-bubble--student, .chat-message.chat-mine .chat-bubble { background: var(--success-light); border-top-right-radius: 2px; }
.chat-bubble--internal { background: var(--warning-light); border: 1px dashed var(--warning); }

/* --- Ranked list (leaderboards). Used but defined nowhere, so both boards on
       the agent reports page rendered as flush, unstyled block text. --- */
.leaderboard-list { display: flex; flex-direction: column; }
.leaderboard-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-5);
    border-bottom: 1px solid var(--gray-100);
}
.leaderboard-item:last-child { border-bottom: none; }
.leaderboard-rank {
    width: 26px;
    height: 26px;
    flex: none;
    border-radius: var(--radius-full);
    background: var(--primary-50);
    color: var(--primary);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-xs);
    font-weight: var(--font-bold);
}
.leaderboard-item:first-child .leaderboard-rank { background: var(--primary); color: var(--white); }
.leaderboard-info { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.leaderboard-name { font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--gray-900); }
.leaderboard-meta { font-size: var(--text-xs); color: var(--gray-500); }
.leaderboard-value { font-size: var(--text-sm); font-weight: var(--font-bold); color: var(--gray-900); font-variant-numeric: tabular-nums; }
.leaderboard-value.success { color: var(--success); }
.leaderboard-value.warning { color: var(--warning-dark); }
.leaderboard-value.danger  { color: var(--danger); }

/* --- Icon + title + meta list row (notifications, bulk imports). --- */
.notification-list, .bulk-list { list-style: none; margin: 0; padding: 0; }
.notification-row, .bulk-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-5);
    border-bottom: 1px solid var(--gray-100);
}
.notification-row:last-child, .bulk-row:last-child { border-bottom: none; }
.notification-row.is-unread { background: var(--primary-50); }
.notification-icon, .bulk-icon {
    width: 36px;
    height: 36px;
    flex: none;
    border-radius: var(--radius-full);
    background: var(--gray-100);
    color: var(--gray-500);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-sm);
}
.notification-icon.primary, .bulk-icon.primary { background: var(--primary-50); color: var(--primary); }
.notification-icon.success, .bulk-icon.success { background: var(--success-light); color: var(--success); }
.notification-icon.warning, .bulk-icon.warning { background: var(--warning-light); color: var(--warning-dark); }
.notification-icon.danger,  .bulk-icon.danger  { background: var(--danger-light);  color: var(--danger); }
.notification-body, .bulk-body { flex: 1; min-width: 0; }
.notification-title, .bulk-title { font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--gray-900); }
.notification-meta, .bulk-meta { margin-top: 2px; font-size: var(--text-xs); color: var(--gray-500); }

/* --- Bulletless, hairline-separated help / definition list. --- */
.help-list { list-style: none; margin: 0; padding: 0; }
.help-list > li {
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--gray-100);
    font-size: var(--text-sm);
    color: var(--gray-700);
    line-height: 1.6;
}
.help-list > li:last-child { border-bottom: 0; }
.help-list > li:first-child { padding-top: 0; }
.help-list strong i { color: var(--primary); margin-right: var(--space-2); }

/* ==========================================================================
   G. COMPACT STAT STRIPS AND TILE GRIDS
   ========================================================================== */

/* Dense alternative to .metrics-grid, for a strip of six small counts. */
:where(.quick-stats-bar) {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: var(--space-5);
    padding: var(--space-4) var(--space-6);
    margin-bottom: var(--space-5);
    background: var(--white);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
}
:where(.quick-stat) { display: flex; flex-direction: column; align-items: center; gap: 2px; flex: 1; min-width: 60px; }
:where(.quick-stat-value) { font-size: var(--text-2xl); font-weight: var(--font-bold); color: var(--gray-900); line-height: 1; }
:where(.quick-stat-label) {
    font-size: 0.6875rem;
    font-weight: var(--font-medium);
    color: var(--gray-500);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
:where(.quick-stat-divider) { width: 1px; height: 40px; flex: none; background: var(--gray-200); }
.quick-stat.warning .quick-stat-value { color: var(--warning-dark); }
.quick-stat.info    .quick-stat-value { color: var(--primary); }
.quick-stat.success .quick-stat-value { color: var(--success); }
.quick-stat.danger  .quick-stat-value { color: var(--danger); }

/* Inline "Critical: 3 / Warnings: 12 / Clean: 40" strip -- the compact sibling
   of .metrics-grid, previously hand-built with inline flex on two pages. */
.summary-bar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-5);
    margin-bottom: var(--space-5);
    background: var(--white);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    font-size: var(--text-sm);
    color: var(--gray-700);
}
.summary-bar-actions { margin-left: auto; display: flex; gap: var(--space-2); align-items: center; }

/* Label + value pair grid (a 4-up stat row inside a panel). */
:where(.perf-grid) {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: var(--space-3) var(--space-5);
    margin-bottom: var(--space-4);
}
:where(.perf-item) { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
:where(.perf-label) {
    font-size: var(--text-xs);
    font-weight: var(--font-bold);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--gray-500);
}
:is(div, span, strong):where(.perf-value) { font-size: var(--text-base); font-weight: var(--font-semibold); color: var(--gray-900); }

/* Headline figure + status pill (commission owing, balance due). */
.stat-highlight, .commission-highlight {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    flex-wrap: wrap;
}
.stat-highlight-value {
    font-size: var(--text-2xl);
    font-weight: var(--font-bold);
    color: var(--gray-900);
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
}
.commission-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 10px;
    border-radius: var(--radius-full);
    background: var(--success-light);
    color: var(--success-dark);
    font-size: var(--text-xs);
    font-weight: var(--font-semibold);
}

/* Responsive auto-fit card grid, for contact tiles and similar. */
.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: var(--space-4);
}
/* Two-up panel grid for report insights. */
.insights-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: var(--space-4);
    align-items: start;
}
/* Panels used as tiles inside a grid must drop their stacking margin, or every
   tile carries 24px of dead space on top of the grid gap. */
.panel-grid, .tile-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: var(--space-4);
    margin-bottom: var(--space-6);
}
.panel-grid > .panel, .panel-grid > .card, .panel-grid > .content-card,
.tile-grid > .panel, .tile-grid > .card,
.metrics-grid > .panel, .metrics-grid > .card,
.insights-grid > .panel, .insights-grid > .card,
.card-grid > .panel, .card-grid > .card { margin-bottom: 0; }

/* The circular icon chip .stat-card markup expects; dashboard.css defined the
   body and the title but never the icon. */
.stat-card-icon {
    width: 44px;
    height: 44px;
    flex: none;
    border-radius: var(--radius-full);
    background: var(--primary-50);
    color: var(--primary);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-lg);
    margin: var(--space-5) 0 0 var(--space-5);
}
.stat-card-icon.success { background: var(--success-light); color: var(--success); }
.stat-card-icon.warning { background: var(--warning-light); color: var(--warning-dark); }
.stat-card-icon.danger  { background: var(--danger-light);  color: var(--danger); }

/* Colour variants for the big number on a report tile. The modifiers were
   already in the markup; only the rules were missing, so all six numbers
   rendered in the same colour and the pass/fail coding was invisible. */
.performance-value.success, .performance-value.text-success { color: var(--success); }
.performance-value.danger,  .performance-value.text-danger  { color: var(--danger); }
.performance-value.warning, .performance-value.text-warning { color: var(--warning-dark); }
.performance-value.info,    .performance-value.primary      { color: var(--primary); }

/* Grid fallback for the hand-built stat strips still spelled `.stats-grid`. */
:where(.stats-grid) {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: var(--space-4);
    margin-bottom: var(--space-5);
}

/* Linear progress bar. Three HR pages built one from inline-styled nested
   divs. `.progress-fill` already ships in dashboard.css, which base.html loads
   globally, so only the track was missing. */
.progress-track {
    height: 8px;
    width: 100%;
    background: var(--gray-100);
    border-radius: var(--radius-full);
    overflow: hidden;
}
.progress-track.small { height: 5px; }
.progress-track.large { height: 12px; }
/* Consumes `--pct` so a template can drive the width from data:
   <div class="progress-track"><span style="--pct:62%"></span></div> */
.progress-track > .progress-fill,
.progress-track > span { display: block; height: 100%; width: var(--pct, 0%); }

/* ==========================================================================
   H. PAGINATION
   ==========================================================================
   A FIFTH pager lived here: .pagination-container / .pagination-link /
   .pagination-info. It is gone. There is one pager (design-system.css,
   `.pager`), rendered by {% pager page_obj noun="..." %}, and
   core/test_design_system.OnePagerTests keeps it that way — a second
   implementation is how the product ended up with a "Page 1 of 7" caption
   that rendered as a disabled button and no way to jump to page 5.
   ========================================================================== */

/* ==========================================================================
   I. COMPONENTS THAT WERE TRAPPED IN ONE APP STYLESHEET
   ========================================================================== */

/* Flash-message box. Independently duplicated in three app sheets and missing
   from a fourth, so HR rendered its messages as bare unstyled text. */
:where(.alert-box) {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-4);
    margin-bottom: var(--space-5);
    border-radius: var(--radius-lg);
    background: var(--gray-100);
    border: 1px solid var(--gray-200);
    color: var(--gray-700);
    font-size: var(--text-sm);
    line-height: 1.6;
}
.alert-box > i { font-size: var(--text-lg); margin-top: 2px; flex: none; }
.alert-box strong { display: block; font-weight: var(--font-semibold); margin-bottom: var(--space-1); }
.alert-box p { margin: 0; font-size: var(--text-sm); color: inherit; }
.alert-box.danger  { background: rgba(var(--danger-rgb), 0.08);  border-color: rgba(var(--danger-rgb), 0.28);  color: var(--danger-dark); }
.alert-box.warning { background: rgba(var(--warning-rgb), 0.10); border-color: rgba(var(--warning-rgb), 0.30); color: var(--warning-dark); }
.alert-box.success { background: rgba(var(--success-rgb), 0.08); border-color: rgba(var(--success-rgb), 0.28); color: var(--success-dark); }
.alert-box.info    { background: rgba(var(--primary-rgb), 0.06); border-color: rgba(var(--primary-rgb), 0.20); color: var(--info-dark); }
.alert-box.danger > i  { color: var(--danger); }
.alert-box.warning > i { color: var(--warning); }
.alert-box.success > i { color: var(--success); }

/* Helper box on the auth screens (the password-reset instructions). */
/* An alert by another name, so it takes the alert construction: tint +
   tint-border + strong text, at control radius (§9.2). */
.auth-info-box {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: 10px var(--space-3);
    margin-bottom: var(--space-4);
    border: 1px solid var(--royal-line);
    border-radius: var(--radius-md);
    background: var(--royal-soft);
    color: var(--royal-strong);
    font-size: var(--text-sm);
    line-height: 1.55;
}
.auth-info-box i { margin-top: 3px; flex: none; }
.auth-info-box p { margin: 0; color: inherit; font-size: var(--text-sm); }

/* Neutral badge. `.badge-secondary` stays canonical; this is the "off"
   spelling already in the templates. */
.badge-muted { background: var(--gray-100); color: var(--gray-500); }

/* Counter chip beside a panel title. `.panel-count` stays canonical. */
:where(.panel-badge) {
    background: var(--primary-50);
    color: var(--primary);
    font-size: var(--text-xs);
    font-weight: var(--font-semibold);
    padding: 4px 10px;
    border-radius: var(--radius-full);
}
.panel-badge.warning { background: var(--warning-light); color: var(--warning-dark); }
.panel-badge.success { background: var(--success-light); color: var(--success-dark); }

/* Smaller heading for a section inside a panel body. */
:is(h1, h2, h3, h4, h5, h6, div):where(.subsection-title) {
    font-size: var(--text-sm);
    font-weight: var(--font-bold);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--gray-500);
    margin: var(--space-5) 0 var(--space-2);
}

/* Rule + heading between sections of a long form. */
:where(.section-divider) {
    border-top: 1px solid var(--gray-200);
    margin: var(--space-5) 0;
    padding-top: var(--space-4);
}
.section-divider h3, .section-divider h4 {
    font-size: var(--text-sm);
    font-weight: var(--font-semibold);
    color: var(--primary);
    margin-bottom: var(--space-3);
}

/* Wizard footer used by the apply flow. */
:where(.step-actions) {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-top: var(--space-6);
    padding-top: var(--space-4);
    border-top: 1px solid var(--gray-100);
}

/* CLI snippets must scroll and wrap, not be clipped by the card's
   `overflow: hidden`. */
.code-block {
    display: block;
    overflow-x: auto;
    white-space: pre-wrap;
    word-break: break-word;
    padding: var(--space-4);
    border-radius: var(--radius-lg);
    background: var(--gray-50);
    border: 1px solid var(--gray-200);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 1.7;
    color: var(--gray-800);
}
/* Inline code chip -- currently five inline style properties on every <code>. */
.code-pill {
    display: inline-block;
    padding: 1px 7px;
    border-radius: var(--radius-sm);
    background: var(--gray-100);
    border: 1px solid var(--gray-200);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--gray-700);
    white-space: nowrap;
}

/* Banner announcing that a page mirrors an external platform. */
.ext-banner {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--space-5);
    border-radius: var(--radius-lg);
    background: var(--info-light);
    border: 1px solid var(--primary-100);
    color: var(--info-dark);
    font-size: var(--text-sm);
}
.ext-banner > i { flex: none; }
.ext-banner .btn { flex: none; }
.eb-text { flex: 1; min-width: 0; }

/* Chart cards stack like panels. Without this every chart card sat flush
   against the panel below it at zero gap. */
.chart-card { margin-bottom: var(--space-6); }


/* ------------------------------------------------------------------
   Form pages read as ONE column.

   .page-header spans the full content width and carries a bottom rule;
   .form-container is a centred 920px (500px when .narrow). On a wide
   screen the rule therefore ran a few hundred pixels past the card on
   both sides, so the form read as marooned in empty space rather than as
   the page's content -- the single most visible composition fault in the
   app.

   Constraining the header to the form's own measure makes the rule stop
   where the content stops, so the eye reads header -> form as a
   continuous column. :has() lets this apply from the design system
   without editing the 30 templates that use the pattern.
   ------------------------------------------------------------------ */
.page-header:has(~ .form-container) {
    max-width: 920px;
    margin-inline: auto;
}

.page-header:has(~ .form-container.narrow) {
    max-width: 500px;
}


/* ------------------------------------------------------------------
   Composition fixes from the full-app visual review.
   ------------------------------------------------------------------ */

/* Empty tables rendered their column headers over the void: {% empty %} puts
   the empty state in a <td> INSIDE the table, so <thead> always painted.
   Hiding it lets the empty state read as intentional rather than as a table
   that failed to load. Fixes ~104 templates without touching one of them. */
.data-table:has(.empty-state) thead,
.table:has(.empty-state) thead { display: none; }

/* ...and the empty row should not light up on hover as though it were data. */
.data-table tbody tr:has(.empty-state):hover,
.table tbody tr:has(.empty-state):hover { background: transparent; }

/* A 1- or 2-tile metrics row inflated each card to ~560px of mostly empty
   surface because the grid always laid out 4 equal tracks. Cap the track and
   pack left when the row is sparse; 4- and 6-tile dashboards are untouched. */
.metrics-grid:has(> :last-child:nth-child(-n+2)) {
    grid-template-columns: repeat(auto-fit, minmax(220px, 300px));
    justify-content: start;
}

/* Row-level delete was a saturated filled red button repeated on every row --
   62 rows on subject_list, 60+ on timetable_list, reading as an unbroken red
   rail that out-shouted the data. Quiet by default inside tables, full danger
   fill on hover/focus. Confirmation screens keep the filled treatment, which
   is where saturated red belongs. */
.data-table .btn-delete,
.table .btn-delete {
    background: transparent;
    color: var(--danger, var(--crimson-strong));
    border: 1px solid color-mix(in srgb, var(--danger, var(--crimson-strong)) 32%, var(--gray-200));
}

.data-table .btn-delete:hover,
.data-table .btn-delete:focus-visible,
.table .btn-delete:hover,
.table .btn-delete:focus-visible {
    background: var(--danger, var(--crimson-strong));
    color: #fff;
    border-color: var(--danger, var(--crimson-strong));
}
