/**
 * Hub Bundle - Concatenated CSS
 * Generated: auto
 */


/* ========== shared/base.css ========== */

/* The Hub - Main Styles */
/* Black & Notre Dame Gold Color Scheme */

:root {
    --primary-color: #C4B454; /* Notre Dame Gold */
    --primary-dark: var(--gold-press); /* old dark nd-gold retired to the pressed gold */
    --secondary-color: #000000; /* Black */
    --secondary-light: #2a2a2a;
    --success-color: #28a745;
    --danger-color: #dc3545;
    --warning-color: #ffc107;
    --light-bg: #f8f9fa;
    --border-color: #dee2e6;
    --text-color: #212529;
    --shadow: 0 2px 4px rgba(0,0,0,var(--shadow-light, 0.1));
    --gold-gradient: linear-gradient(135deg, var(--gold) 0%, var(--gold-press) 100%); /* old light nd-gold endpoint retired to the pressed gold */
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html, body {
    overflow-x: hidden;
    width: 100%;
}

/* Bootstrap 5.3 ships ":root { scroll-behavior: smooth }". That is
   un-designed behavior here (brief: micro-interactions only) and it makes
   programmatic scroll probes lie - window.scrollTo() returns before the
   animated scroll happens, so scrollY reads 0 and the page looks scroll-
   locked (the profile ?tab=contact regression report). Instant is honest;
   JS that wants smooth passes {behavior:'smooth'} explicitly, which still
   works. This loads after the Bootstrap CDN link, so it wins the cascade. */
:root {
    scroll-behavior: auto;
}

body {
    font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
    line-height: 1.6;
    color: var(--text-color);
    background: var(--background-color, #f8f9fa) !important;
    min-height: 100vh;
    margin: 0;
    padding: 0;
}

/* Page wrapper - full viewport grid (shared by Hub and Dashboard) */
/* Sticky-footer shell (matches the admin console's durable pattern): a flex
   column at least the viewport tall. The navbar is position:sticky so it
   stays put under document scroll; .main-content flex-grows to push the
   footer to the viewport bottom on short pages; on long pages the footer
   sits at the content end and can never overlap content. Replaces the old
   height:100vh grid + internal main-content scroll + the position:fixed
   footer that hid content behind it. */
.page-wrapper {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    min-height: 100dvh; /* mobile URL-bar correctness */
    width: 100%;
    overflow-x: hidden;
}

/* Main content area */
.main-content {
    flex: 1 0 auto;   /* grow to fill, never shrink below content */
    width: 100%;
    overflow-x: hidden;
    /* MANDATORY companion of the pinned footer (bottom chrome model): at
       full scroll the last element must clear the fixed footer by >=12px.
       The old pinned footer hid content precisely because this was missing -
       never remove one without the other. */
    padding-bottom: calc(var(--footer-height, 40px) + env(safe-area-inset-bottom, 0px) + 12px);
}

/* Containers */
.container {
    max-width: 800px;
    margin: 0 auto;
    padding: 0 1rem;
}

.form-container {
    background: white;
    padding: 2rem;
    border-radius: 8px;
    box-shadow: var(--shadow);
}

/* Forms */
.form-group {
    margin-bottom: 1.5rem;
}

.form-group label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 500;
}

.form-group input,
.form-group select,
.form-group textarea {
    width: 100%;
    padding: 0.75rem;
    border: 1px solid var(--border-color);
    border-radius: 4px;
    font-size: 1rem;
    font-family: inherit;
}

.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
    outline: none;
    border-color: var(--primary-color);
    box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}

.form-group small {
    display: block;
    margin-top: 0.25rem;
    color: var(--secondary-color);
    font-size: 0.875rem;
}

.form-actions {
    margin-top: 2rem;
    padding-top: 1.5rem;
    border-top: 2px solid var(--border-color);
}

/* Messages */
.message {
    padding: 1rem;
    border-radius: 4px;
    margin-bottom: 1rem;
    display: none;
}

.message-success {
    background: var(--success-bg);
    color: var(--success-text);
    border: 1px solid var(--success-border);
}

.message-error {
    background: var(--danger-bg);
    color: var(--danger-text);
    border: 1px solid var(--danger-border);
}

/* Tables */
.entries-table,
.data-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 1rem;
}

.entries-table th,
.entries-table td,
.data-table th,
.data-table td {
    padding: 0.75rem;
    text-align: left;
    border-bottom: 1px solid var(--border-color);
}

.entries-table th,
.data-table th {
    background: var(--light-bg);
    font-weight: 600;
}

.entries-table tr:hover,
.data-table tr:hover {
    background: var(--bg-secondary);
}

/* Login Page - Black & Gold Theme */
.login-page {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    background: var(--secondary-color);
    background-image:
        radial-gradient(circle at 20% 50%, rgba(var(--primary-color), 0.15) 0%, transparent 50%),
        radial-gradient(circle at 80% 80%, rgba(var(--primary-color), 0.1) 0%, transparent 50%);
}

.login-container {
    width: 100%;
    max-width: 420px;
    padding: 1.5rem;
}

.login-card {
    background: white;
    border-radius: 16px;
    padding: 3rem 2rem;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
    border: 2px solid var(--primary-color);
}

/* Login page brand stack (mascot image + org h1 + site h2). A flex column
   guarantees the three stack in order and never overlap - and there is NO
   height cap (the old mobile 50px cap crushed it). .logo is login-only; the
   header brand uses .nav-brand. */
.logo {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    height: auto;
    margin-bottom: 2.5rem;
}

.logo img {
    max-width: 200px;
    height: auto;
    display: block;
}

.logo-icon {
    font-size: 4rem;
    margin-bottom: 1rem;
    animation: float 3s ease-in-out infinite;
}

@keyframes float {
    0%, 100% { transform: translateY(0px); }
    50% { transform: translateY(-10px); }
}

.logo h1 {
    font-size: 2rem;
    color: var(--secondary-color);
    margin-bottom: 0.5rem;
    font-weight: 700;
    letter-spacing: -0.5px;
}

.logo h2 {
    font-size: 1.1rem;
    color: var(--primary-color);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
}

.login-content {
    text-align: center;
}

.google-login-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    width: 100%;
    padding: 1rem 2rem;
    background: white;
    border: 2px solid #4285f4;
    border-radius: 50px;
    font-size: 1.1rem;
    font-weight: 600;
    color: #4285f4;
    text-decoration: none;
    transition: all 0.3s ease;
    box-shadow: 0 4px 12px rgba(0,0,0,var(--shadow-light));
}

.google-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    background: #f8f9fa;
    border-radius: 50%;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    flex-shrink: 0;
    border: 1px solid #e0e0e0;
}

.google-icon svg {
    display: block;
}

.google-login-btn:hover {
    background: #4285f4;
    color: white;
    border-color: #4285f4;
    box-shadow: 0 6px 20px rgba(66, 133, 244, 0.4);
    transform: translateY(-2px);
}

.google-login-btn:hover .google-icon {
    box-shadow: 0 4px 12px rgba(0,0,0,0.25);
    background: white;
}

.google-login-btn:active {
    transform: translateY(0);
}

.microsoft-login-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    width: 100%;
    padding: 1rem 2rem;
    background: white;
    border: 2px solid #2f2f2f;
    border-radius: 50px;
    font-size: 1.1rem;
    font-weight: 600;
    color: #2f2f2f;
    text-decoration: none;
    transition: all 0.3s ease;
    box-shadow: 0 4px 12px rgba(0,0,0,var(--shadow-light));
}

.microsoft-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    background: #f8f9fa;
    border-radius: 50%;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    flex-shrink: 0;
    border: 1px solid #e0e0e0;
}

.microsoft-icon svg {
    display: block;
}

.microsoft-login-btn:hover {
    background: #2f2f2f;
    color: white;
    border-color: #2f2f2f;
    box-shadow: 0 6px 20px rgba(47, 47, 47, 0.4);
    transform: translateY(-2px);
}

.microsoft-login-btn:hover .microsoft-icon {
    box-shadow: 0 4px 12px rgba(0,0,0,0.25);
    background: white;
}

.microsoft-login-btn:active {
    transform: translateY(0);
}

.login-note {
    margin-top: 1.5rem;
    font-size: 0.9rem;
    color: var(--text-muted);
    font-weight: 500;
}

/* Mobile optimization */
@media (max-width: 480px) {
    .login-container {
        padding: 1rem;
    }

    .login-card {
        padding: 2rem 1.5rem;
    }

    .logo h1 {
        font-size: 1.75rem;
    }

    .logo h2 {
        font-size: 1rem;
    }

    .google-login-btn {
        font-size: 1rem;
        padding: 0.875rem 1.5rem;
    }
}

/* Landing Page */
.landing-container {
    max-width: 1000px;
    margin: 0 auto;
    padding: 2rem 1rem;
}

.landing-container header {
    text-align: center;
    margin-bottom: 3rem;
}

.landing-container h1 {
    font-size: 2.5rem;
    color: var(--gold-text); /* R1b: white page (--background-color) = light surface */
    margin-bottom: 1rem;
}

.welcome-card {
    background: white;
    padding: 3rem;
    border-radius: 12px;
    box-shadow: var(--shadow);
    text-align: center;
}

.welcome-card h2 {
    margin-bottom: 1rem;
}

.features {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 2rem;
    margin: 2rem 0;
}

.feature {
    padding: 1.5rem;
}

.feature h3 {
    margin-bottom: 0.5rem;
    color: var(--gold-text); /* R1b: inside the white .welcome-card */
}

/* Recent Entries */
.recent-entries {
    margin-top: 3rem;
}

.recent-entries h2 {
    margin-bottom: 1rem;
}

.no-data {
    text-align: center;
    color: var(--secondary-color);
    padding: 2rem;
}

/* Responsive */
@media (max-width: 992px) {
    .nav-toggle {
        display: flex !important;
        align-items: center;
        justify-content: center;
    }

    /* ── Slide-out panel from the right ── */
    .nav-links {
        display: flex !important;
        position: fixed;
        top: 0;
        right: 0;
        width: 280px;
        height: 100vh;
        height: 100dvh;
        background: var(--header-bg-color, var(--navbar-color, var(--secondary-color)));
        flex-direction: column;
        gap: 0;
        padding: 4.5rem 0 0 0;
        z-index: 10050;
        transform: translateX(100%);
        transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        /* Scrolls when it has to. With `overflow: hidden` and 4.5rem of top
           padding, a phone held sideways (375px of height) cut off the bottom
           of this menu - Contact Preferences and Log out - with no way to reach
           them: you had to rotate the phone to sign out. No effect whenever the
           content already fits, which is every portrait phone. */
        overflow-y: auto;
    }

    /* Shadow ONLY when open: the closed drawer sits off-screen right, and its
       leftward blur (-4px) otherwise bleeds a grey strip back onto the
       viewport's right edge on every page. */
    .nav-links.active {
        transform: translateX(0) !important;
        box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
    }

    /* Backdrop overlay when nav is open — must be BELOW navbar z-index (10000)
       so the slide-out panel (inside navbar stacking context) renders on top */
    body.nav-open::before {
        content: '';
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
        z-index: 9999;
        animation: fadeIn 0.3s ease;
    }

    body.nav-open {
        overflow: hidden;
    }

    @keyframes fadeIn {
        from { opacity: 0; }
        to { opacity: 1; }
    }

    .nav-user {
        padding: 0.75rem 1rem;
        border-bottom: 1px solid rgba(255, 255, 255, 0.15);
        width: 100%;
        text-align: center;
    }

    .nav-link {
        width: 100%;
        text-align: left;
        padding: 1rem 1.25rem;
        font-size: 1.05rem;
    }

    .btn-logout {
        margin-top: 0.5rem;
        font-size: 1.1rem;
    }

    .role-switcher {
        flex-direction: column;
        width: 100%;
        padding: 1rem;
        border-top: 1px solid rgba(255, 255, 255, 0.15);
        border-bottom: 1px solid rgba(255, 255, 255, 0.15);
    }

    .role-select {
        width: 100%;
        padding: 0.75rem;
        font-size: 1rem;
    }

    .form-container {
        padding: 1rem;
    }

    .features {
        grid-template-columns: 1fr;
    }

    .fuel-entry-container {
        padding: 0.5rem;
    }

    .form-group label {
        font-size: 1.1rem;
        font-weight: 600;
    }

    .form-group input,
    .form-group select,
    .form-group textarea {
        font-size: 1.1rem;
        padding: 1rem;
    }
}
/* Version: 1760996007 */

/* ========== shared/enterprise-design-system.css ========== */

/**
 * ============================================================================
 * ENTERPRISE DESIGN SYSTEM - FOUNDATION
 * ============================================================================
 * Microsoft 365 & Google Workspace inspired admin interface
 *
 * Philosophy: Professional, data-dense, information-first
 * Target: World-class enterprise console experience
 *
 * Design Tokens: Colors, Typography, Spacing, Shadows
 * ============================================================================
 */

/* Inter, self-hosted (SIL OFL) - the platform's designed typeface. One
   variable file covers every weight (100-900). font-display:swap paints in the
   system fallback immediately, then swaps, so text is never invisible. This
   file is first in all three bundles, so the face is declared once for the
   whole platform. */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/assets/fonts/InterVariable.woff2') format('woff2');
}

:root {
    /* ========== BRANDING ========== */
    /* The whole --nd-gold family is RETIRED (2026-07-19 closeout ruling):
       that was the district WEBSITE's gold. Gold in the Hub comes ONLY from
       the --gold-* ramp below - the build bans all three retired golds. */
    --nd-black: #000000;

    /* ========== BUTTON SIZE SCALE (platform rule, 2026-07-30) ========== */
    /* The ONE tokenized button height scale - sizes are never incidental.
       .hub-btn is THE primitive; --sm/--md/--lg select a step. Touch widths
       (<=1024px or pointer:coarse) floor sm+md at 44px; lg stays 48px. Any
       button height literal outside this scale fails the build (Phase 1 gate).
       Desktop heights are EXACTLY three: 28 / 36 / 48. */
    --btn-h-sm: 28px;   /* in-table / dense row actions */
    --btn-h-md: 36px;   /* default: page headers, forms, toolbars */
    --btn-h-lg: 48px;   /* flow/wizard primary actions, mobile CTAs */
    --btn-h-touch: 44px; /* sm+md tap-target floor on touch widths */

    /* ========== NEUTRAL GRAY SCALE (Tailwind - canonical per docs/DESIGN-TOKENS.md) ========== */
    /* Matches the runtime theme layer (theme-css.php) and the hardcoded values
       already used throughout blades/JS. Replaced the Fluent scale 2026-07. */
    --gray-900: #111827;
    /* Deepest text */
    --gray-800: #1F2937;
    /* Primary text */
    --gray-700: #374151;
    /* Secondary text */
    --gray-600: #4B5563;
    /* Muted text */
    --gray-500: #6B7280;
    /* Disabled text */
    --gray-400: #9CA3AF;
    /* Placeholder / icons */
    --gray-300: #D1D5DB;
    /* Borders (medium) */
    --gray-200: #E5E7EB;
    /* Dividers (light) */
    --gray-100: #F3F4F6;
    /* Hover backgrounds */
    --gray-50: #F9FAFB;
    /* Subtle backgrounds (was #FFFFFF - use #fff directly for pure white) */

    /* ========== MICROSOFT BLUE (INTERACTIVE ELEMENTS) ========== */
    --ms-blue: #0078D4;
    --ms-blue-hover: #106EBE;
    --ms-blue-pressed: #005A9E;
    --ms-blue-light: #E3F2FD;

    /* ========== SEMANTIC COLORS ========== */
    /* Status family (Tailwind, canonical per docs/DESIGN-TOKENS.md):
       base = solid fills/borders, -light = chip/alert backgrounds,
       -dark = text on -light backgrounds. Replaced Fluent values 2026-07. */
    --success: #059669;
    --success-light: #ECFDF5;
    --success-dark: #065F46;
    --warning: #D97706;
    --warning-light: #FFFBEB;
    --warning-dark: #92400E;
    --error: #DC2626;
    --error-light: #FEF2F2;
    --error-dark: #991B1B;
    --danger: var(--error); /* naming alias: some markup uses --danger */
    --info: #2563EB;
    --info-light: #EFF6FF;
    --info-dark: #1E40AF;

    /* ========== ELEVATION SHADOWS (MINIMAL) ========== */
    --elevation-0: none;
    --elevation-1: 0 1px 2px rgba(0, 0, 0, 0.05);
    --elevation-2: 0 2px 8px rgba(0, 0, 0, 0.08);
    --elevation-3: 0 4px 16px rgba(0, 0, 0, 0.12);
    --elevation-modal: 0 8px 32px rgba(0, 0, 0, 0.16);

    /* ========== SPACING SCALE (8px GRID) ========== */
    --space-1: 4px;
    /* 0.5 unit */
    --ink: #191712;
    --ink-soft: #4A4638;     /* body text; ALSO the floor for caps-micro labels */
    --ink-mute: #6F6A5C;     /* tertiary text >=13px ONLY (corrected from #807A66/4.29:1); never caps-micro */
    /* ========== CANONICAL GOLD RAMP - ONE hue, six roles ==========
       Source of truth is the theme layer (SiteSettings::getCSSVariables ->
       /api/theme-css.php); this mirror keeps bundles self-sufficient and
       MUST stay value-identical. These definitions and var() fallbacks are
       the ONLY places gold hexes may appear - the bundle build fails on any
       other literal (build-css-bundles.sh gold assertion). */
    --gold: #C4B454;         /* fills; black text on it; gold TEXT on dark surfaces */
    --gold-press: #A89940;   /* hover/pressed on gold */
    --gold-tint: #F5EFD0;    /* selected states, notes, tint chips */
    --gold-border: #D9CD8A;  /* borders on tinted surfaces */
    --gold-text: #75692E;    /* gold text on LIGHT surfaces only (~5.4:1; #8B7E3F retired 2026-07-19, computed 4.07:1) */
    --gold-deep: #6B5D3F;    /* deepest emphasis; white text on it */
    --gold-text-dark: var(--gold); /* pre-ramp alias, kept for compatibility */
    /* ========== STATUS CHIP PALETTE (semantic TINT bg + DARK semantic text;
       all text/bg pairs computed >= 4.5:1 at 12px). Consumed by .status-chip
       in enterprise-components.css and the flow wizard's error/ref chips.
       Pending reuses the gold ramp. NOTE: pending text #75692E darkens the
       brief's #7c7031 (4.30:1 -> 4.75:1) to clear the brief's own AA gate. */
    --chip-pending-text: var(--gold-text, #75692E); /* == the corrected gold-as-text */
    --chip-success-bg: #E7F0E9;
    --chip-success-border: #C4DCCB;
    --chip-success-text: #387047;   /* 5.04:1 on its tint */
    --chip-danger-bg: #F9EAEB;
    --chip-danger-border: #EBC8CA;
    --chip-danger-text: #A4262C;    /* 6.22:1 on its tint */
    --chip-warning-bg: #F8EFDE;
    --chip-warning-border: #EAD6AE;
    --chip-warning-text: #8A5A17;   /* 5.17:1; #B7791F fails AA at 12px */
    --chip-neutral-bg: #F1EFE8;
    --chip-neutral-border: #D3D1C7;
    --chip-neutral-text: #444441;   /* 8.49:1 on its tint */
    /* ========== CATEGORICAL CHIPS (2026-09-28): a TYPE, not a state - the
       receipt kind on Management > Receipts (Fuel / Purchase / Filed /
       Budget). Deliberately NOT the status palette: on that table red means
       "photo still needed" and green would read as approved. Each text/bg
       pair >= 8:1. Were inline hexes in receipts.blade.php. */
    --chip-cat-coral-bg: #FAECE7;
    --chip-cat-coral-text: #712B13;
    --chip-cat-teal-bg: #E1F5EE;
    --chip-cat-teal-text: #085041;
    --chip-cat-indigo-bg: #EEEDFE;
    --chip-cat-indigo-text: #3C3489;
    --chip-cat-pink-bg: #FBEAF0;
    --chip-cat-pink-text: #72243E;
    --tap-target: 2.75rem;
    /* 44px minimum touch target (WCAG); flow-wizard's --flow-target mirrors it */
    --space-2: 8px;
    /* 1 unit (base) */
    --space-3: 12px;
    /* 1.5 units */
    --space-4: 16px;
    /* 2 units */
    --space-5: 20px;
    /* 2.5 units */
    --space-6: 24px;
    /* 3 units */
    --space-7: 28px;
    /* 3.5 units */
    --space-8: 32px;
    /* 4 units */
    --space-10: 40px;
    /* 5 units */
    --space-12: 48px;
    /* 6 units */
    --space-16: 64px;
    /* 8 units */

    /* ========== TYPOGRAPHY SCALE ========== */
    --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;

    /* Font Sizes */
    --text-xs: 12px;
    --text-sm: 13px;
    --text-base: 14px;
    --text-md: 15px;
    --text-lg: 18px;
    --text-xl: 22px;
    --text-2xl: 28px;
    --text-3xl: 32px;
    --text-4xl: 42px;

    /* Font Weights */
    --font-normal: 400;
    --font-medium: 500;
    --font-semibold: 600;
    --font-bold: 700;

    /* Line Heights */
    --leading-tight: 1.2;
    --leading-snug: 1.4;
    --leading-normal: 1.6;
    --leading-relaxed: 1.8;

    /* ========== BORDER RADIUS ==========
       TWO SETS, on purpose.

       The numeric tokens below are LEGACY. They describe nothing currently on
       screen - --radius-lg is the largest of them at 8px, and a Hub tile is
       16px. They are kept because ~30 rules reference them and silently
       repointing those would move pixels in places nobody asked to change.

       The ROLE tokens are the shipped scale, measured off the live pages
       2026-09-01. Reach for these in new work. Building from the legacy set
       lands 8px short of every surface around it, which is how real radius
       drift would eventually get created. */
    --radius-none: 0;
    --radius-sm: 2px;      /* legacy */
    --radius-base: 4px;    /* legacy */
    --radius-md: 6px;      /* legacy */
    --radius-lg: 8px;      /* legacy - NOT the largest radius in the UI */
    --radius-full: 9999px;

    /* Role tokens - what the product actually uses. */
    /* 12px, re-measured 2026-09-10. The 8px here was true on 09-01 and
       went stale when buttons moved: .hub-btn, .flow-btn, .form-input and
       the wizard's own --flow-radius were all a hard-coded 12px while this
       said 8. A token that disagrees with every control it names is worse
       than none - it lands new work 4px short of its neighbours, so people
       stop reaching for it and hard-code what they can see instead. */
    --radius-control: 12px;   /* buttons, inputs, selects, menus */
    --radius-icon-sm: 10px;   /* the 36px section-header icon chip */
    --radius-surface: 12px;   /* panels, stat cards, the larger icon chip */
    --radius-card: 16px;      /* Hub landing tiles */
    --radius-pill: 9999px;    /* year tabs, status chips */

    /* ========== TRANSITION TIMING ========== */
    --transition-fast: 150ms cubic-bezier(0.4, 0.0, 0.2, 1);
    --transition-base: 200ms cubic-bezier(0.4, 0.0, 0.2, 1);
    --transition-slow: 300ms cubic-bezier(0.4, 0.0, 0.2, 1);

    /* ========== Z-INDEX SCALE ========== */
    --z-base: 1;
    --z-dropdown: 10;
    --z-sticky: 20;
    --z-fixed: 30;
    --z-modal-backdrop: 40;
    --z-modal: 50;
    --z-toast: 60;

    /* ========== COMPONENT SIZES ========== */
    --sidebar-width: 280px;
    --sidebar-collapsed: 64px;
    --header-height: 80px; /* must match theme-css.php default (header_height); consumers' fallbacks already assume 80px */
    --footer-height: 40px;
    --command-bar-height: 64px;
    --table-row-height: 48px;
    /* Page gutters: 0 preserves pre-token rendering of .admin-root main
       (the previously-undefined vars computed to 0); themable via theme-css.php */
    --page-gutter-x: 0;
    --page-gutter-y: 0;

    /* ========== LEGACY VARIABLE MAPPING (BACKWARD COMPATIBILITY) ========== */
    /* Map old variables to new system */
    --primary-color: var(--gold); /* legacy fallback aligns with the theme */
    --secondary-color: var(--nd-black);
    /* Mapped so each alias equals the theme-css.php default for the same name */
    --text-primary: var(--gray-900);   /* #111827 */
    --text-secondary: var(--gray-700); /* #374151 */
    --text-muted: var(--gray-500);     /* #6b7280 */
    --border-primary: var(--gray-200); /* #e5e7eb */
    --border-secondary: var(--gray-300); /* #d1d5db */
    --background-color: var(--gray-50);
    --bg-secondary: var(--gray-50);    /* #f9fafb */
    --shadow: var(--elevation-1);
}

/* ========== SECTION ICON PALETTE (named, user decision 2026-07-27) ==========
   Color aids fast identification: sections keep per-section hues by DEFAULT,
   as a NAMED palette - never raw per-section hexes. Sections declare a
   palette KEY in the registry (ConsoleNav::DEFAULT_NAV / config_json "nav",
   same place as their group); the runtime theme layer emits the per-section
   wiring from the registry, and the static rules below mirror the shipped
   seven for bundle self-sufficiency. Every fg/bg pair computes >=3:1
   (WCAG 1.4.11 non-text): indigo 5.62, teal 3.59, green 3.15, blue 4.75,
   orange 3.35, purple 5.02, red 4.41. Consumers use only --section-accent
   (glyph) and --section-tint (tile bg) and fall back to the gold tile. */
:root {
    --section-indigo-fg: #4F46E5; --section-indigo-bg: #EEF2FF;
    --section-teal-fg:   #0D9488; --section-teal-bg:   #F0FDFA;
    --section-green-fg:  #16A34A; --section-green-bg:  #F0FDF4;
    --section-blue-fg:   #2563EB; --section-blue-bg:   #EFF6FF;
    --section-orange-fg: #EA580C; --section-orange-bg: #FFF7ED;
    --section-purple-fg: #9333EA; --section-purple-bg: #FAF5FF;
    --section-red-fg:    #DC2626; --section-red-bg:    #FEF2F2;
}
[data-section="fleet"]            { --section-accent: var(--section-blue-fg);   --section-tint: var(--section-blue-bg); }
[data-section="reimbursements"]   { --section-accent: var(--section-green-fg);  --section-tint: var(--section-green-bg); }
[data-section="bullying_reports"] { --section-accent: var(--section-red-fg);    --section-tint: var(--section-red-bg); }
[data-section="work_orders"]      { --section-accent: var(--section-orange-fg); --section-tint: var(--section-orange-bg); }
[data-section="assets"]           { --section-accent: var(--section-purple-fg); --section-tint: var(--section-purple-bg); }
[data-section="checks"]           { --section-accent: var(--section-teal-fg);   --section-tint: var(--section-teal-bg); }
[data-section="purchase_orders"]  { --section-accent: var(--section-indigo-fg); --section-tint: var(--section-indigo-bg); }

/* Generic palette-key consumption: NON-section surfaces (the Admin console's
   areas) declare data-palette="key" and get the same accent/tint pair the
   sections do - one palette, two attribute routes. */
[data-palette="indigo"] { --section-accent: var(--section-indigo-fg); --section-tint: var(--section-indigo-bg); }
[data-palette="teal"]   { --section-accent: var(--section-teal-fg);   --section-tint: var(--section-teal-bg); }
[data-palette="green"]  { --section-accent: var(--section-green-fg);  --section-tint: var(--section-green-bg); }
[data-palette="blue"]   { --section-accent: var(--section-blue-fg);   --section-tint: var(--section-blue-bg); }
[data-palette="orange"] { --section-accent: var(--section-orange-fg); --section-tint: var(--section-orange-bg); }
[data-palette="purple"] { --section-accent: var(--section-purple-fg); --section-tint: var(--section-purple-bg); }
[data-palette="red"]    { --section-accent: var(--section-red-fg);    --section-tint: var(--section-red-bg); }

/* "Section icon style" = District gold (site_settings section_icon_style,
   applied as data-icon-style on <body>): ONE switch repaints every section
   icon on both consoles - cards and rail together - to BLACK glyphs on the
   bright gold tile (10:1). Never white on #C4B454 (2.1:1, banned by R2). */
[data-icon-style="gold"] [data-section],
[data-icon-style="gold"] [data-palette] {
    --section-accent: #000000;
    --section-tint: var(--gold, #C4B454);
}

/* ========== GLOBAL RESETS ========== */

*,
*::before,
*::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

html {
    font-size: 16px;
    /* Base for rem calculations */
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* ========== ADMIN ROOT SCOPING ========== */
/* All enterprise admin styles are scoped to .admin-root class */
/* This prevents bleeding into The Hub (frontend) pages */

.admin-root {
    font-family: var(--font-family);
    font-size: var(--text-md);
    line-height: var(--leading-normal);
    color: var(--gray-800);
    background: var(--gray-100);
}

.admin-root main {
    padding: var(--page-gutter-y, 0) var(--page-gutter-x, 0);
}

/* ========== TYPOGRAPHY UTILITIES ========== */

.text-xs {
    font-size: var(--text-xs);
}

.text-sm {
    font-size: var(--text-sm);
}

.text-base {
    font-size: var(--text-base);
}

.text-md {
    font-size: var(--text-md);
}

.text-lg {
    font-size: var(--text-lg);
}

.text-xl {
    font-size: var(--text-xl);
}

.text-2xl {
    font-size: var(--text-2xl);
}

.text-3xl {
    font-size: var(--text-3xl);
}

.font-normal {
    font-weight: var(--font-normal);
}

.font-medium {
    font-weight: var(--font-medium);
}

.font-semibold {
    font-weight: var(--font-semibold);
}

.font-bold {
    font-weight: var(--font-bold);
}

.text-primary {
    color: var(--gray-800);
}

.text-secondary {
    color: var(--gray-700);
}

.text-muted {
    color: var(--gray-600);
}

.text-disabled {
    color: var(--gray-500);
}

.text-inverse {
    color: var(--gray-50);
}

/* ========== SPACING UTILITIES ========== */

.p-1 {
    padding: var(--space-1);
}

.p-2 {
    padding: var(--space-2);
}

.p-3 {
    padding: var(--space-3);
}

.p-4 {
    padding: var(--space-4);
}

.p-6 {
    padding: var(--space-6);
}

.p-8 {
    padding: var(--space-8);
}

.m-1 {
    margin: var(--space-1);
}

.m-2 {
    margin: var(--space-2);
}

.m-3 {
    margin: var(--space-3);
}

.m-4 {
    margin: var(--space-4);
}

.m-6 {
    margin: var(--space-6);
}

.m-8 {
    margin: var(--space-8);
}

.gap-2 {
    gap: var(--space-2);
}

.gap-3 {
    gap: var(--space-3);
}

.gap-4 {
    gap: var(--space-4);
}

.gap-6 {
    gap: var(--space-6);
}

/* ========== LAYOUT UTILITIES ========== */

.flex {
    display: flex;
}

.flex-col {
    flex-direction: column;
}

.items-center {
    align-items: center;
}

.justify-center {
    justify-content: center;
}

.justify-between {
    justify-content: space-between;
}

.flex-1 {
    flex: 1;
}

.grid {
    display: grid;
}

.hidden {
    display: none;
}

.block {
    display: block;
}

.inline-block {
    display: inline-block;
}

/* ========== BORDER UTILITIES ========== */

.border {
    border: 1px solid var(--gray-300);
}

.border-t {
    border-top: 1px solid var(--gray-300);
}

.border-b {
    border-bottom: 1px solid var(--gray-300);
}

.border-l {
    border-left: 1px solid var(--gray-300);
}

.border-r {
    border-right: 1px solid var(--gray-300);
}

.rounded {
    border-radius: var(--radius-base);
}

.rounded-md {
    border-radius: var(--radius-md);
}

.rounded-lg {
    border-radius: var(--radius-lg);
}

.rounded-full {
    border-radius: var(--radius-full);
}

/* ========== SHADOW UTILITIES ========== */

.shadow-none {
    box-shadow: var(--elevation-0);
}

.shadow-sm {
    box-shadow: var(--elevation-1);
}

.shadow {
    box-shadow: var(--elevation-2);
}

.shadow-lg {
    box-shadow: var(--elevation-3);
}

.shadow-modal {
    box-shadow: var(--elevation-modal);
}

/* ========== BACKGROUND UTILITIES ========== */

.bg-white {
    background: var(--gray-50);
}

.bg-gray-100 {
    background: var(--gray-100);
}

.bg-gray-200 {
    background: var(--gray-200);
}

.bg-blue {
    background: var(--info, #2563EB); /* semantic info utility, not brand accent */
}

.bg-gold {
    background: var(--gold); /* was --nd-gold (district-website gold); retired to the fill gold */
}

.bg-success {
    background: var(--success-light);
}

.bg-warning {
    background: var(--warning-light);
}

.bg-error {
    background: var(--error-light);
}

/* ========== TRANSITION UTILITIES ========== */

.transition-fast {
    transition: all var(--transition-fast);
}

.transition {
    transition: all var(--transition-base);
}

.transition-slow {
    transition: all var(--transition-slow);
}

/* ========== FOCUS STYLES (ACCESSIBILITY) ========== */

*:focus {
    outline: 2px solid var(--border-focus, #C4B454); /* gold focus ring */
    outline-offset: 2px;
}

*:focus:not(:focus-visible) {
    outline: none;
}

*:focus-visible {
    outline: 2px solid var(--border-focus, #C4B454); /* gold focus ring */
    outline-offset: 2px;
}

/* ========== PRINT STYLES ========== */

@media print {
    * {
        box-shadow: none !important;
        text-shadow: none !important;
    }

    body {
        background: white;
        color: black;
    }

    .no-print {
        display: none !important;
    }
}

/* ========== REDUCED MOTION (ACCESSIBILITY) ========== */

@media (prefers-reduced-motion: reduce) {

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* ========== shared/enterprise-components.css ========== */

/**
 * ============================================================================
 * ENTERPRISE COMPONENTS LIBRARY
 * ============================================================================
 * Reusable UI components inspired by Microsoft 365 & Google Workspace
 *
 * SCOPE: All components scoped to .admin-root to prevent conflicts with The Hub
 *
 * Components:
 * 0. Admin Shell Layout (Master Container)
 * 1. Dashboard Metrics Cards
 * 2. Command Bar (Action Toolbar)
 * 3. Enterprise Data Tables
 * 4. Sidebar Navigation
 * 5. Chips/Tags
 * 6. Icon Buttons
 * 7. Buttons (Primary, Secondary, Tertiary)
 * 8. Breadcrumbs
 * ============================================================================
 */

/* ==========================================================================
   THE BUTTON PRIMITIVE (2026-07-27). One class, three variants, identical
   on <a> and <button>: explicit height + line-height:1 kill the two ways
   element type changed size (anchors inherit body line-height 1.6 ->
   20.8px while buttons use 'normal' ~15px; bordered variants grew 2px
   because height was content-derived). Never style a link as a button ad
   hoc - put it on this primitive. Named hub-btn (the legacy Bootstrap-family
   btn primitive was migrated onto this and DELETED 2026-07-30; the build
   gate check-button-primitive.py fails if it returns).
   ========================================================================== */
.hub-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* An icon and its label must not touch. Without this the primitive renders
       "GSync All Users" and "CRefresh" - align-items centres them and nothing
       separates them. On the primitive rather than per-button because it is
       every icon button in the product; an icon-only button has one child, so
       a gap costs it nothing. */
    gap: 8px;
    height: var(--btn-h-md, 36px); /* explicit - border-box keeps borders out of it */
    line-height: 1;        /* kills the a-vs-button divergence */
    padding: 0 16px;
    font-size: 13px;
    font-weight: 600;
    font-family: inherit;
    box-sizing: border-box;
    white-space: nowrap;
    border: 0;
    border-radius: var(--radius-control);
    cursor: pointer;
    text-decoration: none;
    color: var(--ink, #191712);
    background: transparent;
    transition: background 150ms ease, border-color 150ms ease;
}

/* A row of fields with an action beside them.
   Two defects it prevents, both measured and both recurring:

   THE 4px DROP. align-items:flex-end aligns MARGIN boxes, and
   `.admin-root label:not(...)` gives every label margin-bottom:4px - so the
   field's margin box ends 4px below the field itself and the button lines up
   with the margin, not the input. Measured on Budget settings before this:
   Save Budget bottom 236.3 against an input bottom of 232.3.

   THE STRETCH. In a grid the button is a grid ITEM and fills its track -
   "Save 2025-26" rendered 383px wide. justify-self keeps it its own size.

   Used instead of repeating inline align-items:flex-end, which appears 13
   times across 9 files and has now been hand-fixed twice. */
.hub-field-row {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: flex-end;
}

/* .admin-root is carried because the rule this has to beat is
   `.admin-root label:not(.form-check-label):not(.pkg-filter-label):not(.hub-toggle)`
   - four class-level selectors. A bare `.hub-field-row > label` is one, loses,
   and leaves the 4px in place; measured exactly that way on the first attempt.
   Both shells live under .admin-root, so this wins on real specificity rather
   than on !important. */
.hub-field-row > label,
.hub-field-grid > label,
.admin-root .hub-field-row > label:not(.form-check-label):not(.pkg-filter-label):not(.hub-toggle),
.admin-root .hub-field-grid > label:not(.form-check-label):not(.pkg-filter-label):not(.hub-toggle) {
    /* The margin is what flex-end was aligning to. The :not() chain is not
       decoration - it matches the excluding rule's (0,4,1) and adds the
       .hub-field-row class to reach (0,5,1). Two classes alone lost, and the
       label went on measuring 4px. */
    margin-bottom: 0;
}

.hub-field-row > .hub-btn,
.hub-field-row > form > .hub-btn {
    flex: 0 0 auto;
}

.hub-field-grid > .hub-btn,
.admin-root .hub-field-grid > .hub-btn {
    justify-self: start;
    align-self: end;
}

/* Same HEIGHT as the button beside them, not just the same bottom edge.
   .hub-btn is 36px (--btn-h-md, and check-density.py enforces it); these
   fields are inline-styled padding:9px + 1px border and land at 39.6px, so
   their tops sat 3.6px higher than the button's. The button height is the
   specified one, so the fields meet it.

   Scoped to these two utilities deliberately - the same inline field styling
   is everywhere, and resizing all of it from here would change pages nobody
   asked about. */
.hub-field-row input:not([type=checkbox]):not([type=radio]),
.hub-field-row select,
.hub-field-grid input:not([type=checkbox]):not([type=radio]),
.hub-field-grid select {
    height: var(--btn-h-md, 36px);
}

/* Touch widths: the .hub-btn block above lifts the button to --btn-h-touch
   (44px), but this height lock did not follow it - so below 1024px and on
   every coarse pointer the fields sat 36px against a 44px button, an 8px
   mismatch on exactly the rows this utility exists to align. Same media
   condition and same selector list as above, after it in source order. */
@media (max-width: 1024px), (pointer: coarse) {
    .hub-field-row input:not([type=checkbox]):not([type=radio]),
    .hub-field-row select,
    .hub-field-grid input:not([type=checkbox]):not([type=radio]),
    .hub-field-grid select {
        height: var(--btn-h-touch, 44px);
    }
}

.hub-btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold, #C4B454) 45%, transparent);
}

.hub-btn--ghost {
    background: var(--surface, #FFFFFF);
    border: 1.5px solid #CFC9B0;   /* secondary recipe */
}

.hub-btn--ghost:hover,
.hub-btn--ghost[aria-expanded="true"] {
    border-color: var(--gold-border, #D9CD8A);
    background: var(--gold-tint, #F5EFD0);
}

.hub-btn--gold {
    background: var(--btn-primary-bg, #C4B454);
    color: #000;                   /* R1: text on gold is black */
    font-weight: 700;
}

.hub-btn--gold:hover {
    background: var(--gold-press, #A89940);
}

.hub-btn--danger {
    background: var(--surface, #FFFFFF);
    color: #A4262C;
    border: 1px solid #A4262C;
}

.hub-btn--danger:hover {
    background: var(--chip-danger-bg, #F9EAEB);
}

/* Compact variant for DENSE desktop contexts (in-table row actions, chip-side
   affordances): 28px, 12px label. Composes with --ghost/--gold/--danger. The
   touch block below still lifts it to 44px - 44px is a TOUCH requirement,
   not a desktop one. */
.hub-btn--sm {
    height: var(--btn-h-sm, 28px);
    padding: 0 10px;
    font-size: 12px;
    border-radius: 8px;
}

/* Large variant: flow/wizard primary actions and full-bleed mobile CTAs.
   48px on EVERY width (already >= the 44px touch floor, so it never shrinks). */
.hub-btn--lg {
    height: var(--btn-h-lg, 48px);
    padding: 0 24px;
    font-size: 15px;
}

/* Icon-only variant: square 1:1 at the current height step, no side padding.
   Compose with --sm for a 28px dense icon button (e.g. a table kebab). Always
   pair with an aria-label (icon carries no text). */
.hub-btn--icon {
    width: var(--btn-h-md, 36px);
    padding: 0;
}
.hub-btn--icon.hub-btn--sm {
    width: var(--btn-h-sm, 28px);
}
.hub-btn--icon.hub-btn--lg {
    width: var(--btn-h-lg, 48px);
}

/* Touch widths: sm+md floor at the 44px tap-target; lg keeps its 48px, and
   icon buttons stay square by matching width to the lifted height. The base
   .hub-btn rule (44px) applies to md AND sm here; --lg/--icon re-assert after
   so source order keeps lg=48 and icons square. 44px is a TOUCH requirement,
   not a desktop one (platform rule, 2026-07-28). */
@media (max-width: 1024px), (pointer: coarse) {
    .hub-btn {
        height: var(--btn-h-touch, 44px);
    }
    .hub-btn--lg {
        height: var(--btn-h-lg, 48px);
    }
    .hub-btn--icon {
        width: var(--btn-h-touch, 44px);
    }
    /* The compound --icon.--sm (0,2,0) outranks the bare --icon lift (0,1,0)
       regardless of source order - without this re-assert the kebab trigger
       shipped 28px WIDE x 44px tall at touch widths (caught by the 2026-07-30
       adversarial review; media queries do not raise specificity). */
    .hub-btn--icon.hub-btn--sm {
        width: var(--btn-h-touch, 44px);
    }
    .hub-btn--icon.hub-btn--lg {
        width: var(--btn-h-lg, 48px);
    }
}

/* PLATFORM RULE (2026-07-19): buttons never wrap. A label that would
   overflow its container gets a SHORTER LABEL - never a smaller font,
   never a second line. Shared file: applies on hub, admin, and mgmt. */
button,
.flow-btn,
.action-btn,
.hub-btn,
.hub-btn-primary {
    white-space: nowrap;
}

/* ==========================================================================
   ROW-ACTIONS KEBAB MENU (platform rule, 2026-07-30). ONE overflow-menu
   primitive: a single 28px icon trigger (.hub-btn--icon.hub-btn--sm) opens an
   anchored panel of a row's actions, collapsing N inline buttons into one
   target so dense console tables never overflow their pane. Behavior lives in
   public/assets/js/hub-menu.js (open/close, Esc, outside-click, arrow keys,
   focus return, aria-expanded). Accessible menu semantics: role=menu on the
   panel, role=menuitem on each action. Markup: partials/row-menu.blade.php.
   ========================================================================== */
.hub-menu { position: relative; display: inline-block; }
.hub-menu-panel {
    position: absolute;
    top: calc(100% + 4px);
    right: 0;
    z-index: 40;
    min-width: 168px;
    padding: 4px;
    background: var(--surface, #FFFFFF);
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
    box-shadow: 0 1px 2px rgba(23, 21, 15, 0.06), 0 8px 24px rgba(23, 21, 15, 0.08);
    display: grid;
    gap: 2px;
}
.hub-menu-panel[hidden] { display: none; }
/* A form wrapper inside the panel is transparent to the grid (its submit
   button IS the menu row). */
.hub-menu-panel form { margin: 0; display: contents; }
/* Menu rows are NOT .hub-btn - they are full-width menu items with their own
   reset (documented exception in check-button-primitive.py). The button-gate
   exemption does NOT waive a11y: 36px desktop / 44px touch (below) + a visible
   :focus-visible ring (below); roving focus is in hub-menu.js. */
.hub-menu-item {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 36px;
    padding: 0 10px;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: var(--ink, #191712);
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    text-align: left;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
}
.hub-menu-item:hover {
    background: var(--gold-tint, #F5EFD0);
}
/* Keyboard focus MUST show a visible RING - a background tint alone is not a
   focus indicator (fails keyboard users). Dark gold --gold-text computes
   >=4.5:1 on white AND on the tint (>=3:1 non-text floor, R2); inset offset so
   the panel's 4px padding never clips it. Verified by check-glyph-contrast.py
   (menu-item focus state). */
.hub-menu-item:focus-visible {
    background: var(--gold-tint, #F5EFD0);
    outline: 2px solid var(--gold-text, #75692E);
    outline-offset: -2px;
}
.hub-menu-item--danger { color: #A4262C; }
.hub-menu-item--danger:hover { background: var(--chip-danger-bg, #F9EAEB); }
.hub-menu-item--danger:focus-visible {
    background: var(--chip-danger-bg, #F9EAEB);
    outline: 2px solid #A4262C;
    outline-offset: -2px;
}
.hub-menu-item-icon { display: inline-flex; width: 16px; height: 16px; flex: none; }
.hub-menu-item-icon svg { width: 100%; height: 100%; }
.hub-menu-dots { display: block; }
@media (max-width: 1024px), (pointer: coarse) {
    .hub-menu-item { min-height: 44px; }
}

/* ========== 0. ADMIN SHELL LAYOUT (MASTER CONTAINER) ========== */

/* Gutter token: :root ONLY (never on .admin-shell — inheritance would shadow
   the media step-downs in _responsive.css). Base 20px; -> 16 <=1280, 12 <=600. */
:root { --admin-content-gutter: 20px; }

.admin-shell {
    /* ONE sidebar-width token drives the grid's first column. Desktop rail state
       flips --admin-sidebar-w to --admin-rail-w via [data-sidebar-pref] / the
       auto-rail media query (Lever 1, _responsive.css). */
    --admin-sidebar-w: 280px;
    --admin-rail-w: 72px;
    display: grid;
    grid-template-columns: var(--admin-sidebar-w) minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr) auto;
    min-height: 100vh;
    min-height: 100dvh; /* dvh accounts for the mobile browser toolbar; a plain
                           100vh strands the grid's footer row (row 3) behind
                           the toolbar, reading as a clipped footer. */
    background: var(--gray-50);
    transition: grid-template-columns 160ms cubic-bezier(0.4, 0.0, 0.2, 1);
}

/* The old 2-state .has-collapsed-sidebar desktop rail (fixed 69px) is RETIRED:
   Lever 1's token-driven [data-sidebar-pref] rail (_responsive.css) flips
   --admin-sidebar-w to --admin-rail-w (72px) and is the single desktop width
   mechanism. The class is no longer applied on desktop; the mobile off-canvas
   overlay uses .admin-sidebar.open (unchanged). */

/* Sidebar styling is defined in admin-layout.css */

.admin-header {
    grid-column: 2;
    grid-row: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-4);
    border-bottom: 1px solid var(--gray-300);
    background: var(--gray-50);
    position: sticky;
    top: 0;
    z-index: 20;
    height: 72px;
    box-sizing: border-box;
    min-width: 0; /* grid item: let it shrink below content width so the
                     breadcrumb ellipsis engages instead of panning the whole
                     console sideways at phone widths (matches .admin-main). */
}

/* The breadcrumb is the flexible side of the header: it must be allowed to
   shrink (flex items default to min-width:auto = content width) so its own
   overflow/ellipsis can take effect; the avatar/bell/switcher never shrink. */
.admin-header .admin-breadcrumb,
.admin-header .breadcrumb {
    min-width: 0;
}

/* The phone back-chip: server-computed PARENT of the current page (never a
   self-link - EnterpriseHeader picks it). Hidden on desktop, where the full
   trail renders; the <=640px block in _responsive.css swaps them. */
.admin-header .breadcrumb-back {
    display: none;
}

/* Hamburger that opens the <=1024px sidebar drawer (EnterpriseHeader).
   Hidden on desktop; shown in the drawer media block below. */
.admin-nav-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: var(--tap-target, 2.75rem);
    height: var(--tap-target, 2.75rem);
    margin-right: var(--space-2);
    border: none;
    background: none;
    border-radius: var(--radius-base);
    color: var(--gray-700);
    font-size: 1.15rem;
    cursor: pointer;
    flex-shrink: 0;
}

.admin-nav-toggle:hover {
    background: var(--gray-200);
}

.admin-main {
    grid-column: 2;
    grid-row: 2;
    padding: var(--space-3) var(--space-4);
    overflow: auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
    min-width: 0; /* grid item: without this, wide children push the page sideways */
    position: relative;
    z-index: 1;
}

/* Main content wrapper (replaces old tab system with sidebar navigation) */
.admin-main-content {
    display: flex;
    flex-direction: column;
    opacity: 1;
    /* Lever 2: symmetric gutter via the ONE token. Top/bottom stay explicit
       longhands (they carry footer/bottom-chrome clearance — do not touch). */
    padding-block: var(--space-4);
    padding-inline: var(--admin-content-gutter);
    height: 100%;
    min-width: 0; /* see .admin-main */
}

/* Fixed content header - stays at top */
.admin-main-content .content-header,
.admin-tab .tab-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem var(--space-4);
    /* Bleed to the content edges: L/R negatives mirror the symmetric gutter token
       so the header stays flush after Lever 2 (was -space-4 / -space-6). */
    margin: calc(var(--space-4) * -1) calc(var(--admin-content-gutter) * -1) 0 calc(var(--admin-content-gutter) * -1);
    background: white;
    border-bottom: 2px solid var(--border-primary);
    flex-shrink: 0;
    min-height: 60px;
}

.admin-main-content .content-header h1,
.admin-tab .tab-header h1 {
    margin: 0;
    font-size: 1.75rem;
    font-weight: 700;
    color: var(--text-primary);
}

.admin-main-content .content-header h1 i,
.admin-tab .tab-header h1 i {
    margin-right: 0.5rem;
    opacity: 0.7;
}

.admin-main-content .content-header .text-muted,
.admin-tab .tab-header .text-muted {
    margin: 0.25rem 0 0;
    font-size: 0.9rem;
}

/* Scrollable content body */
.content-body,
.tab-content-scroll {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    padding: var(--space-4) 0;
    background: white;
    min-width: 0;
}

/* Legacy tab wrapper - backwards compatibility */
.admin-tab.active {
    display: flex;
    flex-direction: column;
    opacity: 1;
    padding: var(--space-4);
    padding-right: var(--space-6);
    height: 100%;
}

/* Wider padding on large screens. (Lever 2: the .admin-main-content /
   .admin-tab fat-gutter override — padding-right:3rem — is REMOVED; the
   --admin-content-gutter token is the sole source of inline padding.) */
@media (min-width: 1600px) {
    .content-body,
    .tab-content-scroll {
        padding: 2rem 1rem;
    }
}

/* Ensure all direct children respect container width */
.content-body > *,
.tab-content-scroll > * {
    max-width: 100%;
}

.admin-footer {
    grid-column: 2;
    grid-row: 3;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-4);
    border-top: 1px solid var(--gray-300);
    background: var(--gray-50);
    font-size: var(--text-xs);
    color: var(--gray-600);
    position: sticky;
    bottom: 0;
    z-index: 10;
}

/* Admin Header Components */
.admin-header .breadcrumb {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
    color: var(--gray-600);
    min-width: 0; /* allow shrink: unshrinkable children forced 449px min-content */
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.admin-header .breadcrumb a,
.admin-header .breadcrumb span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.admin-header .breadcrumb a {
    color: var(--gold-text, #75692E); /* R1b: gold-as-text on light */
    text-decoration: none;
}

.admin-header .breadcrumb a:hover {
    text-decoration: underline;
}

/* The top header's action cluster (Management/Admin switcher, bell, avatar).
   Own class on purpose: "header-actions" is the page-content toolbar class,
   and its <=767px stacking rule must never apply inside the 72px header.
   Row + centered at ALL widths. */
.admin-header .admin-header-actions {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-3);
}

/* ============================================================
   HEADER CONTROL CHROME (2026-08-10, measured live at 1197px).
   Shared header, so this reaches every admin + management page.
   Three defects fixed on the ACTIONS controls only — the .hub-btn
   primitive itself is untouched (its 13px/36px steps are platform
   law everywhere else):
     1. GLYPH SCALE — .svg-icon svg is 1em, so a 13px-font button
        rendered a 13x13 glyph: the outlier against the 16-20px
        platform icon scale (the adjacent profile items are 17px).
        Pin 18px and normalize stroke to 2 — the 2.2 bump below
        exists for SMALL chrome and reads heavy once scaled up.
     2. ICON->LABEL GAP — .hub-btn sets display:inline-flex but
        never a gap, so the glyph literally touched its label
        (computed gap:normal = 0px). Explicit 8px, applied to any
        header control pairing a glyph with a label.
     3. TAP TARGETS — 36px desktop height failed the 44px minimum
        on a bell that is a primary control. 44px here (bell square,
        labelled control min-height) is a DELIBERATE divergence from
        the ">=1025px never over 36px" density law, registered in
        scripts/check-density.py so the rule stays truthful.
   The header's own 72px height and the 12px inter-control gap above
   are deliberately NOT touched. Colors/palette unchanged (brand law:
   white glyph on gold, ink on light). */
.admin-header .admin-header-actions .hub-btn {
    gap: 8px;
    min-height: var(--btn-h-touch, 44px);   /* min-height beats the primitive's height:36px */
}
.admin-header .admin-header-actions .hub-btn--icon {
    width: var(--btn-h-touch, 44px);
    height: var(--btn-h-touch, 44px);       /* square 44x44 (the bell) */
}
.admin-header .admin-header-actions .hub-btn svg {
    width: 18px;
    height: 18px;
    stroke-width: 2;                        /* outranks the 2.2 small-chrome bump */
}

/* R1b: the admin header surface is light (--gray-50), so the signed-in name
   uses the dark gold-as-text, not the bright dark-surface gold (--gold, which
   stays on the DARK staff header, where this .admin-root scope doesn't reach). */
.admin-root .nav-user-name {
    color: var(--gold-text, #75692E);
}

.admin-header .search-input {
    width: 240px;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--gray-300);
    border-radius: var(--radius-base);
    font-size: var(--text-sm);
    background: var(--gray-50);
}

.admin-header .search-input:focus {
    outline: none;
    border-color: var(--border-focus, #C4B454);
}

/* ICON-PREFIXED INPUTS (2026-07-27): when a glyph overlays a field, the
   text must start clear of it - padding-left >= 36px (icon + both gaps).
   Specificity (0,3,0) on purpose: the base .admin-root input[type=...]
   rule (0,2,1) was silently beating page-level search styles and parking
   placeholders under their icons. */
.admin-root .mgmt-home-search .mgmt-home-search-input,
.admin-root .header-search .search-input,
.admin-root .input-has-icon input {
    padding-left: 2.5rem;
}

/* ========================================
   ENTERPRISE FORM INPUTS
   Google Workspace / Microsoft 365 style
   ======================================== */

/* --- Base text inputs & textareas --- */
.admin-root .form-control,
.admin-root input[type="text"],
.admin-root input[type="search"],
.admin-root input[type="email"],
.admin-root input[type="number"],
.admin-root input[type="date"],
.admin-root input[type="datetime-local"],
.admin-root input[type="password"],
.admin-root input[type="tel"],
.admin-root input[type="url"],
.admin-root textarea {
    font-family: var(--font-family);
    font-size: var(--text-base);
    font-weight: var(--font-normal);
    color: var(--gray-800);
    background: var(--gray-50);
    border: 1px solid var(--gray-400);
    /* Was --radius-base (4px, marked "legacy" in the token file). These are
       the only controls in the product that were not already 12px, so this
       is the one line here that moves a pixel - management fields finally
       match the buttons sitting next to them. */
    border-radius: var(--radius-control);
    padding: 8px 12px;
    line-height: var(--leading-snug);
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
}

.admin-root .form-control:hover,
.admin-root input[type="text"]:hover,
.admin-root input[type="search"]:hover,
.admin-root input[type="email"]:hover,
.admin-root input[type="number"]:hover,
.admin-root input[type="date"]:hover,
.admin-root input[type="password"]:hover,
.admin-root textarea:hover {
    border-color: var(--gray-700);
}

.admin-root .form-control:focus,
.admin-root input[type="text"]:focus,
.admin-root input[type="search"]:focus,
.admin-root input[type="email"]:focus,
.admin-root input[type="number"]:focus,
.admin-root input[type="date"]:focus,
.admin-root input[type="password"]:focus,
.admin-root textarea:focus {
    outline: none;
    border-color: var(--border-focus, #C4B454);
    box-shadow: 0 0 0 1px var(--border-focus, #C4B454);
}

.admin-root .form-control::placeholder,
.admin-root input::placeholder,
.admin-root textarea::placeholder {
    color: var(--gray-600);
    opacity: 1;
}

.admin-root .form-control:disabled,
.admin-root input:disabled,
.admin-root textarea:disabled,
.admin-root .form-control[readonly],
.admin-root input[readonly] {
    background: var(--gray-200);
    color: var(--gray-600);
    border-color: var(--gray-300);
    cursor: not-allowed;
}

/* --- Small variant --- */
.admin-root .form-control-sm,
.admin-root .form-control.form-control-sm {
    font-size: var(--text-sm);
    padding: 6px 10px;
    height: auto;
    line-height: var(--leading-snug);
}

/* --- Select inputs --- */
.admin-root .form-select,
.admin-root select {
    font-family: var(--font-family);
    font-size: var(--text-base);
    font-weight: var(--font-normal);
    color: var(--gray-800);
    background-color: var(--gray-50);
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23605E5C' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 12px;
    border: 1px solid var(--gray-400);
    /* Matches the text inputs above and the buttons beside them. A select is
       a control; --radius-base is the legacy 4px. */
    border-radius: var(--radius-control);
    padding: 8px 32px 8px 12px; /* 32px right = custom caret clearance */
    /* Caret clearance floor: appearance:none selects paint the SVG caret at
       calc(100% - 10px); a squeezed select (the 59px fiscal-year case) put
       the caret ON the text. Inline styles must never shrink padding-right
       below ~28px. */
    min-width: 96px;
    line-height: var(--leading-snug);
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    cursor: pointer;
}

.admin-root .form-select:hover,
.admin-root select:hover {
    border-color: var(--gray-700);
}

.admin-root .form-select:focus,
.admin-root select:focus {
    outline: none;
    border-color: var(--border-focus, #C4B454);
    box-shadow: 0 0 0 1px var(--border-focus, #C4B454);
}

.admin-root .form-select:disabled,
.admin-root select:disabled {
    background-color: var(--gray-200);
    color: var(--gray-600);
    border-color: var(--gray-300);
    cursor: not-allowed;
}

/* --- Small select variant --- */
.admin-root .form-select-sm,
.admin-root .form-select.form-select-sm {
    font-size: var(--text-sm);
    padding: 6px 28px 6px 10px;
    background-position: right 8px center;
    background-size: 10px;
    height: auto;
}

/* --- Checkbox & Radio (Google style) --- */
.admin-root .form-check-input {
    width: 18px;
    height: 18px;
    border: 2px solid var(--gray-600);
    border-radius: var(--radius-sm);
    background: var(--gray-50);
    cursor: pointer;
    transition: all var(--transition-fast);
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    vertical-align: middle;
    position: relative;
}

.admin-root .form-check-input[type="radio"] {
    border-radius: 50%;
}

.admin-root .form-check-input:checked {
    background-color: var(--btn-primary-bg, #C4B454);
    border-color: var(--btn-primary-bg, #C4B454);
}

.admin-root .form-check-input:checked[type="checkbox"]::after {
    content: '';
    position: absolute;
    left: 4px;
    top: 1px;
    width: 6px;
    height: 10px;
    border: solid white;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

.admin-root .form-check-input:focus {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--gold, #C4B454) 45%, transparent);
    border-color: var(--border-focus, #C4B454);
}

.admin-root .form-check-label {
    font-size: var(--text-base);
    color: var(--gray-800);
    margin-left: 6px;
    cursor: pointer;
    vertical-align: middle;
}

/* --- Input groups (e.g., currency prefix) --- */
.admin-root .input-group {
    display: flex;
    align-items: stretch;
}

.admin-root .input-group .input-group-text {
    font-size: var(--text-base);
    color: var(--gray-700);
    background: var(--gray-200);
    border: 1px solid var(--gray-400);
    border-right: none;
    border-radius: var(--radius-base) 0 0 var(--radius-base);
    padding: 8px 12px;
    line-height: var(--leading-snug);
}

.admin-root .input-group .form-control {
    border-radius: 0 var(--radius-base) var(--radius-base) 0;
}

/* --- Labels & Form groups --- */
.admin-root .form-label,
.admin-root label:not(.form-check-label):not(.pkg-filter-label):not(.hub-toggle) {
    font-size: var(--text-sm);
    font-weight: var(--font-medium);
    color: var(--gray-700);
    margin-bottom: 4px;
    display: inline-block;
}

.admin-root .form-group {
    margin-bottom: var(--space-4);
}

.admin-root .form-text {
    font-size: var(--text-xs);
    color: var(--gray-600);
    margin-top: 4px;
}

/* Admin Navigation Links */
.admin-nav,
.mgmt-nav {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding-left: 10px; /* Caret gutter — inside sidebar overflow bounds */
}

.admin-nav-link {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-radius: 0 999px 999px 0; /* Google-style half-circle right edge */
    text-decoration: none;
    color: rgba(255, 255, 255, 0.8);
    font-size: var(--text-md);
    transition: all var(--transition-fast);
}

.admin-nav-link:hover {
    background: rgba(255, 255, 255, 0.1);
    color: white;
}

.admin-nav-link.active {
    background: var(--btn-primary-bg, #C4B454); /* final: gold fill, black label */
    color: var(--btn-primary-text, #000000);
}

.admin-nav-link.active i,
.admin-nav-link.active .svg-icon {
    color: #000; /* R2 corrected 2026-07-27: white on #C4B454 = 2.1:1 FAILS; black matches the label at 10:1 */
}

.admin-nav-link i {
    width: 20px;
    text-align: center;
    font-size: 18px;
    flex-shrink: 0;
}

/* Page Header Component */
.admin-root .nd-page-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: var(--space-6);
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--gray-300);
    margin-left: calc(var(--space-4) * -1);
    margin-right: calc(var(--space-4) * -1);
    padding-left: var(--space-4);
    padding-right: var(--space-4);
}

.admin-root .nd-page-header h1 {
    font-size: var(--text-2xl);
    font-weight: var(--font-semibold);
    color: var(--gray-900);
    margin: 0 0 var(--space-1) 0;
}

.admin-root .nd-page-header p {
    font-size: var(--text-sm);
    color: var(--gray-600);
    margin: 0;
}

/* Responsive Admin Shell: the <=1024px drawer/shell rules live in
   shared/_responsive.css (MUST concatenate last - build asserts it). */


/* Backdrop base (all widths; only ever .show below 1025px) */
.admin-drawer-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    z-index: 10005; /* under the drawer (10010), over everything else */
}

/* ========== SUMMARY FIGURE GRID ========== */
/*
 * The two-or-three headline figures a register puts above its rows - budget /
 * spent / remaining, and the same shape wherever else it turns up.
 *
 * Two across on a phone, and a row that ends up holding ONE card is CENTRED at
 * the same size as its neighbours rather than hanging on the left. Owner,
 * 2026-09-01, on three budget tiles on a phone: "can we center remaining
 * balance cards?" and then, on a first pass that stretched it edge to edge:
 * "total card didn't need to grow. just center".
 *
 * So the orphan keeps a one-column WIDTH and only its POSITION changes. It is
 * given the whole row to sit in (grid-column: 1 / -1), pulled back to a single
 * column by width, and centred in what is left. The width is exact rather than
 * approximate: the row is 2c + gap wide, so calc(50% - gap/2) resolves to
 * exactly c, the same figure the two cards above it get. box-sizing is
 * border-box platform-wide (base.css), so the card's padding and border are
 * inside that number.
 *
 * From 641px the three figures fit on one line, auto-fit leaves no orphan to
 * correct, and BOTH declarations have to be switched back off - with three
 * cards the third is odd and last, so it would otherwise be squeezed to half a
 * row and floated into the middle of it.
 *
 * Named .hub-stat-grid rather than .stat-grid on purpose: shared-media.css
 * carries a legacy `.stats-grid { grid-template-columns: 1fr !important }`
 * phone override and it concatenates AFTER this file, so the near-miss name
 * would be one typo away from silently collapsing to a single column.
 */
.hub-stat-grid {
    --stat-gap: 12px;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--stat-gap);
}

.hub-stat-grid > :last-child:nth-child(odd) {
    grid-column: 1 / -1;
    justify-self: center;
    width: calc(50% - var(--stat-gap) / 2);
}

@media (min-width: 641px) {
    .hub-stat-grid {
        grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    }

    .hub-stat-grid > :last-child:nth-child(odd) {
        grid-column: auto;
        justify-self: stretch;
        width: auto;
    }
}

/* ========== 1. DASHBOARD METRICS CARDS ========== */

.admin-root .metrics-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: var(--space-6);
    margin-bottom: var(--space-6);
}

.admin-root .metric-card {
    display: flex;
    gap: var(--space-4);
    background: var(--gray-50);
    border: 1px solid var(--gray-300);
    border-radius: var(--radius-base);
    padding: var(--space-4);
    transition: box-shadow var(--transition-base);
}

.admin-root .metric-card:hover {
    box-shadow: var(--elevation-2);
}

.admin-root .metric-icon {
    width: 48px;
    height: 48px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--gray-100);
    border-radius: var(--radius-full);
    font-size: 20px;
    color: var(--ms-blue);
    flex-shrink: 0;
}

.admin-root .metric-icon.gold {
    background: var(--warning-light);
    color: var(--gold-text); /* was --nd-gold (district-website gold); glyph on a light tint = light-surface gold */
}

.admin-root .metric-icon.success {
    background: var(--success-light);
    color: var(--success);
}

.admin-root .metric-icon.error {
    background: var(--error-light);
    color: var(--error);
}

.admin-root .metric-content {
    flex: 1;
    min-width: 0;
}

.admin-root .metric-value {
    font-size: var(--text-3xl);
    font-weight: var(--font-semibold);
    color: var(--gray-900);
    line-height: var(--leading-tight);
    margin-bottom: var(--space-1);
}

.admin-root .metric-label {
    font-size: var(--text-sm);
    color: var(--gray-700);
    margin-bottom: var(--space-2);
}

.admin-root .metric-change {
    font-size: var(--text-sm);
    font-weight: var(--font-medium);
}

.admin-root .metric-change.success {
    color: var(--success);
}

.admin-root .metric-change.error {
    color: var(--error);
}

.admin-root .metric-change i {
    font-size: 11px;
    margin-right: var(--space-1);
}

/* ========== ADMIN MODULE CARDS ========== */

.admin-root .mgmt-modules-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
    gap: var(--space-6);
    margin-top: var(--space-6) !important;
    padding: var(--space-5) var(--space-6);
}

/* Responsive adjustments for admin grid */
@media (max-width: 991px) {
    .admin-root .mgmt-modules-grid {
        grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
        gap: var(--space-5);
        margin-top: var(--space-5) !important;
        padding: var(--space-4);
    }
}

@media (max-width: 767px) {
    .admin-root .mgmt-modules-grid {
        grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
        gap: var(--space-4);
        margin-top: var(--space-4) !important;
        padding: var(--space-3);
    }
}

@media (max-width: 575px) {
    .admin-root .mgmt-modules-grid {
        grid-template-columns: 1fr;
        gap: var(--space-3);
        margin-top: var(--space-3) !important;
        padding: var(--space-2);
    }
}

/* DE-GREYED 2026-08-10: the tile shipped a COOL card (--gray-100 #F3F4F6 fill,
   --gray-300 #D1D5DB border) on the platform's WARM page (#FAF9F4) — that
   mismatch was the "grey" in the tile area. Now a white card on a warm
   hairline, matching the platform card treatment (.hub-menu-panel et al).
   Radius + padding deliberately unchanged. ONE component, so this lands on
   /admin/users, /admin/settings and every /management/{section} landing at
   once. Hover warms the border and lifts the shadow — NO transform (the old
   translateY(-2px) jumped the whole tile under the cursor). */
.admin-root .mgmt-module-card {
    display: block;
    background: var(--surface, #FFFFFF);
    border: 1px solid var(--line, #E6E2D2);
    border-radius: var(--radius-lg);
    padding: var(--space-5) var(--space-6);
    text-decoration: none;
    transition: border-color 150ms ease, box-shadow 150ms ease;
    position: relative;
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
    cursor: pointer;
}

.admin-root .mgmt-module-card:hover {
    border-color: var(--gold-border, #D9CD8A);
    box-shadow: var(--elevation-2);
}

/* Whole-tile anchors are keyboard targets: a visible ink ring (gold-on-white
   fails 3:1), never the UA default that the card's own border would swallow. */
.admin-root .mgmt-module-card:focus-visible {
    outline: 2px solid var(--ink, #191712);
    outline-offset: 2px;
}

.admin-root .mgmt-module-card .module-header {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

.admin-root .mgmt-module-card .module-icon {
    width: 52px;
    height: 52px;
    border-radius: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    flex-shrink: 0;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.admin-root .mgmt-module-card:hover .module-icon {
    transform: scale(1.05);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.06);
}

/* Role chip - ONE component, four authority tiers (all gold-family + neutral).
   Lives here (shared, in all three bundles) so hub pages like profile.php get
   it too. STRUCTURE is on the base; COLOR comes from a tier modifier derived
   from the role via Hub\Roles::badgeTier(). Base carries the Staff colors as
   the default so an un-tiered .role-badge still renders sanely. */
.role-badge,
.badge[class*="role-badge-"] {
    display: inline-block;
    padding: 0.25rem 0.6rem;
    margin: 0.15rem;
    border-radius: 12px;
    font-size: 0.75rem;
    font-weight: 600;
    white-space: nowrap;
    background: var(--gold-tint, #F5EFD0);
    border: 1px solid var(--gold-border, #D9CD8A);
    color: var(--gold-deep, #6B5D3F);
}

/* THE empty-state cell, platform-wide.

   Every "nothing here yet" row used to carry its own inline padding, at six
   different values across twenty cells (24px x11, 20px x5, 16px x1, 48px
   x1). An inline style attribute outranks any selector, so a register that
   was correctly compact WITH data became a fat slab the moment it had none
   - which is exactly what Richard saw on /admin/sections/substitute_requests
   and read, reasonably, as "not condensed".

   Qualified with the table class so it beats `.register-table td` (0,0,1,1)
   on specificity rather than on source order. Roomier than a data row on
   purpose - an empty state is a message, not a record - but ONE number, and
   nowhere near the 48px it reached at worst. */
.register-table td.table-empty,
.data-table td.table-empty,
td.table-empty {
    padding: 14px 12px;
    text-align: center;
    color: var(--gray-600, #8a8886);
    white-space: normal;
}

/* Google group chips in the users table. Small and quiet by default -
   they are context, not the subject of the row - except for the ones that
   GRANT a role, which get the gold treatment because they are the answer to
   "why does this person have that badge". */
.group-chip {
    display: inline-block;
    padding: 1px 7px;
    margin-right: 4px;
    border-radius: 10px;
    font-size: 11px;
    line-height: 1.5;
    white-space: nowrap;
    vertical-align: middle;
    background: var(--gray-100, #faf9f8);
    border: 1px solid var(--gray-300, #edebe9);
    color: var(--gray-700, #605e5c);
}
.group-chip--grants {
    background: var(--gold-tint, #F5EFD0);
    border-color: var(--gold-border, #D9CD8A);
    color: var(--gold-deep, #6B5D3F);
    font-weight: 600;
}
.group-chip--none {
    background: none;
    border-style: dashed;
    color: var(--gray-600, #8a8886);
}

/* "+2" pill in the users table Role cell: the count of roles collapsed
   behind the highest one. Deliberately NOT a reused .role-badge--basic -
   it is a counter, not a role, and must not read as one more role. Measured
   29.4px for "+1" and 36.8px for the "+13" worst case. */
.role-more {
    display: inline-block;
    padding: 0.25rem 0.45rem;
    border-radius: 12px;
    font-size: 0.7rem;
    font-weight: 700;
    line-height: 1;
    white-space: nowrap;
    background: var(--gray-200, #f3f2f1);
    border: 1px solid var(--gray-300, #edebe9);
    color: var(--gray-700, #605e5c);
}

/* Role membership editor - the "N people" cell on Admin > Sections opens
   to show who actually holds the role, with add/remove in place. Lives in
   a table cell, so everything here has to survive a narrow track. */
.role-members__summary {
    cursor: pointer;
    font-weight: var(--font-medium, 500);
    color: var(--gold-text, #75692E);
}
.role-members__list {
    list-style: none;
    margin: 8px 0 0;
    padding: 0;
    border-top: 1px solid var(--gray-200, #f3f2f1);
}
.role-members__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 4px 0;
    border-bottom: 1px solid var(--gray-200, #f3f2f1);
}
.role-members__who {
    color: var(--gray-900, #201f1e);
}
.role-members__src {
    font-size: var(--text-xs, 12px);
    color: var(--gray-600, #8a8886);
    white-space: nowrap;
}
.role-members__add {
    display: flex;
    gap: 6px;
    align-items: center;
    margin-top: 8px;
}
/* Same HEIGHT as the Add button beside it, not just the same centre line. The
   button is hub-btn--sm (28px, --btn-h-sm); .filter-select sets no height at
   all and derives ~37px from 8px+8px padding + 2px border + a 14px line box,
   so the button rendered ~9px short of the dropdown. This row is a dense
   in-table action, so the select meets the 28px button rather than the button
   growing to 36px. The vertical padding has to go with it or it fights the
   height; 26px of content box is ample for 14px text.

   The touch block below is not optional. `.hub-btn`'s touch lift is
   `(max-width: 1024px), (pointer: coarse)` and applies to --sm too, so the Add
   button is 44px there. Pinning the select at a bare 28px would turn today's
   ~7px mismatch into a 16px one on every phone and every <=1024px window. */
.role-members__add .filter-select {
    flex: 1;
    min-width: 0;
    height: var(--btn-h-sm, 28px);
    padding-top: 0;
    padding-bottom: 0;
}
@media (max-width: 1024px), (pointer: coarse) {
    .role-members__add .filter-select {
        height: var(--btn-h-touch, 44px);
    }
}

/* The Role cell in the users table is a button that opens the full role
   editor. It holds one .role-badge per role the user holds - people can be
   e.g. Roster Coordinator AND Staff - so it WRAPS instead of clipping the
   second badge. Resets the UA button chrome: only the badges should show. */
.role-cell-btn {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 2px;
    width: 100%;
    padding: 0;
    background: none;
    border: 0;
    font: inherit;
    color: inherit;
    text-align: center;
    cursor: pointer;
}
.role-cell-btn .role-badge {
    margin: 0;               /* the flex gap owns the spacing */
}
.role-cell-btn:focus-visible {
    outline: 2px solid var(--gold-deep, #6B5D3F);
    outline-offset: 2px;
    border-radius: 12px;
}

.role-badge--executive {
    background: var(--gold-deep, #6B5D3F);   /* deep district gold; white text ~5.8:1 AA */
    color: #FFFFFF;
    border: 0;
}

.role-badge--administrator {
    background: var(--gold, #C4B454);   /* district gold; black text ~8.9:1 AA */
    color: #000000;
    border: 0;
}

.role-badge--staff {
    background: var(--gold-tint, #F5EFD0);   /* gold tint chip */
    color: var(--gold-deep, #6B5D3F);
    border: 1px solid var(--gold-border, #D9CD8A);
}

.role-badge--basic {
    background: #F1EFE8;   /* neutral for unassigned/unknown */
    color: #444441;
    border: 1px solid #D3D1C7;
}

/* Student = a separate population, deliberately NON-gold (indigo) so a student
   never reads as staff. Tint for lists; solid for emphasis/hero. */
.role-badge--student {
    background: #EEEDFE;
    color: #3C3489;   /* #3C3489 on #EEEDFE ~8:1 AA */
    border: 1px solid #CECBF6;
}

.role-badge--student-solid {
    background: #3C3489;
    color: #FFFFFF;   /* white on #3C3489 ~9:1 AA */
    border: 0;
}

/* Status chips - ONE implementation for every register/list, present in both
   the hub and management bundles. Brief recipe: semantic TINT background +
   DARK semantic text (white-on-semantic fills are retired); pending is the
   gold tint. 8px chip radius per the shape scale. Palette lives in the token
   layer (enterprise-design-system.css --chip-*); every pair computes >=4.5:1
   at the chip's 12px text. */
.status-chip {
    display: inline-block;
    padding: 3px 10px;
    border-radius: 8px;
    font-size: 12px;
    font-weight: 600;
    line-height: 1.4;
    white-space: nowrap;
    background: var(--chip-neutral-bg, #F1EFE8);
    border: 1px solid var(--chip-neutral-border, #D3D1C7);
    color: var(--chip-neutral-text, #444441);
}

.status-chip--pending {
    background: var(--gold-tint, #F5EFD0);
    border-color: var(--gold-border, #D9CD8A);
    color: var(--chip-pending-text, #75692E);
}

.status-chip--success {
    background: var(--chip-success-bg, #E7F0E9);
    border-color: var(--chip-success-border, #C4DCCB);
    color: var(--chip-success-text, #387047);
}

.status-chip--danger {
    background: var(--chip-danger-bg, #F9EAEB);
    border-color: var(--chip-danger-border, #EBC8CA);
    color: var(--chip-danger-text, #A4262C);
}

.status-chip--warning {
    background: var(--chip-warning-bg, #F8EFDE);
    border-color: var(--chip-warning-border, #EAD6AE);
    color: var(--chip-warning-text, #8A5A17);
}

.status-chip--neutral {
    background: var(--chip-neutral-bg, #F1EFE8);
    border-color: var(--chip-neutral-border, #D3D1C7);
    color: var(--chip-neutral-text, #444441);
}

/* Register tables - ONE implementation for the management/admin list pages
   (POs, checks, work orders, incidents, vehicles, assets, fuel tracker),
   replacing the per-page inline-styled clones. Wrap in
   .table-scroll.table-scroll--panel; dense registers additionally take
   .table-cards + data-label per cell for the phone card transform. The
   per-table minimum width rides on --table-min-width (a custom property,
   set inline on the table tag) so the card recipe's min-width:0 still wins
   below 640px - a literal inline min-width would beat the recipe. */
.register-table {
    width: 100%;
    border-collapse: collapse;
    min-width: var(--table-min-width, 0);
}

/* COMPACT + NO-WRAP density (platform law, 2026-07-29 user mandate "all
   tables compact with no wrapping... no subtexts"): register rows are a
   single line - 7px/12px padding, never wrap. Overflow is handled by the
   .table-scroll container (horizontal scroll), never by wrapping a cell to
   two lines. Card mode (<=640px, _responsive.css) resets white-space so
   values reflow inside their card instead of clipping. */
.register-table th {
    text-align: left;
    background: var(--gray-100, #faf9f8);
    border-bottom: 1px solid var(--gray-300, #edebe9);
    padding: 7px 12px;
    font-weight: 600;
    white-space: nowrap;
}

.register-table td {
    padding: 7px 12px;
    border-bottom: 1px solid var(--gray-200, #f3f2f1);
    white-space: nowrap;
    height: auto;
}

.register-table tbody tr:last-child td {
    border-bottom: 0;
}

/* The bordered panel look the registers share; a modifier so plain
   .table-scroll (bare overflow container) stays available. */
.table-scroll--panel {
    border: 1px solid var(--gray-300, #edebe9);
    border-radius: 8px;
    background: var(--gray-50, #fff);
}

/* ============================================================
   FROZEN HEADER + BOUNDED SCROLL for panel registers (>=641px).
   GATED so the <=640px .table-cards flip (shared/_responsive.css)
   is untouched — ungated, the tbody box-shadows + sticky leak
   onto the phone card state. Live-measured problem: register
   tables are border-collapse:collapse with position:relative th
   and the panel has max-height:none, so a tall register (e.g.
   student_directory, 165 rows) scrolls the WHOLE DOCUMENT and the
   header/tabs/toolbar/footer ride away. Fix couples four things on
   the SHARED primitives: (1) move off collapse so sticky+z-index
   obey, (2) re-express the HORIZONTAL row rules (these tables draw
   no vertical column separators) as INSET box-shadows — zero
   layout change, never doubles width, invents no vertical rules,
   (3) freeze an OPAQUE thead on its CURRENT fill, (4) bound the
   PANEL with max-height so it — not the document — scrolls. */
@media (min-width: 641px) {
    /* (1) reliability: separate borders so z-index on th obeys and
       the header keeps painting its rules while stuck. */
    .table-scroll--panel table.register-table {
        border-collapse: separate;
        border-spacing: 0;
    }

    /* (2) pixel parity: row rules become inset shadows. Horizontal
       only — no -1px x — so no phantom column separators appear. */
    .table-scroll--panel table.register-table > tbody > tr > td {
        border: 0;
        box-shadow: inset 0 -1px 0 0 var(--gray-200, #f3f2f1);
    }
    .table-scroll--panel table.register-table > tbody > tr:last-child > td {
        box-shadow: none;   /* mirror the original last-row: no bottom rule */
    }

    /* (3) freeze the header. REUSE the current opaque fill (--gray-100)
       so appearance is unchanged; th carries its own bottom rule so
       the header edge stays drawn while frozen. Do NOT shrink the
       font (vision-needs staff); do NOT add `left` stickiness — the
       thead rides the same scroller, so columns stay aligned. */
    .table-scroll--panel table.register-table > thead > tr > th {
        position: sticky;
        top: 0;
        z-index: 2;                                   /* above tbody td (auto) */
        border: 0;
        background: var(--gray-100, #faf9f8);         /* OPAQUE, alpha 1 */
        box-shadow: inset 0 -1px 0 0 var(--gray-300, #edebe9);
        height: 44px;                                 /* min-height is ignored on table-cell; height acts as a floor */
    }

    /* (4) bound the panel = the load-bearing change. max-height NOT
       height (short tables stay natural). vh fallback FIRST, then
       dvh (mobile-chrome safe). min-height floor so short/zoomed
       viewports degrade to page scroll, not a 3-row panel.
       scroll-padding-top clears the frozen header on keyboard focus.
       12px real clearance below to the pager/footer. */
    .table-scroll--panel {
        overflow-x: auto;
        overflow-y: auto;
        /* SCROLL TRAP, fixed. Richard, 2026-08-24: "if my mouse hovers over the
           who can use table, it kills scroll for the page".

           `overscroll-behavior: contain` blocks wheel CHAINING to the page. On
           a panel that cannot scroll at all - the access list measured a 200px
           box around 198px of content - the wheel was swallowed and the page
           sat still. It bit the scrollable panels too, which stopped dead at
           their last row instead of handing the rest of the gesture back.

           Containment is still wanted on X: a horizontal overscroll on a wide
           table is what triggers the browser's back-swipe gesture. So contain
           the axis that needs it and let the vertical chain. */
        overscroll-behavior-x: contain;
        overscroll-behavior-y: auto;
        /* Was 200px, which padded a two-row table out to 200 and left ~140px of
           empty panel under it. The max-height below still caps long tables;
           short ones now size to their content. */
        min-height: 0;
        /* max(320px, ...): the 21rem fallback is a GUESS (the measuring
           script lives only in the Blade console shell), and on a landscape
           phone - 375px of viewport height - the guess eats the whole screen:
           the calc lands near zero and the register renders as a sliver of
           frozen header with no rows, indistinguishable from a failed page
           load. 320px keeps roughly eight rows tappable; a viewport tall
           enough for the full calc is untouched, because max() only engages
           when the calc is the smaller side. */
        max-height: max(320px, calc(100vh  - var(--table-chrome, 21rem)));
        max-height: max(320px, calc(100dvh - var(--table-chrome, 21rem)));
        scroll-padding-top: var(--register-thead-h, 48px);
        margin-bottom: 12px;
    }
}

/* (5) --table-chrome = everything above the panel + pager + footer +
   12px clearance. student_directory-like ~326px, so the 21rem(336px)
   default covers it. The ResizeObserver (layout) overrides this with
   a MEASURED px value; the :has() bumps are the no-JS fallback for
   tabbed/paginated registers that reserve more. */
.admin-register { --table-chrome: 21rem; }
.admin-register:has(.status-pills),
.admin-register:has(.pagination) { --table-chrome: 26rem; }

/* Click-to-copy address in a dense register cell. Renders as monospace TEXT,
   not a button: the table shows the local part ("39.harper.beem") and the
   clipboard gets the full address. Keyboard-reachable because it is a real
   <button> (documented in check-button-primitive.py's EXEMPT_CLASSES — it
   carries no button chrome by design). */
.sd-copy-login {
    border: 0;
    background: transparent;
    padding: 0;
    font: inherit;
    font-family: ui-monospace, Menlo, Consolas, monospace;
    color: inherit;
    cursor: pointer;
    text-align: left;
}
.sd-copy-login:hover { text-decoration: underline; }
.sd-copy-login:focus-visible { outline: 2px solid var(--ink, #191712); outline-offset: 2px; }

/* ============================================================
   HUB FILTER BAR + MULTISELECT (client-side register filtering).
   ONE toolbar (.hub-filterbar) replacing the old two-row toolbar:
   live search + multiselect facets + boolean toggles + active
   chips + live count + Clear all. Filtering toggles [hidden] on
   the SAME <tr> (no separate DOM). Tokens are platform tokens
   (gold ramp / --ink / --surface / --line) — no new literals.
   ============================================================ */
.table-scroll--panel table.register-table > tbody > tr[hidden] { display: none !important; }

.hub-filterbar {
    position: sticky;
    top: 0;
    z-index: 5;                                  /* above the frozen thead (z-index:2) */
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-bottom: 16px;
    padding: 10px 12px;
    background: var(--surface, #FFFFFF);         /* OPAQUE so the popover/thead never bleed through */
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
}
.hub-filterbar__search {
    flex: 1 1 240px;
    min-width: 200px;
    height: var(--btn-h-md, 36px);
    padding: 0 12px;
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 8px;
    background: var(--surface, #FFFFFF);
    color: var(--ink, #191712);
    font-size: 14px;
}
.hub-filterbar__search::placeholder { color: var(--ink-mute, #6F6A5C); }
/* PHONE DISCLOSURE (2026-09-07, Richard: "mobile trip history view is not good").
   At >=641px the bar is what it always was - one wrapped row of inline controls:
   the trigger does not exist and the group is display:contents, so its children
   stay DIRECT flex items of the bar (measured at 768 and 1200 before/after: every
   pre-existing control at the same x/y/w/h). The <=640px block further down
   flips both.
   The count and the chips live OUTSIDE the group in the DOM (the count is an
   aria-live region and must never sit inside a display:none box); `order` puts
   Clear All and the chips back after the count so the desktop row still reads
   "... [spacer] Showing N of M  Clear All" exactly as before. The count is not
   focusable, so DOM order, focus order and visual order still agree. */
.hub-filterbar__disclose { display: none; }
.hub-filterbar__more { display: contents; }
.hub-filterbar__clear, .hub-filterbar__chips { order: 1; }

/* Export destination tiles (EXP-02) + scope picker (EXP-03). Tiles are
   buttons that retarget ONE form -- absent entirely for platforms the user
   cannot reach ("gated visibly"), never present-and-disabled. */
.exp-tiles {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin-bottom: 10px;
}
.exp-tile {
    position: relative;
    display: grid;
    gap: 4px;
    text-align: left;
    width: min(300px, 100%);
    padding: 14px 16px;
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
    background: var(--surface, #FFFFFF);
    color: var(--ink, #191712);
    cursor: pointer;
}
.exp-tile span { font-size: 12px; color: var(--ink-mute, #6F6A5C); }
.exp-tile__radio {
    /* Visually hidden, still focusable: keyboard users get native radio
       semantics, sighted users get the tile. */
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
}
.exp-tile:has(.exp-tile__radio:checked) {
    background: var(--gold-tint, #F5EFD0);
    border-color: var(--gold-border, #D9CD8A);
}
.exp-tile:has(.exp-tile__radio:checked) strong { color: var(--gold-deep, #6B5D3F); }
.exp-tile:has(.exp-tile__radio:focus-visible) {
    outline: 2px solid var(--ink, #191712);
    outline-offset: 2px;
}
/* EXP-01: the scope uses the full content width -- grades and the student
   picker side by side at desktop, stacking under 900px. */
/* Scope on the left, Renaissance settings on the right (Richard,
   2026-08-19). The settings pane is the thing being filled in against the
   grade choices, so it belongs beside them, not below the fold. */
.exp-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(320px, 460px);
    gap: 20px;
    align-items: start;
}
/* The details carried an inline margin-top for when it sat BELOW the form;
   as a grid column that just pushes it out of line with the scope panel. */
.exp-layout > details { margin-top: 0 !important; }
/* One column once the two panes would fight: the student list needs room,
   and the homeroom grid is itself a wrapping grid. */
@media (max-width: 1200px) {
    .exp-layout { grid-template-columns: 1fr; }
    .exp-layout > details { margin-top: 20px !important; }
}

/* Students BELOW grades, not beside them - they read as a narrowing of the
   grade choice above, and side by side the pair squeezed both. */
.exp-scope {
    display: grid;
    grid-template-columns: 1fr;
    gap: 16px;
    align-items: start;
}
.exp-scope fieldset {
    border: 1px solid var(--gray-300, #edebe9);
    border-radius: 6px;
    padding: 12px;
    margin: 0;
}
.exp-student-list {
    display: grid;
    gap: 2px;
    max-height: 320px;
    overflow-y: auto;
}
.exp-student {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    padding: 3px 4px;
    border-radius: 4px;
}
.exp-student:hover { background: var(--gray-100, #faf9f8); }

/* Substitute-request row modal (SR-21) — a native <dialog>, so focus trap,
   Esc-to-close and the backdrop come from the platform, not from JS. */
/* Shown when an emailed ?open=<id> link resolves to nothing - the request
   was cancelled or removed since the mail went out. Uses the existing
   warning-chip tokens; no new colour. */
.sr-deeplink-note {
    margin: 0 0 12px;
    padding: 10px 12px;
    border-radius: 8px;
    font-size: 13px;
    background: var(--chip-warning-bg);
    border: 1px solid var(--chip-warning-border);
    color: var(--chip-warning-text);
}

/* Level legend under "Who Can Use This Section" - one line, not a paragraph. */
.sec-legend {
    margin: 0 0 10px;
    font-size: 13px;
    color: var(--ink-mute, #6F6A5C);
    line-height: 1.6;
}
.sec-legend strong { color: var(--ink, #191712); }

/* PAGE HEADER - icon, name + metadata on one line, description beneath.
   Replaces a three-line inline-styled block whose icon aligned to the top of
   the stack instead of to the title. Named generically so other section-style
   headers can adopt it. */
.page-head {
    display: flex;
    align-items: center;
    gap: 14px;
    margin: 0 0 18px;
}
.page-head__icon {
    flex: none;
    width: 40px;
    height: 40px;
    font-size: 40px;          /* icon-font glyphs size off this, SVGs off the box */
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* The section's own accent, the way .section-title-icon did it - my first
       pass hard-set --ink-mute and flattened every section to the same grey.
       Richard, 2026-08-24: "svg too small though imo" - it was also the wrong
       colour. Falls back to gold, then to the same grey the old rule used. */
    color: var(--section-accent, var(--gold-text, #75692E));
}
/* Lucide ships inline SVG inside the span; without this it keeps its own
   intrinsic size and ignores the box it was given. */
.page-head__icon svg {
    width: 100%;
    height: 100%;
}
.page-head__body { min-width: 0; }
.page-head__line {
    display: flex;
    align-items: baseline;
    gap: 12px;
    flex-wrap: wrap;
}
.page-head__title {
    margin: 0;
    font-size: 26px;
    line-height: 1.2;
}
.page-head__meta {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12px;
    color: var(--ink-mute, #6F6A5C);
    background: var(--gray-100, #faf9f8);
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 6px;
    padding: 2px 7px;
    white-space: nowrap;
}
.page-head__desc {
    margin: 3px 0 0;
    color: var(--ink-mute, #6F6A5C);
    font-size: 14px;
    max-width: 70ch;
}

/* SECTION SETTINGS TABLE (Admin > Sections > a section).

   Setting | Current | control, one row each - the Google Admin console shape,
   which is what Richard asked for: "Admin need to see lots of info quickly...
   Could mirror Google probably be best". It replaces three stacked full-width
   cards that carried one sentence each (measured 279px for three facts).

   The SHAPE is borrowed; the surface is not. Google's blue links and Material
   type would fight the gold token system and the colour/title-case gates. */
.sec-settings {
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 10px;
    background: var(--surface, #FFFFFF);
    margin: 0 0 20px;
    overflow: hidden;
}
.sec-settings__row {
    display: grid;
    grid-template-columns: 190px minmax(0, 1fr) auto;
    align-items: center;
    gap: 16px;
    padding: 10px 16px;
    border-top: 1px solid var(--line, #E6E2D2);
}
.sec-settings__row:first-child { border-top: 0; }
.sec-settings__row--head {
    padding: 7px 16px;
    background: var(--gray-100, #faf9f8);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ink-mute, #6F6A5C);
}
.sec-settings__label { font-weight: 600; font-size: 14px; }
.sec-settings__value {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    font-size: 14px;
    min-width: 0;
}
.sec-settings__control { justify-self: end; }
.sec-settings__hint { color: var(--ink-mute, #6F6A5C); font-size: 13px; }
.sec-settings__opt {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 14px;
    cursor: pointer;
}
/* The inline-flex above was inert. `.admin-root label:not(.form-check-label)
   :not(.pkg-filter-label):not(.hub-toggle)` scores (0,4,1) - each :not()
   argument counts - and beats a bare .sec-settings__opt (0,1,0), so these
   labels rendered display:inline-block: the checkbox sat on the text baseline
   instead of centred, and the 6px gap did nothing. The same rule's
   margin-bottom:4px also made the label's MARGIN box 4px taller than the label
   inside .sec-settings__value's align-items:center, putting the Save button's
   centre ~2px below the checkbox labels'. Matching the :not() chain and adding
   the class reaches (0,5,1) and wins on real specificity, not !important.
   Only display and margin-bottom are re-asserted here so that
   .sec-settings__opt--dev's gap:8px (0,1,0) still wins on source order. */
.admin-root label.sec-settings__opt:not(.form-check-label):not(.pkg-filter-label):not(.hub-toggle) {
    display: inline-flex;
    margin-bottom: 0;
}
.sec-settings__opt--dev { gap: 8px; }
/* STACKED: a label that holds a text field rather than a checkbox. The
   field takes the full row and the hint sits UNDER it, instead of running
   on inline after the input and breaking mid-sentence wherever the column
   happens to end. Same :not() chain as the rule above, for the same
   reason: a bare modifier at (0,1,0) loses to the admin-root label rule at
   (0,4,1), which is exactly why these three labels had been carrying an
   inline style="display:block" to get by. */
.admin-root label.sec-settings__opt--stack:not(.form-check-label):not(.pkg-filter-label):not(.hub-toggle) {
    display: block;
    cursor: default;
}
/* One rule, two selectors: the field and the hint stack the same way. A
   selector LIST on purpose. Written as two rules, the first selector ended
   in the form-input class followed by an opening brace - the exact text
   ControlRadiusTokenTest searches this sheet for to find the real
   form-input block 650 lines below. A comma is not a brace. And this
   comment must not spell that text out either: comments are not stripped
   by a substring search, so a comment quoting it would be the match. */
.sec-settings__opt--stack > .form-input,
.sec-settings__opt--stack > .sec-settings__hint { display: block; margin-top: 4px; }
/* The visibility form lays its two cells into the ROW's grid rather than
   making a nested box - otherwise this row stops lining up with the two
   above it. */
.sec-settings__form { display: contents; }

@media (max-width: 700px) {
    .sec-settings__row { grid-template-columns: 1fr; gap: 6px; }
    .sec-settings__control { justify-self: start; }
    .sec-settings__row--head { display: none; }
}

/* ONE REQUEST, ONE PAGE (Richard, 2026-08-20: "there needs to be a page
   dedicated to that info... probably just take out the modal and make it a
   page"). Facts on the left, the two decision forms beside them, so an
   emailed link lands on something readable rather than a dialog over a
   register. */
/* Richard, 2026-08-20: "I feel like we are wasting some space here".
   Measured at 1568: the Request panel was 930px wide holding five short facts
   that need about 350, with the whole left column empty below it while the
   action panels stacked in a 340px rail. Two columns was the wrong split for
   this content.

   Now the facts run ACROSS the top in a self-sizing row, and the things you
   act on sit beneath them side by side. The page fills its width because the
   content is laid along it, not because anything was stretched. */
.sr-page {
    display: block;
}
.sr-page__actions {
    display: grid;
    /* TWO columns, explicitly. auto-fit with a 320px minimum produced FOUR
       tracks in a 1290px pane, so Cover rendered as a lonely quarter-width
       card. There are only ever two things to act on here - name the cover,
       or decide - so the layout should say two. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 16px;
    align-items: start;
    margin-top: 16px;
}
@media (max-width: 900px) {
    .sr-page__actions { grid-template-columns: 1fr; }
}
/* The withdraw disclosure spans the row: it is the rare, destructive one and
   should not sit shoulder to shoulder with Approve. */
.sr-page__withdraw { grid-column: 1 / -1; }
.sr-page__withdraw > summary {
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    gap: 9px;
}
.sr-page__withdraw > summary::-webkit-details-marker { display: none; }
.sr-page__withdraw > summary::before {
    content: '';
    width: 7px;
    height: 7px;
    flex: none;
    border-right: 2px solid var(--ink-mute, #6F6A5C);
    border-bottom: 2px solid var(--ink-mute, #6F6A5C);
    transform: rotate(-45deg);
    transition: transform 120ms ease;
}
.sr-page__withdraw[open] > summary::before { transform: rotate(45deg); }
.sr-page__withdraw:not([open]) > summary { margin-bottom: 0; }

.sr-page__head {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin: 0 0 16px;
}
.sr-page__panel {
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
    padding: 16px;
    background: var(--surface, #FFFFFF);
}
/* No adjacent-sibling margin: .sr-page__actions is a grid and its gap owns
   the spacing. The old rule was written when these panels stacked, and in a
   grid it pushed the SECOND card 16px lower than the first - measured, Cover
   at y=307 and Decision at y=323. */
.sr-page__panel-title {
    margin: 0 0 12px;
    font-size: 14px;
    font-weight: 600;
}
/* Facts along the row, not down the page. auto-fit means five short facts
   spread across a wide pane and reflow to fewer columns as it narrows, with
   no breakpoint to maintain. */
.sr-page__facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 14px 24px;
    margin: 0;
    font-size: 14px;
}
.sr-page__facts dt {
    color: var(--ink-mute, #6F6A5C);
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 2px;
}
.sr-page__facts dd { margin: 0; }
/* Each dt+dd is one cell: the pair has to be wrapped to sit together in the
   grid, so the pairs are their own blocks. */
.sr-page__fact { min-width: 0; }
.sr-page__form { display: grid; gap: 8px; }
/* Buttons in these forms size to their text; the grid would otherwise stretch
   them the full column, which is how Save Request ended up 758px wide. */
.sr-page__form .hub-btn { justify-self: start; }
/* Delete rides the title line, pushed to the far right. */
/* ONE auto margin, carried by the GROUP.
   Three buttons each carrying margin-left:auto each claim the free space in
   turn, so they end up strung across the header - measured 2026-09-10 at
   x=1012, 1435 and 1840 on a 1920 screen - instead of sitting together. */
.sr-page__head-actions {
    margin-left: auto;
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
}
.sr-page__head-actions form { display: inline-flex; margin: 0; }
/* Any child inside the group must NOT take an auto margin of its own. */
.sr-page__head-actions .sr-page__head-action { margin-left: 0; }
/* Retained for a lone trailing action elsewhere. */
.sr-page__head-action { margin-left: auto; }
.sr-page__label { font-size: 12px; color: var(--ink-mute, #6F6A5C); }
/* Belt: #sr-within-block carries .sr-page__form for its layout, and that
   class's display:grid (0,1,0) beats the UA [hidden] sheet - so without this
   the within fields would show whether the box is ticked or not. Same fix as
   the register rows above. */
#sr-within-block[hidden] { display: none !important; }
/* Same belt, same reason: .sr-page__form's display:grid beats [hidden]. */
#sr-within-single[hidden], #sr-within-periods[hidden] { display: none !important; }
.sr-page-banner {
    padding: 10px 14px;
    border-radius: 8px;
    margin: 0 0 14px;
    font-size: 14px;
}
.sr-page-banner--ok {
    background: var(--chip-success-bg);
    border: 1px solid var(--chip-success-border);
    color: var(--chip-success-text);
}
.sr-page-banner--bad {
    background: var(--chip-danger-bg);
    border: 1px solid var(--chip-danger-border);
    color: var(--chip-danger-text);
}

/* CENTRE MODAL DIALOGS.
   A <dialog> opened with showModal() is centred by the UA's `margin: auto`
   against its `inset: 0`. The reset at the top of this platform
   (`*, ::before, ::after { margin: 0 }`) overrides that, so every modal
   dialog was pinned to the TOP-LEFT CORNER - measured at (0,0), 560x522 in
   a 1366x900 viewport. Richard, 2026-08-20: "the open modal is not located
   very well".
   Fixed on `dialog:modal` rather than on .sr-modal so the next dialog anyone
   adds does not inherit the bug; .sr-modal[open] repeats it as a fallback
   for engines without :modal. */
dialog:modal,
.sr-modal[open] {
    margin: auto;
}

.sr-modal {
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
    padding: 20px;
    width: min(560px, calc(100vw - 32px));
    background: var(--surface, #FFFFFF);
    color: var(--ink, #191712);
}
.sr-modal::backdrop { background: rgba(0, 0, 0, 0.45); }
.sr-modal__head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 12px;
}
.sr-modal__facts {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 4px 14px;
    margin: 0 0 16px;
    font-size: 13px;
}
.sr-modal__facts dt { color: var(--ink-mute, #6F6A5C); }
.sr-modal__facts dd { margin: 0; }
.sr-modal__assign, .sr-modal__decide {
    display: grid;
    gap: 8px;
    border-top: 1px solid var(--line, #E6E2D2);
    padding-top: 12px;
    margin-top: 12px;
}
.sr-modal__input {
    height: var(--btn-h-md, 36px);
    padding: 0 10px;
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 8px;
    background: var(--surface, #FFFFFF);
    color: var(--ink, #191712);
    font-size: 14px;
}
.sr-modal__within {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
}
@media (pointer: coarse) {
    .sr-modal__input { height: var(--btn-h-touch, 44px); }
}

/* Staff Directory section tabs. Buttons, not links: they drive the Department
   facet client-side rather than reloading, so the filter bar's count stays the
   authority on what is shown. Wraps rather than scrolls -- a tab row that
   scrolls hides sections, and hidden sections are the ones nobody checks. */
.staff-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 12px;
}
.staff-tab[aria-pressed="true"] {
    background: var(--gold-tint, #F5EFD0);
    border-color: var(--gold-border, #D9CD8A);
    color: var(--gold-deep, #6B5D3F);
    font-weight: 700;
}
.staff-tab__count {
    margin-left: 6px;
    font-size: 12px;
    color: var(--ink-mute, #6F6A5C);
}
.staff-tab[aria-pressed="true"] .staff-tab__count { color: var(--gold-deep, #6B5D3F); }

/* TBL-07 / ASSET-03 — a register cell that must not let ONE bad record dictate
   the table's width. The no-wrap density law means a single over-long value sets
   the column's intrinsic minimum: a 65-character string typed into an asset-tag
   field stretched that column to 560px, nearly half the table, and pushed the
   whole register into a horizontal scrollbar.

   The full value stays reachable via the cell's title attribute, so nothing is
   lost — it is truncated, not hidden. Per SD-27's lesson, only apply this to
   cells holding TEXT: a truncated cell must never contain a control, or the
   control becomes unreachable. */
.register-table td.cell-clamp {
    max-width: var(--cell-clamp, 14ch);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Date-range facet (TBL-02). Heights track --btn-h-md so the range legs sit on
   the same axis as the search box and the multiselect triggers — the filter row
   is one measured control height, per the locked 36/44 rule. */
.hub-daterange {
    display: flex;
    align-items: center;
    gap: 6px;
}
.hub-daterange__leg {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.hub-daterange__legend {
    font-size: 13px;                     /* was 12px on --ink-mute: the brief keeps the muted tone for >=13px only */
    color: var(--ink-soft, #4A4638);
    white-space: nowrap;
}
.hub-daterange input[type="date"] {
    height: var(--btn-h-md, 36px);
    padding: 0 8px;
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 8px;
    background: var(--surface, #FFFFFF);
    color: var(--ink, #191712);
    font-size: 14px;
}
.hub-filterbar__spacer { flex: 1 1 auto; }
.hub-filterbar__count { font-size: 13px; color: var(--ink-mute, #6F6A5C); white-space: nowrap; }
.hub-filterbar__chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; width: 100%; }
.hub-filterbar__chips:empty { display: none; }

/* Active-filter chip (removable). Gold-tint fill per the platform selected state. */
.hub-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: var(--btn-h-sm, 28px);
    padding: 0 6px 0 10px;
    border-radius: 99px;
    border: 1px solid var(--gold-border, #D9CD8A);
    background: var(--gold-tint, #F5EFD0);
    color: var(--ink, #191712);
    font-size: 12px;
    white-space: nowrap;
}
/* The painted circle stays 18px; the TAP area is extended to 34px by the
   transparent ::after in the touch block further down. At a bare 18x18 this
   was under a fifth of the required 44x44, and the chips sit shoulder to
   shoulder once they wrap on a phone, so removing one filter was a coin flip
   between it, its neighbour, and nothing at all. */
.hub-chip__x {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--ink-mute, #6F6A5C);
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
}
.hub-chip__x:hover { color: var(--ink, #191712); background: var(--gold-border, #D9CD8A); }

/* Boolean toggle (e.g. "Needs card only"): a pressed .hub-btn. On == gold fill,
   black text (brand law), matching .filter-pill--on. */
.hub-toggle[aria-pressed="true"] {
    background: var(--btn-primary-bg, #C4B454);
    border-color: transparent;
    color: var(--btn-primary-text, #000000);
    font-weight: 600;
}

/* Multiselect facet */
.hub-multiselect { position: relative; display: inline-block; }
.hub-multiselect__trigger {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    height: var(--btn-h-md, 36px);
    padding: 0 12px;
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 8px;
    background: var(--surface, #FFFFFF);
    color: var(--ink, #191712);
    font-size: 14px;
    cursor: pointer;
    white-space: nowrap;
}
.hub-multiselect__trigger[aria-expanded="true"] { border-color: var(--gold-press, #A89940); }
.hub-multiselect__trigger-caret {
    width: 0; height: 0;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid var(--ink-mute, #6F6A5C);   /* CSS triangle — no icon font/glyph */
}
/* ONE count pill: the facet trigger's and the phone Filters trigger's. 12/700 is
   the platform micro-label floor (was 11/600). */
.hub-multiselect__badge,
.hub-filterbar__badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 99px;
    background: var(--gold, #C4B454);                  /* solid gold, black count (brand law) */
    border: 0;
    color: var(--btn-primary-text, #000000);
    font-size: 12px;
    font-weight: 700;
}
.hub-multiselect__badge[hidden],
.hub-filterbar__badge[hidden] { display: none; }   /* hide the "0" when nothing is selected */
.hub-multiselect__panel {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    z-index: 40;                                 /* above the sticky filter bar + frozen thead */
    min-width: 220px;
    max-height: 320px;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 4px;
    background: var(--surface, #FFFFFF);
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
    box-shadow: 0 1px 2px rgba(23, 21, 15, 0.06), 0 8px 24px rgba(23, 21, 15, 0.08);
}
.hub-multiselect__panel[hidden] { display: none; }
.hub-multiselect__panel--up { top: auto; bottom: calc(100% + 4px); }
.hub-multiselect__option {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    min-height: 40px;
    border-radius: 8px;
    color: var(--ink, #191712);
    font-size: 14px;
    cursor: pointer;
    user-select: none;
}
.hub-multiselect__option:hover,
.hub-multiselect__option[data-active="true"] { background: var(--gold-tint, #F5EFD0); }
.hub-multiselect__option input[type="checkbox"] {
    width: 16px;
    height: 16px;
    accent-color: var(--gold, #C4B454);          /* gold box, white check */
    margin: 0;
    flex-shrink: 0;
    pointer-events: none;                         /* clicks pass to the option; JS is the single source */
}

/* Neutral-dark focus ring (gold-on-white fails 3:1). 44px tap targets on touch.
   box-shadow:none suppresses the platform .hub-btn gold ring on the trigger +
   toggle so every filter control shows the same accessible ink outline. */
.hub-filterbar__search:focus-visible,
.hub-filterbar__disclose:focus-visible,
.hub-multiselect__trigger:focus-visible,
.hub-multiselect__option:focus-visible,
.hub-toggle:focus-visible,
.hub-chip__x:focus-visible {
    outline: 2px solid var(--ink, #191712);
    outline-offset: 2px;
    box-shadow: none;
}
/* Same condition as the .hub-btn touch lift above, not `(pointer: coarse)`
   alone. The two were written to different breakpoints, so a FINE-pointer
   window narrower than 1025px rendered 44px buttons (.hub-btn lifts at
   `(max-width: 1024px), (pointer: coarse)`) inside a 36px bar - the mismatch
   running the wrong way. This makes both halves agree on when that height
   changes. Deliberately NOT also forcing .hub-filterbar .hub-btn to 36px on
   desktop: the bar's buttons are .hub-btn--sm, and scripts/check-density.py
   fails any --sm button computing over 30px. */
@media (max-width: 1024px), (pointer: coarse) {
    .hub-multiselect__option { min-height: 44px; }
    .hub-multiselect__trigger, .hub-filterbar__search,
    .hub-daterange input[type="date"] { height: var(--btn-h-touch, 44px); }
}
@media (max-width: 640px) {
    /* NOT re-stuck (position:static since ee3ba85): the staff nav.navbar is
       sticky top:0 z-index 10000 against this bar's z 5 and the console
       scroller is .admin-main - two shells, two top offsets, for a 61px row
       that is one flick from the list; and once opened the 207-311px group
       would pin too. 8px/12px padding and the 10px margin match the
       .table-cards card rhythm under it. */
    .hub-filterbar { position: static; padding: 8px 12px; margin-bottom: 10px; }
    /* Inside a section panel the bar was a 12px-radius box flush inside the
       panel's own 12px-radius box (doubled side borders). Keep only the rule
       between the bar and the cards. Console registers, where the bar is its
       own box in .admin-main-content, are untouched. */
    .section-panel > .hub-filterbar { border-width: 0 0 1px; border-radius: 0; }

    /* ROW ONE: search + Filters. Was flex-basis:100% (a whole row to itself).
       Basis 0 with a 120px floor keeps the Filters button beside it down to
       320px (296 inner - 104 button-with-badge - 8 gap = 184 >= 120).
       Measured: 184 / 239 / 278px at 320/375/414. 16px so iPhone Safari does
       not zoom the page on focus (the .hub-filterbar__sortsel precedent). */
    .hub-filterbar__search { flex: 1 1 0; min-width: 120px; font-size: 16px; }
    .hub-filterbar__disclose { display: inline-flex; flex: 0 0 auto; }   /* 78x44 (104 with the badge) */

    /* THE GROUP: display:none until hub-filterbar.js adds .hub-filterbar--open
       to the bar; then a full-width sub-bar on the bar's own recipe (wrap, 8px
       gap, --line). State is the CLASS, never the hidden attribute on this box:
       Bootstrap 5's reboot ([hidden]{display:none!important}) is on these
       pages and at >=641px the group must stay display:contents. Without JS it
       stays collapsed - every control in it is JS-driven anyway.
       PHONE FIELD BUDGET - DROPPED FROM THE DEFAULT PHONE VIEW, all still in the
       DOM for hub-filterbar.js: the sort select (44px), the Kind facet when
       present (44px), the date range (94px), the toggles (44px) and Clear All
       (44px) - the controls that put the first trip card at page-y 817 on an
       812px screen. "Showing N of M" (21px) is clipped, not dropped: see
       .hub-filterbar__count below. Active chips stay OUTSIDE and visible. */
    .hub-filterbar__more {
        display: none;
        width: 100%;
        flex-wrap: wrap;
        align-items: center;
        gap: 8px;
        padding-top: 8px;
        border-top: 1px solid var(--line, #E6E2D2);
    }
    .hub-filterbar--open .hub-filterbar__more { display: flex; }   /* 207px on the fuel page at 375; 259 at 320; +52 with Kind */
    /* Clear All rides the right end of the toggles row (the spacer absorbs the
       slack); at 320 it wraps to its own right-aligned row. */
    .hub-filterbar__clear { margin-left: auto; }

    /* The live count is CLIPPED while nothing filters (the platform
       visually-hidden recipe): it stays in the accessibility tree so
       "Showing 2 of 3" is still announced, and it never shares row one with
       the search box. Once something filters - a chip, or a text-only search
       that draws no chip - it is a 21px line of its own. */
    .hub-filterbar__count {
        position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
        overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
    }
    .hub-filterbar--filtered .hub-filterbar__count {
        position: static; width: auto; height: auto; margin: 0;
        overflow: visible; clip: auto; clip-path: none; flex: 0 0 100%;
    }

    .hub-multiselect, .hub-multiselect__trigger { width: 100%; }
    .hub-multiselect__panel { min-width: 0; width: 100%; }
    /* Two date legs side by side overflow a 375px viewport; stack them. */
    .hub-daterange { flex-wrap: wrap; width: 100%; }
    .hub-daterange__leg { flex: 1 1 100%; }
    .hub-daterange input[type="date"] { flex: 1 1 auto; min-width: 0; font-size: 16px; }   /* 16px: iOS focus zoom */
}

/* Filter pills (register toolbars): pill radius, active = gold fill with
   BLACK text (R1). Grows to the 44px tap target on touch widths. */
.filter-pill {
    display: inline-flex;
    align-items: center;
    /* On the button token scale (was padding-derived 35px - a 1px stray
       against the 36px primitive; 2026-07-31 audit nit). */
    height: var(--btn-h-md, 36px);
    padding: 0 14px;
    border-radius: 99px;
    border: 1px solid var(--gray-300, #edebe9);
    background: var(--gray-50, #fff);
    color: var(--gray-800, #323130);
    font-size: 13px;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
}

.filter-pill--on {
    background: var(--btn-primary-bg, #C4B454);
    border-color: transparent;
    color: var(--btn-primary-text, #000000);
    font-weight: 600;
}

/* 1024, not 1023, and coarse pointers too - matching every other touch lift
   in this file (see the .hub-btn block). At 1023 the district's iPads missed
   out entirely: they report 1024, 1080, 1194 or 1366 in landscape, so the
   status tabs - the main filtering control on every register - stayed 36px
   beside 44px buttons, and below the tap floor on a device that is all touch. */
@media (max-width: 1024px), (pointer: coarse) {
    .filter-pill {
        min-height: var(--tap-target, 2.75rem);
    }
}

/* Humane empty values ("Unassigned", "No reading yet") render muted - never
   a bare dash. --ink-mute is legal here: register body text is >=13px. */
.empty-value {
    color: var(--ink-mute, #6F6A5C);
    font-weight: 400;
}

/* Inline-grid entry forms (group mappings, per-user overrides): the desktop
   column template rides inline on the form tag, so the phone stack needs
   !important to out-rank it. */
.entry-form-grid {
    display: grid;
    gap: var(--space-3, 12px);
    align-items: end;
}

@media (max-width: 899px) {
    .entry-form-grid {
        grid-template-columns: 1fr !important;
    }
}

/* ==========================================================================
   FORM-PAGE PATTERN - one shared scaffold for every create/edit form page
   (student directory, profile, ...). Documented in docs/DESIGN-BRIEF.md.
   Replaces the per-form inline max-width caps and bottom submit buttons.
     .form-page-header - sticky title row; primary action top-right + Cancel.
                         The Save <button> lives here and targets the body form
                         via form="<id>", so it can sit outside the <form>.
     .form-grid        - full-width responsive field grid: 2-up on desktop,
                         1-up on mobile (the 1-up drop lives in _responsive.css
                         so its media query out-ranks this base rule).
     .form-field       - one labelled control; --full spans the whole row
                         (textareas, notes, fieldsets).
   ========================================================================== */
.form-page-header {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
    /* Container-agnostic: aligns with the content column and stays put on
       scroll. Opaque background + bottom rule so fields pass cleanly behind it
       (works the same inside the admin shell and the narrower profile page). */
    margin: 0 0 var(--space-4);
    padding: var(--space-3) 0;
    background: white;
    border-bottom: 1px solid var(--border-primary);
}

.form-page-header__titles {
    min-width: 0;
}

.form-page-header__back {
    display: inline-block;
    margin-bottom: var(--space-1);
    font-size: var(--text-sm);
}

.form-page-header__titles h1 {
    margin: 0;
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--text-primary);
}

.form-page-header__actions {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
}

.form-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3) var(--space-4);
    align-items: start;
}

.form-field {
    display: block;
    min-width: 0;
    margin: 0;
}

/* A field, note, or fieldset that must own the whole row. */
.form-field--full {
    grid-column: 1 / -1;
}

.form-field__label {
    display: block;
    font-weight: 600;
    margin-bottom: var(--space-1);
}

.form-input {
    width: 100%;
    padding: 10px 12px;
    font: inherit;
    box-sizing: border-box;
    border: 1px solid var(--gray-400);
    border-radius: var(--radius-control);
    background: var(--surface);
    color: var(--ink);
}

.form-input:focus-visible {
    outline: none;
    border-color: var(--gold-border);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold, #C4B454) 35%, transparent);
}

/* CURRENT RECEIPT on an edit form (2026-09-14): a thumbnail card above the
   photo control - the image (or a PDF tile), the friendly caption, when it was
   added. The whole card is the link to the file; 44px is the card's height. */
.receipt-thumbs { display: grid; gap: 6px; margin-bottom: 10px; }

/* WHEN A RECEIPT IS ALREADY ATTACHED, the flow's own "Photo of the receipt /
   Add a Photo" block steps aside: the two buttons above do that job now, and
   stacked together the page offered three ways to attach one photo.

   CLIPPED, NEVER display:none. Those buttons WORK by clicking the flow's own
   file input - it is what the upload sheet is bound to - so taking it out of
   the layout would break the control that replaced it. Same visually-hidden
   pattern the range calendar uses to park its native date inputs while they
   keep submitting. */
.form-field:has(.receipt-acts) > .hub-upload {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    clip-path: inset(50%);
    overflow: hidden;
    white-space: nowrap;
}

/* How many more are behind this one. On the thumbnail, because a second row
   of pictures on a form is a gallery nobody asked for. */
.receipt-thumb { position: relative; }
.receipt-thumb__more {
    position: absolute;
    /* ON THE PICTURE, not over the words. Anchored to the card this sat
       on the caption - at 390px the "+1" landed in the middle of
       "Photo (183 KB)". translateX(-100%) pulls it back by its own
       width, which is the one measurement no offset here can know. */
    top: calc(var(--thumb-pad) + 2px);
    left: calc(var(--thumb-pad) + var(--thumb-img) - 2px);
    transform: translateX(-100%);
    min-width: 22px;
    height: 22px;
    padding: 0 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    background: var(--ink, #201f1e);
    color: var(--surface, #fff);
    font-size: 12px;
    font-weight: 700;
}

/* A PICKED PHOTO IS NOT A SAVED PHOTO, and it says so by LOOKING different
   rather than by adding a sentence underneath. Richard, 2026-09-14: "indicate
   the change by highlighting the changes or something vs another sentence."
   The sentence that used to live here is what he was answering - a line of
   text is the thing people skim past, which is how the original problem
   started.

   The ribbon is generated from an attribute rather than hard-coded, so the
   wording lives with the behaviour in one place. */
.receipt-thumb.is-unsaved {
    outline: 3px solid var(--gold, #C4B454);
    outline-offset: 2px;
    border-radius: var(--radius-control);
}
.receipt-thumb.is-unsaved::after {
    content: attr(data-unsaved);
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 3px 4px;
    background: var(--gold, #C4B454);
    color: var(--ink, #201f1e);
    font-size: 11px;
    font-weight: 700;
    text-align: center;
    border-radius: 0 0 var(--radius-control) var(--radius-control);
}

/* THE RECEIPT VIEWER - only built when a trip carries more than one, so a
   single receipt keeps its plain link and nobody pays for a pager they do not
   need. Lives inside HubDrawer, which supplies the backdrop, the Escape key
   and the focus trap; everything here is just the contents. */
.receipt-viewer { display: grid; gap: 8px; }
.receipt-viewer__stage {
    display: grid;
    /* auto, not 44px: the arrows are .hub-btn--icon - 44px square at touch
       widths, 36px on a desktop pointer - and a fixed 44px track would park
       a 36px arrow against one side of it. */
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 6px;
}
.receipt-viewer__img {
    width: 100%;
    max-height: 60vh;
    object-fit: contain;
    border-radius: var(--radius-control);
    background: var(--gray-50, #F9FAFB);
}
/* The arrows are .hub-btn.hub-btn--ghost.hub-btn--icon: the primitive owns
   the square, the secondary border, the gold focus ring and the 36px
   desktop / 44px touch step (DESIGN-BRIEF section 2, icon-only buttons).
   Only the chevron's size is left here - at the primitive's 13px a lone
   &lsaquo; is a speck. Same file, later than .hub-btn, so it wins. */
.receipt-viewer__nav {
    font-size: 22px;
}
.receipt-viewer__cap {
    margin: 0;
    font-size: 14px;
    color: var(--gray-600, #8a8886);
}
.receipt-viewer__dot {
    margin: 0;
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
.receipt-viewer__manage { justify-self: start; }

/* THE TWO PHOTO BUTTONS under the current receipt's thumbnail. A pair, so
   they share a row and wrap together rather than one dropping alone; full
   touch height, because on a phone this is the control people actually use. */
.receipt-acts {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 2px 0 8px;
}
.receipt-acts .hub-btn {
    flex: 1 1 auto;
    min-height: var(--btn-h-touch, 44px);
}

/* RETIRED 2026-09-14: the radio pair this replaced. Kept only until the next
   sweep confirms nothing else renders .receipt-mode.
   REPLACE / ADD ANOTHER - what a new photo does to the one already attached.
   Radios, because the page must work with no JavaScript on a phone with one
   bar of signal in a school car park; styled as the two buttons they read as.

   The native dot is KEPT rather than hidden: it is the one affordance a
   screen reader and a forced-colours mode can both rely on, and the gold
   ground is added on top so the state is never carried by colour alone.

   The whole label is the target, at the 44px touch floor - tapping the words
   selects, which is what anybody expects of something shaped like a button. */
.receipt-mode {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 2px 0 8px;
}
.receipt-mode__opt {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: var(--btn-h-touch, 44px);
    padding: 0 14px;
    border: 1px solid var(--gray-400, #c8c6c4);
    border-radius: var(--radius-control);
    background: var(--surface, #fff);
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
    /* Neither option may be the one that wraps to its own line on a narrow
       phone while the other stays put - they are a pair and read as a pair. */
    flex: 1 1 auto;
    justify-content: center;
}
.receipt-mode__opt input {
    width: auto;
    flex: 0 0 auto;
    margin: 0;
}
.receipt-mode__opt:has(input:checked) {
    background: var(--gold-tint, #F5EFD0);
    border-color: var(--gold-border, #D9CD8A);
}
.receipt-mode__opt:has(input:focus-visible) {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold, #C4B454) 35%, transparent);
}
.receipt-thumbs__row { display: flex; flex-wrap: wrap; gap: 10px; }
.receipt-thumb {
    /* The card's two fixed sizes, named once. The +n badge positions
       itself from these rather than from numbers copied out of here. */
    --thumb-img: 72px;
    --thumb-pad: 6px;
    display: grid;
    grid-template-columns: var(--thumb-img) minmax(0, 1fr);
    grid-template-rows: auto auto;
    column-gap: 12px;
    align-items: center;
    min-height: 56px;
    padding: var(--thumb-pad);
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
    background: var(--surface, #FFFFFF);
    color: var(--ink, #191712);
    text-decoration: none;
}
.receipt-thumb:focus-visible { outline: 2px solid var(--gold-text, #75692E); outline-offset: 2px; }
.receipt-thumb__img, .receipt-thumb__pdf {
    grid-row: 1 / 3;
    width: var(--thumb-img);
    height: 54px;
    object-fit: cover;
    border-radius: 8px;
    background: var(--gray-100, #F3F2F1);
}
.receipt-thumb__pdf { display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: var(--ink-soft, #4A4638); }
.receipt-thumb__cap { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.receipt-thumb__when { font-size: 12px; color: var(--ink-mute, #6F6A5C); }

.form-hint {
    margin: var(--space-2) 0 0;
    color: var(--gray-600);
    font-size: var(--text-sm);
}

/* Sticky-header enablement: .admin-main is the scroll container, but its child
   .admin-main-content is height:100% - which clamps a sticky child's containing
   block to one viewport, so .form-page-header un-pins after one screen on a long
   form. Let the content column grow to its content ONLY on form pages (scoped
   via :has so the admin landing pages that use a fixed-height .content-body
   inner scroll are untouched). Now the header pins for the whole scroll. */
.admin-main-content:has(> .form-page-header) {
    height: auto;
    min-height: 100%;
}

/* Nav landing cards (Users/Settings landings) reuse the module-card tile as a
   link, with a live count pill on the right. Nonzero counts are the gold chip;
   zero is muted so an empty queue does not read as "attention needed". */
.admin-root .nav-landing-card {
    text-decoration: none;
}

.admin-root .nav-landing-count {
    margin-left: auto;
    align-self: center;
    min-width: 1.75rem;
    padding: 0.15rem 0.6rem;
    border-radius: 999px;
    font-size: 0.85rem;
    font-weight: 700;
    text-align: center;
    background: var(--gray-200);
    color: var(--gray-600);
}

.admin-root .nav-landing-count.has-count {
    background: var(--gold-tint, #F5EFD0);
    color: var(--gold-deep, #6B5D3F);
    border: 1px solid var(--gold-border, #D9CD8A);
}

/* GOLD DECISIONS: one district-gold icon square for every module tile - the
   per-color rainbow (blue/purple/orange/green/teal/gray) is retired. Gold
   fill, WHITE glyph (R2: icons on gold are white). The color class on the
   markup is now inert; all variants resolve to the same gold. */
.admin-root .mgmt-module-card .module-icon,
.admin-root .mgmt-module-card .module-icon.blue,
.admin-root .mgmt-module-card .module-icon.purple,
.admin-root .mgmt-module-card .module-icon.orange,
.admin-root .mgmt-module-card .module-icon.green,
.admin-root .mgmt-module-card .module-icon.teal,
.admin-root .mgmt-module-card .module-icon.gray {
    background: var(--btn-primary-bg, #C4B454);
    color: #000000; /* R2 corrected 2026-07-27: white on #C4B454 = 2.1:1 FAILS; black = 10:1 (tile decision may move this to deep bronze + white) */
}

.admin-root .mgmt-module-card .module-icon .svg-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
}

.admin-root .mgmt-module-card .module-icon .svg-icon svg {
    width: 100%;
    height: 100%;
    stroke: currentColor;
    fill: none;
}

/* FontAwesome / Bootstrap icons inside module-icon */
.admin-root .mgmt-module-card .module-icon i {
    font-size: 24px;
    color: inherit;
}

/* Per-section identity on management/admin surfaces. The dashboard tile takes
   the section tint + accent (falls back to the gold tile for unmapped
   sections); .section-title-icon is the page-header glyph next to a section
   title, accent-colored inside a [data-section] wrapper. */
.admin-root .mgmt-module-card .module-icon[data-section],
.admin-root .mgmt-module-card .module-icon[data-palette] {
    background: var(--section-tint, var(--gold-tint, #F5EFD0));
    color: var(--section-accent, var(--gold-text, #75692E));
}

.section-title-icon {
    flex-shrink: 0;
    margin-top: 4px;
    font-size: 26px;
    color: var(--section-accent, var(--gray-600, #4B5563));
}

/* SMALL CHROME WEIGHT (2026-07-28 decision): brand marks, console-switcher
   buttons, and header icon-buttons render at 14-20px where the standard
   Lucide stroke goes thin - bump to 2.2 so outlines hold up. SECTION and
   card icons keep their weight; this is small chrome only. (CSS wins over
   the SVG's stroke-width presentation attribute; fill-only paths, e.g. the
   shield's solid half, are unaffected.) */
.sidebar-logo svg,
.context-switch-btn svg,
.admin-header .btn-icon svg {
    stroke-width: 2.2;
}

/* Inline Lucide icon - the ONE icon output of IconResolver / hub_icon(). Base
   sizing is text-relative (1em) so a bare inline icon matches its label;
   container rules below (.section-title-icon, .mgmt-module-card .module-icon,
   .action-icon, .section-head-icon in section-shell.css) override where a
   fixed size is wanted. Present in all three bundles. */
.svg-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 0;
    color: inherit;
    vertical-align: -0.125em;
}
.svg-icon svg {
    width: 1em;
    height: 1em;
}
/* Loader glyphs (former fa-spin / bi-hourglass): hub_icon('loader-circle',
   ['spin'=>true]) or a .spin wrapper class. */
.svg-icon.spin svg {
    animation: hub-icon-spin 1s linear infinite;
}
@keyframes hub-icon-spin {
    to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
    .svg-icon.spin svg { animation: none; }
}

span.svg-icon.section-title-icon {
    display: inline-flex;
    width: 30px;
    height: 30px;
}

span.svg-icon.section-title-icon svg {
    width: 100%;
    height: 100%;
}

.admin-root .mgmt-module-card h3 {
    font-size: var(--text-lg);
    font-weight: var(--font-semibold);
    color: var(--gray-900);
    margin: 0;
}

.admin-root .mgmt-module-card .module-description {
    font-size: var(--text-sm);
    color: var(--gray-600);
    line-height: var(--leading-relaxed);
    margin: 0;
}

/* ── Rich Card variant (Google Admin Console style) ── */
.admin-root .mgmt-rich-card {
    cursor: default;
}

.admin-root .mgmt-rich-card .module-header {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    margin-bottom: 0;
}

.admin-root .mgmt-rich-card .module-header-text {
    flex: 1;
    min-width: 0;
}

.admin-root .mgmt-rich-card .module-header-text h3 {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: var(--font-semibold);
    color: var(--gray-900);
    line-height: 1.3;
}

.admin-root .mgmt-rich-card .module-subtitle {
    margin: var(--space-1) 0 0 0;
    font-size: var(--text-sm);
    color: var(--gray-600);
    line-height: var(--leading-relaxed);
}

/* ORPHAN-ROW FIX. .mgmt-modules-grid is display:grid with no align-items, so
   items resolve to stretch and each ROW is sized to its own tallest card. A
   card alone on a short final row is never stretched, so it renders at its
   intrinsic height while the full row above sits at the height of whichever
   card wrapped its description to two lines. Measured on /admin/users: four
   cards at 116.2px, the lone Google Groups card at 94px. Reserving a two-line
   description slot makes every card's intrinsic height identical, which makes
   the per-row stretch a no-op.

   SCOPED TO .nav-landing-card ON PURPOSE - do NOT fold this into the rule
   above. .module-subtitle is emitted by six views. The two DASHBOARDS emit
   <p class="module-subtitle"> UNCONDITIONALLY from a nullable DB column, so an
   empty description is an empty <p> that today collapses to 0px; an unscoped
   min-height would inject a blank band into every such card. Those two views
   do NOT carry nav-landing-card, so this selector misses them by construction.

   Token-derived, not px: a literal height would need re-deriving at each of
   the 991/767/575 breakpoints. */
.admin-root .nav-landing-card .module-subtitle {
    min-height: calc(2 * var(--leading-relaxed) * var(--text-sm));
}

.admin-root .mgmt-rich-card .module-manage-link {
    font-size: var(--text-sm);
    font-weight: var(--font-semibold);
    color: var(--primary-color);
    text-decoration: none;
    white-space: nowrap;
    flex-shrink: 0;
    padding: 2px 0;
    transition: color var(--transition-base);
}

.admin-root .mgmt-rich-card .module-manage-link:hover {
    color: var(--gold-deep); /* was a kill-list blue fallback */
    text-decoration: underline;
}

.admin-root .module-quick-actions,
.admin-root .module-links {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--gray-200);
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

/* Grouped console IA: dashboard group headings + card link anatomy */
.mgmt-card-group {
    margin-bottom: var(--space-6);
}

/* Content group headings are SECTION DIVIDERS, not a repeat of the rail's
   nav label: 13px caps ink-soft with a hairline rule running right, and
   clear separation (>=24px) from whatever sits above (the toolbar). */
.mgmt-card-group {
    margin-top: var(--space-6, 24px);
}
.mgmt-card-group-heading {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0 0 var(--space-3);
    padding: 0 var(--space-6);
    font-size: 13px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ink-soft, #4A4638);
}

.mgmt-card-group-heading::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--line, #E6E2D2);
}

/* Linked card title: the name IS the way in (replaces the Manage link) */
.admin-root .module-title-link {
    color: inherit;
    text-decoration: none;
}

.admin-root .module-title-link:hover,
.admin-root .module-title-link:focus-visible {
    text-decoration: underline;
    color: var(--gold-text);
}

.admin-root .module-quick-link {
    font-size: var(--text-sm);
    color: var(--gold-text); /* R3: gold as text on the light card */
    text-decoration: none;
    padding: var(--space-1) 0;
}

.admin-root .module-quick-link:hover {
    text-decoration: underline;
    color: var(--gold-deep);
}

.admin-root .module-link-count {
    color: var(--gray-700);
    font-weight: 600;
}

/* Two-column layout when a package has 5+ pages */
.admin-root .module-quick-actions.two-col {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-1) var(--space-4);
}

@media (max-width: 480px) {
    .admin-root .module-quick-actions.two-col {
        grid-template-columns: 1fr;
    }
}

:is(.admin-root, .hub-page) .module-action-link {
    font-size: var(--text-sm);
    color: var(--gold-text); /* R3: was --primary-color = fill gold as text on light */
    text-decoration: none;
    padding: var(--space-1) 0;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    transition: color var(--transition-base);
    overflow: hidden;
    min-width: 0;
}

:is(.admin-root, .hub-page) .module-action-link > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

:is(.admin-root, .hub-page) .module-action-link:hover {
    color: var(--gold-deep); /* was a kill-list blue fallback */
    text-decoration: underline;
}

:is(.admin-root, .hub-page) .module-action-link i {
    font-size: 0.75rem;
    opacity: 0.7;
    flex-shrink: 0;
}

/* Inside running text (p/li) the action link must flow inline - the flex
   default would force it onto its own line and break the sentence. */
:is(.admin-root, .hub-page) p .module-action-link,
:is(.admin-root, .hub-page) li .module-action-link {
    display: inline;
}

/* ── Attention Section (management dashboard) ─────── */
.admin-root .attention-section {
    margin-top: var(--space-5);
}

.admin-root .attention-section h2 {
    font-size: var(--text-base);
    font-weight: var(--font-semibold);
    color: var(--gray-800);
    margin: 0 0 var(--space-3) 0;
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.admin-root .attention-section .attention-icon-bell {
    color: var(--warning);
}

.admin-root .attention-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.admin-root .attention-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    background: var(--white, #fff);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    transition: all var(--transition-base);
}

.admin-root .attention-item:hover {
    border-color: var(--primary-color);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}

.admin-root .attention-item.type-warning {
    border-left: 3px solid var(--warning);
}

.admin-root .attention-item.type-danger {
    border-left: 3px solid var(--error);
}

.admin-root .attention-item.type-info {
    border-left: 3px solid var(--info);
}

.admin-root .attention-icon {
    width: 36px;
    height: 36px;
    border-radius: var(--radius-base);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1rem;
    flex-shrink: 0;
}

.admin-root .attention-icon.warning {
    background: var(--warning-light);
    color: var(--warning);
}

.admin-root .attention-icon.danger {
    background: var(--error-light);
    color: var(--error);
}

.admin-root .attention-icon.info {
    background: var(--info-light);
    color: var(--info);
}

.admin-root .attention-body {
    flex: 1;
    min-width: 0;
}

.admin-root .attention-title {
    font-size: var(--text-sm);
    font-weight: var(--font-medium);
    color: var(--gray-900);
    margin: 0;
}

.admin-root .attention-subtitle {
    font-size: var(--text-xs);
    color: var(--gray-500);
    margin: 0;
}

.admin-root .attention-action {
    font-size: var(--text-xs);
    font-weight: var(--font-semibold);
    color: var(--primary-color);
    text-decoration: none;
    padding: 4px 12px;
    border-radius: 9999px;
    background: var(--primary-50, #eef);
    white-space: nowrap;
    transition: all var(--transition-base);
}

.admin-root .attention-action:hover {
    background: var(--primary-color);
    color: var(--btn-primary-text, #000000);
}

/* ========== 2. COMMAND BAR (ACTION TOOLBAR) ========== */
.admin-root .command-bar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: var(--command-bar-height);
    padding: 0 var(--space-8);
    background: var(--gray-50);
    border-bottom: 1px solid var(--gray-300);
    position: sticky;
    top: 0;
    z-index: var(--z-sticky);
}

.admin-root .command-bar-left {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.admin-root .command-bar-title {
    font-size: var(--text-2xl);
    font-weight: var(--font-semibold);
    color: var(--gray-900);
    line-height: var(--leading-tight);
    margin: 0;
}

.admin-root .command-bar-right {
    display: flex;
    gap: var(--space-2);
    align-items: center;
}

/* ========== 3. BREADCRUMBS ========== */
.admin-root .breadcrumb {
    font-size: var(--text-sm);
    color: var(--gray-600);
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.admin-root .breadcrumb a {
    color: var(--gold-text, #75692E);
    text-decoration: none;
    transition: color var(--transition-fast);
}

.admin-root .breadcrumb a:hover {
    text-decoration: underline;
    color: var(--ms-blue-hover);
}

.admin-root .breadcrumb-sep {
    color: var(--gray-500);
}

/* ========== 4. ENTERPRISE DATA TABLES ========== */
.admin-root .data-table-container {
    background: var(--gray-50);
    border: 1px solid var(--gray-300);
    border-radius: var(--radius-base);
    overflow-x: auto;
    overflow-y: visible;
}

.admin-root .data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-base);
}

.admin-root .data-table thead {
    background: var(--gray-100);
    position: sticky;
    top: 0;
    z-index: var(--z-base);
}

.admin-root .data-table th {
    padding: var(--space-3) var(--space-4);
    text-align: left;
    font-weight: var(--font-semibold);
    color: var(--gray-800);
    border-bottom: 2px solid var(--gray-400);
    white-space: nowrap;
    font-size: var(--text-base);
}

.admin-root .data-table th.sortable {
    cursor: pointer;
    user-select: none;
    transition: background var(--transition-fast);
}

.admin-root .data-table th.sortable:hover {
    background: var(--gray-200);
}

.admin-root .data-table th i {
    margin-left: var(--space-1);
    font-size: 12px;
    color: var(--gray-600);
}

.admin-root .data-table tbody tr {
    border-bottom: 1px solid var(--gray-300);
    transition: background var(--transition-fast);
}

.admin-root .data-table tbody tr:hover {
    background: var(--gray-200);
}

.admin-root .data-table tbody tr:last-child {
    border-bottom: none;
}

.admin-root .data-table tbody tr.selected {
    background: var(--gold-tint, #F5EFD0); /* selected row: gold tint */
}

.admin-root .data-table td {
    padding: var(--space-3) var(--space-4);
    color: var(--gray-800);
    height: var(--table-row-height);
    vertical-align: middle;
}

/* COMPACT + NO-WRAP density for the admin data tables (logs, users - and
   the custom groups table) - one line per row, drop the 48px row-height
   floor. The users-data-table's own 6px/8px override still wins (later in
   the cascade); this covers every OTHER data table. Card mode resets
   white-space (see _responsive.css). Platform law 2026-07-29. */
.admin-root .data-table th,
.admin-root .data-table td {
    padding: 7px 12px;
    height: auto;
    white-space: nowrap;
}
/* Groups table: the users-groups page ships its own inline
   '.groups-table thead th / tbody td' padding (specificity 0-1-2), so the
   compact padding must out-rank it - hence the .admin-root prefix (0-2-2).
   (The blade sets no white-space, so the bare-selector nowrap below lands
   uncontested.) */
.admin-root .groups-table thead th,
.admin-root .groups-table tbody td {
    padding: 7px 12px;
}
.groups-table th,
.groups-table td {
    white-space: nowrap;
}

/* A cell that caps its own width (inline max-width - the Reason/Notes text
   columns) must clip with an ellipsis under the no-wrap law, never let its
   single line overflow onto the next column. Scoped to max-width cells ONLY
   so an action cell's kebab/menu popover is never clipped by overflow. */
.register-table td[style*="max-width"],
.admin-root .data-table td[style*="max-width"] {
    overflow: hidden;
    text-overflow: ellipsis;
}

.admin-root .data-table td.text-muted {
    color: var(--gray-600);
}

.admin-root .data-table .table-actions {
    width: 100px;
    text-align: right;
}

/* User cell with avatar */
.admin-root .user-cell {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.admin-root .user-avatar {
    width: 32px;
    height: 32px;
    border-radius: var(--radius-full);
    background: var(--ms-blue);
    color: var(--gray-50);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-sm);
    font-weight: var(--font-semibold);
    flex-shrink: 0;
}

.admin-root .user-avatar.gold {
    background: var(--btn-primary-bg, #C4B454);
}

/* ========== 5. CHIPS/TAGS ========== */
.admin-root .chip {
    display: inline-flex;
    align-items: center;
    padding: var(--space-1) var(--space-3);
    border-radius: 12px;
    font-size: var(--text-sm);
    font-weight: var(--font-medium);
    white-space: nowrap;
    gap: var(--space-2);
}

.admin-root .chip-admin {
    background: var(--warning-light);
    color: var(--gold-text, #75692E);
}

.admin-root .chip-teacher {
    background: var(--info-light);
    color: var(--ms-blue);
}

.admin-root .chip-staff {
    background: var(--gray-200);
    color: var(--gray-700);
}

.admin-root .chip-success {
    background: var(--success-light);
    color: var(--success);
}

.admin-root .chip-error {
    background: var(--error-light);
    color: var(--error);
}

.admin-root .chip-warning {
    background: var(--warning-light);
    color: var(--warning);
}

.admin-root .chip i {
    font-size: 12px;
}

/* Filter chips (.nd-chip) - Interactive filters */
.admin-root .nd-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--gray-300);
    border-radius: var(--radius-base);
    background: var(--gray-50);
    color: var(--gray-700);
    font-size: var(--text-sm);
    font-weight: var(--font-medium);
    cursor: pointer;
    transition: all var(--transition-fast);
    user-select: none;
}

.admin-root .nd-chip:hover {
    background: var(--gray-200);
    border-color: var(--gray-400);
}

.admin-root .nd-chip.active {
    background: var(--ms-blue);
    color: var(--gray-50);
    border-color: var(--border-focus, #C4B454);
}

.admin-root .nd-chip i {
    font-size: 12px;
}

/* Status pills (.nd-pill) - Read-only status indicators */
.admin-root .nd-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 4px var(--space-3);
    border-radius: 12px;
    font-size: var(--text-sm);
    font-weight: var(--font-medium);
    white-space: nowrap;
}

.admin-root .nd-pill.pill-success {
    background: var(--success-light);
    color: var(--success);
}

.admin-root .nd-pill.pill-warning {
    background: var(--warning-light);
    color: var(--warning);
}

.admin-root .nd-pill.pill-error {
    background: var(--error-light);
    color: var(--error);
}

.admin-root .nd-pill.pill-info {
    background: var(--info-light);
    color: var(--ms-blue);
}

.admin-root .nd-pill.pill-neutral {
    background: var(--gray-200);
    color: var(--gray-700);
}

.admin-root .nd-pill i {
    font-size: 11px;
}

/* Table toolbar with filter chips */
.admin-root .table-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--space-3) 0;
    margin-bottom: var(--space-3);
    gap: var(--space-3);
}

.admin-root .toolbar-left {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.admin-root .toolbar-right {
    display: flex;
    gap: var(--space-2);
}

/* ========== 8. SIDEBAR NAVIGATION ========== */

.admin-sidebar {
    width: var(--sidebar-width);
    background: var(--gray-50);
    border-right: 1px solid var(--gray-300);
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overflow-x: hidden;
}

/* Sidebar header styling moved to admin-layout.css */

.admin-root .sidebar-logo {
    width: 40px;
    height: 40px;
    background: var(--btn-primary-bg, #C4B454);
    border-radius: var(--radius-base);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #000; /* R2 corrected 2026-07-27: white on #C4B454 = 2.1:1 FAILS; black = 10:1. (Deep-bronze+white variant pending the tile decision.) */
    font-size: 20px;
    flex-shrink: 0;
}

.admin-root .sidebar-title {
    font-size: var(--text-lg);
    font-weight: var(--font-semibold);
    color: var(--gray-900);
}

.admin-root .sidebar-menu {
    list-style: none;
    padding: var(--space-2);
    margin: 0;
}

.admin-root .sidebar-menu li {
    margin-bottom: var(--space-1);
}

.admin-root .sidebar-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-base);
    text-decoration: none;
    color: var(--gray-800);
    font-size: var(--text-md);
    transition: background var(--transition-fast);
    position: relative;
}

.admin-root .sidebar-item:hover {
    background: var(--gray-200);
}

.admin-root .sidebar-item.active {
    background: var(--gray-300);
}

.admin-root .sidebar-item.active::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    background: var(--btn-primary-bg, #C4B454);
}

.admin-root .sidebar-item i {
    width: 20px;
    text-align: center;
    color: var(--gray-700);
    flex-shrink: 0;
    font-size: 16px;
}

.admin-root .sidebar-item.active i {
    color: var(--gold-text, #75692E);
}

.admin-root .sidebar-label {
    flex: 1;
}

.admin-root .sidebar-badge {
    background: #d32f2f; /* align with the other sidebar badges */
    color: var(--gray-50);
    font-size: var(--text-xs);
    font-weight: var(--font-semibold);
    padding: 2px var(--space-2);
    border-radius: 10px;
    min-width: 20px;
    text-align: center;
}

.admin-root .sidebar-badge.gold {
    background: var(--btn-primary-bg, #C4B454);
}

.admin-root .sidebar-divider {
    height: 1px;
    background: var(--gray-300);
    margin: var(--space-2) 0;
}

.admin-root .sidebar-group-header {
    padding: var(--space-4) var(--space-3) var(--space-2);
    font-size: var(--text-sm);
    font-weight: var(--font-semibold);
    color: var(--gray-600);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* ========== 8. ENTERPRISE SUBTABS ========== */
/* User subtabs styling moved to admin-dashboard.css */

/* Subtab button styling moved to admin-dashboard.css */

/* Subtab button active state moved to admin-dashboard.css */

.admin-root .subtab-btn i {
    font-size: 14px;
}

/* ========== 9. LOADING STATES ========== */
.admin-root .skeleton {
    background: linear-gradient(90deg,
            var(--gray-200) 25%,
            var(--gray-300) 50%,
            var(--gray-200) 75%);
    background-size: 200% 100%;
    animation: skeleton-loading 1.5s infinite ease-in-out;
    border-radius: var(--radius-base);
}

@keyframes skeleton-loading {
    0% {
        background-position: 200% 0;
    }

    100% {
        background-position: -200% 0;
    }
}

.admin-root .spinner {
    width: 24px;
    height: 24px;
    border: 3px solid var(--gray-300);
    border-top-color: var(--btn-primary-bg, #C4B454);
    border-radius: var(--radius-full);
    animation: spinner-rotate 0.8s linear infinite;
}

@keyframes spinner-rotate {
    to {
        transform: rotate(360deg);
    }
}

/* ========== 10. ALERTS/NOTIFICATIONS ========== */
.admin-root .alert {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-base);
    font-size: var(--text-md);
    line-height: var(--leading-snug);
}

.admin-root .alert i {
    flex-shrink: 0;
    font-size: 18px;
}

.admin-root .alert-success {
    background: var(--success-light);
    color: var(--success);
    border-left: 4px solid var(--success);
}

.admin-root .alert-warning {
    background: var(--warning-light);
    color: var(--warning);
    border-left: 4px solid var(--warning);
}

.admin-root .alert-error {
    background: var(--error-light);
    color: var(--error);
    border-left: 4px solid var(--error);
}

.admin-root .alert-info {
    background: var(--info-light);
    color: var(--ms-blue);
    border-left: 4px solid var(--ms-blue);
}

/* ========== 11. EMPTY STATES ========== */
.admin-root .empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: var(--space-16);
    text-align: center;
}

.admin-root .empty-state-icon {
    font-size: 64px;
    color: var(--gray-400);
    margin-bottom: var(--space-4);
}

.admin-root .empty-state-title {
    font-size: var(--text-xl);
    font-weight: var(--font-semibold);
    color: var(--gray-800);
    margin-bottom: var(--space-2);
}

.admin-root .empty-state-description {
    font-size: var(--text-md);
    color: var(--gray-600);
    margin-bottom: var(--space-6);
    max-width: 480px;
}

/* SIDEBAR NAVIGATION LINKS */

/* All nav items extend to the sidebar edge via margin-left: -10px
   (cancels .admin-nav's 10px left padding). Padding-left is increased
   by 10px to compensate, keeping text/icons aligned.

   Layout from sidebar edge:  [0px edge] [20px pad] [ICON 20px] [12px gap] [TEXT]
   Caret at left:2px (always 2px from sidebar edge, inside bounds).
   border-radius: 0 999px = flat left edge, round right = pill to edge.
   ======================================== */

.admin-nav-link,
.mgmt-nav-link {
    margin-left: -10px;
    padding-left: 24px; /* 14px + 10px margin compensation */
    border-radius: 0 999px 999px 0;
    margin-right: 4px;
}

/* ========================================
   EXPANDABLE NAVIGATION ITEMS (Google Admin Style)

   ALL items extend to the sidebar edge via margin-left: -10px.
   Caret is a CSS ::before at left:2px (2px from sidebar edge).

   Positions (from sidebar left edge):
     Caret:     2px (left:2px on trigger at edge)
     Icon:      20px (padding-left)
     Text:      20 + 20(icon) + 12(gap) = 52px
     L2 text:   padding-left 52px
     L3 text:   padding-left 68px  (+16px step)

   Active states only change colors — no margin/padding overrides needed.
   ======================================== */

.nav-expandable {
    position: relative;
    overflow: visible;
    margin: 0;
    padding: 0;
}

.nav-nested-expandable {
    margin: 0;
}

/* Trigger button — extends to sidebar edge like all nav items */
.nav-expandable-trigger {
    position: relative; /* Anchor for the .nav-exp-caret chevron */
    width: calc(100% + 10px); /* Compensate for negative margin */
    border: none;
    background: transparent;
    cursor: pointer;
    text-align: left;
    display: flex;
    align-items: center;
    gap: var(--space-3); /* 12px — same as .admin-nav-link */
    padding: var(--space-2) var(--space-3);
    padding-left: 24px; /* 14px + 10px margin compensation */
    margin: 0;
    margin-left: -10px; /* Extend to sidebar edge */
    margin-right: 4px;
    border-radius: 0 999px 999px 0;
    color: inherit;
    font: inherit;
}

/* ===== CARET - real Lucide chevron element, 2px from sidebar edge.
   (Replaces the FA ::before glyph: the icon font is retired, so its
   private-use codepoint rendered as an empty tofu box. Drawn icons in CSS
   are now build-banned - see the icon gate.) When the trigger row goes
   active on gold, admin-layout's ".admin-nav-link.active .svg-icon" rule
   recolors this like every other glyph on the row. ===== */
.nav-expandable-trigger .nav-exp-caret {
    position: absolute;
    left: 2px; /* 2px from sidebar edge (trigger is at edge via margin-left:-10px) */
    top: 50%;
    transform: translateY(-50%) rotate(0deg);
    /* 10px: the chevron stroke fills its box, so it reads larger than the
       old FA caret glyph did at the same nominal size */
    width: 10px;
    height: 10px;
    display: inline-flex;
    color: var(--gray-400);
    transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
                color 0.15s ease;
    pointer-events: none;
}

.nav-expandable-trigger .nav-exp-caret svg {
    width: 100%;
    height: 100%;
    display: block;
}

/* Rotate caret when expanded */
.nav-expandable.expanded > .nav-expandable-trigger .nav-exp-caret {
    transform: translateY(-50%) rotate(90deg);
    color: var(--gray-500);
}

.nav-expandable-trigger:hover .nav-exp-caret {
    color: var(--gray-700);
}

.nav-submenu {
    background: transparent;
    border-left: none;
    margin-left: 0;
    padding-left: 0;
}

/* ===== LEVEL 2: Sublinks (text items under L1 expandable) ===== */
.admin-nav-link.nav-sublink,
.mgmt-nav-link.nav-sublink {
    padding-left: 56px !important; /* 46px indent + 10px margin compensation */
    font-size: var(--text-md) !important;
    font-weight: var(--font-normal) !important;
    opacity: 0.85;
}

.admin-nav-link.nav-sublink span,
.mgmt-nav-link.nav-sublink span {
    font-weight: var(--font-normal) !important;
}

.admin-nav-link.nav-sublink i,
.mgmt-nav-link.nav-sublink i {
    display: none; /* Hide icons on sublinks — text only */
}

/* ===== LEVEL 2: Nested expandable trigger (package under category) ===== */
.nav-nested-expandable > .nav-expandable-trigger {
    padding-left: 56px !important; /* Aligns with L2 sublinks (46 + 10 compensation) */
    font-weight: var(--font-normal) !important;
    opacity: 0.85;
}

/* Allow text wrapping for long labels in submenu items (e.g. package names) */
.nav-nested-expandable > .nav-expandable-trigger,
.admin-nav-link.nav-sublink,
.mgmt-nav-link.nav-sublink,
.nav-submenu-nested .nav-leaf-link,
.admin-nav-link.nav-expandable-trigger {
    /* Sidebar NAV labels may wrap (they are navigation, not action buttons -
       the global button nowrap rule must not clip admin-configurable long
       section names; includes the L1 <button> triggers). */
    white-space: normal !important;
    overflow: visible !important;
    line-height: 1.4;
    /* Rhythm floor: bare .admin-nav-link inherits line-height 1.6 from
       .admin-root, so at 15px/8px padding it lands on exactly 40px. The
       1.4 above (kept - it tightens leading on wrapped long section names)
       makes these rows 15*1.4+16 = 37px. The min-height floors every row
       in this group at the same 40px; align-items:center on the parents
       recentres the shorter line box, and multi-line labels still grow
       past the floor. */
    min-height: 40px;
}

/* Nested caret — repositioned for L2 indentation */
.nav-nested-expandable > .nav-expandable-trigger .nav-exp-caret {
    left: 41px; /* ~15px before the 56px text start */
    top: 13px; /* Align with first line of text */
    transform: rotate(0deg);
}

.nav-nested-expandable > .nav-expandable-trigger:hover {
    opacity: 1;
}

/* Nested caret rotation when expanded */
.nav-nested-expandable.expanded > .nav-expandable-trigger .nav-exp-caret {
    transform: rotate(90deg);
}

/* ===== LEVEL 3: Leaf links (pages inside packages) ===== */
.nav-submenu-nested .nav-leaf-link {
    padding-left: 72px !important; /* +16px from L2 (56+16) */
    font-size: var(--text-md) !important;
}

.nav-submenu-nested .nav-leaf-link i {
    display: none;
}

/* ===== LEVEL 3: Nav group expandable (sub-groups inside packages) ===== */
.nav-group-expandable {
    margin: 0;
}

.nav-group-expandable > .nav-group-trigger {
    padding-left: 72px !important; /* Same indent as leaf links */
    font-weight: var(--font-medium) !important;
    opacity: 0.9;
}

.nav-group-expandable > .nav-group-trigger .nav-exp-caret {
    left: 60px; /* Tight to text — 72px text start minus caret width (~12px) */
    top: 50%;
    transform: translateY(-50%) rotate(0deg);
}

.nav-group-expandable.expanded > .nav-group-trigger .nav-exp-caret {
    transform: translateY(-50%) rotate(90deg);
}

/* ===== LEVEL 4: Group leaf links (pages inside nav groups) ===== */
.nav-submenu-group .nav-group-leaf {
    padding-left: 82px !important; /* Closer to parent group (72+10 instead of +16) */
    font-size: var(--text-sm) !important;
    position: relative;
}

/* Small dot bullet for group children — visual connector */
.nav-submenu-group .nav-group-leaf::after {
    content: '';
    position: absolute;
    left: 72px;
    top: 50%;
    transform: translateY(-50%);
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--gray-400);
    transition: background 0.15s ease;
}

.nav-submenu-group .nav-group-leaf:hover::after,
.nav-submenu-group .nav-group-leaf.active::after {
    background: #C99700;
}

.nav-submenu-group .nav-group-leaf i {
    display: none;
}

/* Level 4 group leaf active */
.nav-submenu-group .nav-group-leaf.active {
    background: rgba(201, 151, 0, 0.2);
    opacity: 1;
    font-weight: var(--font-medium) !important;
    color: #2D1F05;
    margin-right: var(--space-2);
}

/* ===== HOVER STATES ===== */
.admin-nav-link.nav-sublink:hover,
.mgmt-nav-link.nav-sublink:hover {
    opacity: 1;
    background: rgba(255, 255, 255, 0.08);
    font-weight: var(--font-normal) !important;
}

/* ===== ACTIVE PILL STATES ===== */
/* All items already extend to sidebar edge — active only changes colors */

/* Level 2 sublink active */
.admin-nav-link.nav-sublink.active,
.mgmt-nav-link.nav-sublink.active {
    /* GOLD DECISIONS: sublinks match the parent active state (this 0-3-0
       rule out-specificities .admin-nav-link.active - keep them in sync). */
    background: var(--btn-primary-bg, #C4B454);
    opacity: 1;
    font-weight: var(--font-medium) !important;
    color: var(--btn-primary-text, #000000);
    margin-right: var(--space-2);
}

/* Level 3 leaf active */
.nav-submenu-nested .nav-leaf-link.active {
    background: rgba(201, 151, 0, 0.2);
    opacity: 1;
    font-weight: var(--font-medium) !important;
    color: #2D1F05;
    margin-right: var(--space-2);
}

/* ===== PARENT TRIGGER HIGHLIGHTING ===== */
.nav-expandable-trigger.has-active-descendant {
    background: rgba(255, 255, 255, 0.05);
}

/* L1 trigger: solid gold when collapsed with active child */
.nav-expandable-trigger.active {
    background: var(--btn-primary-bg, #C4B454) !important;
    color: var(--btn-primary-text, #000000) !important;
}

.nav-expandable-trigger.active i,
.nav-expandable-trigger.active .svg-icon {
    color: #fff !important; /* R2: white glyph on the gold pill */
}

/* Caret color in active state */
.nav-expandable-trigger.active::before {
    color: white !important;
}

.nav-expandable-trigger.active i {
    color: white !important;
}

/* L1 gold border when expanded without active child */
.admin-nav > .nav-expandable.expanded:not(.has-active-child) > .nav-expandable-trigger,
.mgmt-nav > .nav-expandable.expanded:not(.has-active-child) > .nav-expandable-trigger {
    border: 2px solid rgba(201, 151, 0, 0.6) !important;
    border-left: none !important;
}

/* Collapsed sidebar handling */
.admin-sidebar.collapsed .nav-submenu {
    display: none !important;
}

/* ==========================================================================
   RESPONSIVE FOUNDATION (Phase 0)

   Breakpoint conventions for all NEW rules (media queries cannot use var(),
   so these are literal conventions - documented here, the shared source
   nearest the top of all three bundles):
     phone:  max-width: 640px
     tablet: max-width: 1024px  (the admin sidebar drawer currently keys on
             992px; Phase 1 moves it to 1024px)
   Legacy 767/768/769/991 queries are grandfathered, not rewritten here.
   ========================================================================== */

/* One shared scroll container for wide tables: the TABLE scrolls, never the
   page. Every .data-table should sit directly inside one of these. */
.table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    max-width: 100%;
}

/* (content-header phone margins live in shared/_responsive.css) */


/* (.table-cards recipe lives in shared/_responsive.css - the tail - so
   later .data-table base rules can never out-cascade it.) */



/* ==========================================================================
   HUB-TOGGLE - THE toggle primitive (platform rule, 2026-07-31). One switch
   implementation: track + thumb + visually-hidden checkbox, gold on-state,
   #75692E focus ring (>=3:1), disabled state, and a 44px hit area at touch
   widths. Same law as buttons: no page-local switch reinventions - the
   component gate flags ad-hoc *-switch/*-toggle classes. (The notifications
   page's bespoke .notif-switch shipped 0x0 because the admin label rule
   blockified it - that class of bug is why this primitive exists.)
   Markup: <label class="hub-toggle"><input type="checkbox" ...>
           <span class="hub-toggle-track" aria-hidden="true"></span></label>
   ========================================================================== */
.hub-toggle {
    position: relative;
    display: inline-flex;
    align-items: center;
    flex: none;
    cursor: pointer;
    margin: 0;
}
.hub-toggle input {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
}
.hub-toggle-track {
    display: inline-block;
    width: 34px;
    height: 20px;
    border-radius: 99px;
    background: var(--gray-400, #c8c6c4);
    transition: background 150ms ease;
    position: relative;
}
.hub-toggle-track::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
    transition: transform 150ms ease;
}
.hub-toggle input:checked + .hub-toggle-track { background: var(--gold, #C4B454); }
.hub-toggle input:checked + .hub-toggle-track::after { transform: translateX(14px); }
/* Focus ring: gold-text #75692E (5.5:1 on white) - the tint-only ring fails
   the 3:1 focus floor (glyph-contrast gate precedent). */
.hub-toggle input:focus-visible + .hub-toggle-track {
    outline: none;
    box-shadow: 0 0 0 2px var(--gold-text, #75692E);
}
.hub-toggle input:disabled + .hub-toggle-track { opacity: 0.55; cursor: default; }
.hub-toggle:has(input:disabled) { cursor: default; }
/* Touch widths: extend the HIT AREA to 44px without inflating the visual
   track (row-actions precedent: exemptions never waive accessibility). */
@media (max-width: 1024px), (pointer: coarse) {
    .hub-toggle {
        min-width: 44px;
        min-height: 44px;
        justify-content: center;
    }
}

/* ==========================================================================
   HUB-UPLOAD - the upload field (2026-09-07). ONE real <input type=file> per
   field; the Camera / Photos / Files sheet (.hub-drawer--sheet in
   shared/drawer.css) feeds it. Markup: Hub\Components\UploadSheet::render();
   behavior: public/assets/js/hub-upload-sheet.js (adds .hub-upload--enhanced).

   JS OFF: the native input is a normal FORMS-rule control (15px, 12px radius,
   1px --line, 52px tall as measured) and the trigger stays [hidden]. That
   [hidden] rule is load-bearing: .hub-btn { display:inline-flex } (0,1,0)
   beats the UA [hidden] and would paint a dead button beside the working input.
   JS ON: the input collapses to 1px at the field's top-left (the .hub-toggle
   recipe: still focusable, still required, still what the form posts), so an
   empty required submit anchors here instead of throwing "invalid form control
   is not focusable"; the trigger is the control people see.
   ========================================================================== */
.hub-upload {
    position: relative;
    display: block;
}
.hub-upload__label {
    display: block;
    margin: 0 0 8px;
    font-weight: 600;           /* font-size inherits, exactly like .form-field__label */
    color: var(--ink, #191712);
}
.hub-upload__input {
    display: block;
    width: 100%;
    min-height: 44px;
    padding: 10px 12px;
    box-sizing: border-box;
    font-family: inherit;
    font-size: 15px;
    color: var(--ink, #191712);
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
    background: var(--surface, #FFFFFF);
}
.hub-upload__input:focus-visible {
    outline: none;
    border-color: var(--gold, #C4B454);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold, #C4B454) 20%, transparent);   /* the input ring recipe (.hub-drawer-search) */
}
.hub-upload--enhanced .hub-upload__input {
    position: absolute;
    top: 0;
    left: 0;
    width: 1px;
    height: 1px;
    min-height: 0;
    padding: 0;
    margin: 0;
    border: 0;
    opacity: 0;
    overflow: hidden;
    pointer-events: none;
}
.hub-upload__trigger {
    width: 100%;                /* phone: full form width - 317px at 375, 262px at 320 */
}
.hub-upload__trigger[hidden] {
    display: none;
}
@media (min-width: 641px) {
    .hub-upload__trigger { width: auto; }   /* 201px at 800+; 44px tall to 1024, 36px above */
}
.hub-upload__name {
    margin: 8px 0 0;
    font-size: 13px;
    color: var(--ink-mute, #6F6A5C);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.hub-upload__name:empty {
    display: none;
}
.hub-upload__error {
    margin: 8px 0 0;
    font-size: 13px;
    font-weight: 600;
    color: var(--chip-danger-text, #A4262C);   /* the brief's semantic danger */
}
.hub-upload__error[hidden] {
    display: none;
}
.hub-upload__hint {
    margin: 8px 0 0;
    font-size: 13px;            /* the hint floor; receipt.php's old 12px line retires */
    line-height: 1.45;
    color: var(--ink-mute, #6F6A5C);
}

/* ---------- The upload overlay (hub-upload-sheet.js) ----------
   Richard, 2026-09-07: "toss uploading over the top with an awesome
   animation (imagine how Google does theirs) so we can't move on before it
   completes." Above the sheet (10010) and the nav (10000-10051): the scrim
   takes every tap while an upload is in flight; the JS disables the buttons
   underneath for the keyboard. The ring is driven by real XHR upload progress;
   on success it hands over to the platform's success beat - the gold disc
   pops and the white check draws (the flow success screen's own recipe,
   restated here because flow-wizard.css ships in the hub bundle only). */
/* SUBMITTING (hub-submit.js): the pressed button says so and spins; the
   card below is the upload overlay reused in its indeterminate state. The
   body class keeps the page from scrolling under the scrim, as the upload
   hold does. Reduced motion: the ring does not sweep (see hub-upload-sweep). */
.hub-btn.is-submitting {
    position: relative;
    padding-left: 34px;
    cursor: progress;
}
.hub-btn.is-submitting::before {
    content: "";
    position: absolute;
    left: 12px;
    top: 50%;
    width: 14px;
    height: 14px;
    margin-top: -7px;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: hub-submit-spin 700ms linear infinite;
}
@keyframes hub-submit-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .hub-btn.is-submitting::before { animation: none; border-right-color: currentColor; opacity: 0.6; }
}
body.hub-submit-hold-open { overflow: hidden; }

.hub-upload-progress {
    position: fixed;
    inset: 0;
    z-index: 10060;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
}
.hub-upload-progress[hidden] { display: none; }
.hub-upload-progress__scrim {
    position: absolute;
    inset: 0;
    background: rgba(25, 23, 18, 0.55);      /* --ink at 55%: the drawer scrim, a shade deeper */
    animation: hub-upload-fade 200ms ease;
}
.hub-upload-progress__card {
    position: relative;
    width: min(100%, 320px);
    padding: 28px 24px 20px;
    background: var(--surface, #FFFFFF);
    border-radius: 12px;
    box-shadow: 0 1px 2px rgba(23, 21, 15, 0.06), 0 8px 24px rgba(23, 21, 15, 0.08);
    text-align: center;
    animation: hub-upload-rise 250ms cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes hub-upload-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes hub-upload-rise { from { opacity: 0; transform: translateY(12px) scale(0.98); } to { opacity: 1; transform: none; } }

/* The ring: a --line track, a gold arc whose dashoffset IS the progress.
   Before the first byte moves (Preparing) the arc sweeps indeterminately. */
.hub-upload-progress__ring {
    position: relative;
    width: 96px;
    height: 96px;
    margin: 0 auto 16px;
}
.hub-upload-progress__ring svg { width: 100%; height: 100%; display: block; transform: rotate(-90deg); }
.hub-upload-progress__track { fill: none; stroke: var(--line, #E6E2D2); stroke-width: 5; }
.hub-upload-progress__bar {
    fill: none;
    stroke: var(--gold, #C4B454);
    stroke-width: 5;
    stroke-linecap: round;
    transition: stroke-dashoffset 180ms linear;
}
/* INDETERMINATE - a submit hold has no percentage to show, so the ring carries
   the whole message on its own.

   WHY THIS WAS REWRITTEN (owner, 2026-09-15: "our Submitting animation doesnt
   actually animate"). The old rule pinned stroke-dashoffset with !important and
   then asked a keyframe to animate that exact property. An !important
   declaration beats an animation, so the arc was frozen at offset 0 and the
   ring sat perfectly still through the entire wait it exists to cover. The
   keyframe was correct; it was never allowed to apply.

   TWO MOTIONS, deliberately. The svg turns at a constant rate while the arc
   itself opens and closes. A fixed-length arc at a fixed speed reads as a dead
   loop within about two seconds - the changing length is what keeps it looking
   like work being done rather than a decoration that happens to rotate. */
.hub-upload-progress:not(.is-uploading):not(.is-done) .hub-upload-progress__ring svg {
    transform-origin: 50% 50%;
    animation: hub-upload-turn 1.6s linear infinite;
}
.hub-upload-progress:not(.is-uploading):not(.is-done) .hub-upload-progress__bar {
    transition: none;                         /* the determinate easing fights the keyframe */
    animation: hub-upload-sweep 1.6s cubic-bezier(0.45, 0.05, 0.35, 1) infinite,
               hub-upload-glow 1.6s ease-in-out infinite;
}
/* The svg is parked at -90deg so a determinate arc starts at twelve o'clock;
   the turn has to begin there too or the ring jumps on its first frame. */
@keyframes hub-upload-turn {
    from { transform: rotate(-90deg); }
    to   { transform: rotate(270deg); }
}
/* r = 23, so the circumference is 144.5. The arc opens from a dot to two thirds
   of the circle and closes again, travelling as it goes. */
@keyframes hub-upload-sweep {
    0%   { stroke-dasharray: 6 139; stroke-dashoffset: 0; }
    50%  { stroke-dasharray: 95 50; stroke-dashoffset: -22; }
    100% { stroke-dasharray: 6 139; stroke-dashoffset: -144; }
}
/* NO FILTER ON A ROTATING ELEMENT. This used to breathe with
   drop-shadow, and a filter makes the browser rasterise that subtree into a
   filter REGION - a box. The box is a child of the svg, so it turned with the
   ring, and on a phone you could see it: a faint pale square wheeling behind
   the gold arc (owner photographed it, 2026-09-23: "Circle shows a box that
   has a faded look when at the top").

   The breath is kept, on stroke-opacity, which composites with no region and
   no box. The two real motions - the svg turning and the arc opening and
   closing - are untouched; they are what read as work being done. */
@keyframes hub-upload-glow {
    0%, 100% { stroke-opacity: 0.8; }
    50%      { stroke-opacity: 1; }
}
/* Reduced motion still gets an answer - see the media block below. */
@keyframes hub-upload-hold {
    0%, 100% { opacity: 0.4; }
    50%      { opacity: 1; }
}
.hub-upload-progress__pct {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--ink, #191712);
}
/* The success beat: disc and check hidden until .is-done, then the flow
   success screen's pop + draw. White check on gold: icons on gold are white (R2). */
.hub-upload-progress__disc { fill: var(--gold, #C4B454); transform-origin: center; transform: scale(0); }
.hub-upload-progress__mark { stroke: #FFFFFF; stroke-width: 4.5; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 34; stroke-dashoffset: 34; }
.hub-upload-progress.is-done .hub-upload-progress__track,
.hub-upload-progress.is-done .hub-upload-progress__bar { opacity: 0; transition: opacity 150ms; }
.hub-upload-progress.is-done .hub-upload-progress__disc { animation: hub-upload-pop 500ms cubic-bezier(0.22, 1.2, 0.36, 1) forwards; }
.hub-upload-progress.is-done .hub-upload-progress__mark { animation: hub-upload-draw 700ms ease-out 500ms forwards; }
@keyframes hub-upload-pop { to { transform: scale(1); } }
@keyframes hub-upload-draw { to { stroke-dashoffset: 0; } }
/* the check is drawn in the svg's -90deg frame; undo it for the glyph only */
.hub-upload-progress__disc, .hub-upload-progress__mark { transform-box: fill-box; }
.hub-upload-progress.is-done svg { transform: none; }

.hub-upload-progress__phase {
    margin: 0;
    font-size: 15px;
    font-weight: 700;
    color: var(--ink, #191712);
}
.hub-upload-progress__name {
    margin: 4px 0 0;
    font-size: 13px;
    color: var(--ink-mute, #6F6A5C);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.hub-upload-progress__cancel { margin-top: 16px; }
.hub-upload-progress.is-done .hub-upload-progress__cancel { visibility: hidden; }
/* Nothing underneath scrolls while the page is held. */
body.hub-upload-progress-open { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
    .hub-upload-progress__scrim,
    .hub-upload-progress__card,
    .hub-upload-progress:not(.is-uploading):not(.is-done) .hub-upload-progress__ring svg,
    .hub-upload-progress:not(.is-uploading):not(.is-done) .hub-upload-progress__bar,
    .hub-upload-progress.is-done .hub-upload-progress__disc,
    .hub-upload-progress.is-done .hub-upload-progress__mark { animation: none; }
    /* Reduced motion asks for no MOVEMENT, not for no feedback - and this card
       exists entirely for the person who cannot tell whether their tap
       registered. So nothing travels, but a fixed two-thirds arc breathes in
       place, which says "working" without anything moving across the screen. */
    .hub-upload-progress:not(.is-uploading):not(.is-done) .hub-upload-progress__bar {
        stroke-dasharray: 95 50;
        stroke-dashoffset: 0;
        animation: hub-upload-hold 2s ease-in-out infinite;
    }
    .hub-upload-progress__bar { transition: none; }
    .hub-upload-progress.is-done .hub-upload-progress__disc { transform: scale(1); }
    .hub-upload-progress.is-done .hub-upload-progress__mark { stroke-dashoffset: 0; }
}

/* ==========================================================================
   WORK ORDER DETAIL (/management/work_orders/{id})
   Base = mobile-first single column. The two-column desktop layout, the
   sticky right rail, the pinned bottom action bar, and the breadcrumb
   collapse all live in _responsive.css (media queries stay in the tail).
   Actions always FILL their row (never auto-width/left-clustered); context-
   aware gold primary; 44px+ touch targets.
   ========================================================================== */

/* Let the content column grow to its content (not clamp to one viewport) so a
   sticky rail pins for the whole scroll — same enablement as form pages. */
.admin-main-content.wo-detail {
    height: auto;
    min-height: 100%;
    max-width: none;   /* kills the legacy 760px cap for this page */
}

.wo-grid {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    min-width: 0;
}

/* display:contents so the side/main children reorder in one column on mobile;
   desktop turns these back into real grid tracks. */
.wo-col-main,
.wo-col-side {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    min-width: 0;
}

.wo-titlebar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}
.wo-title {
    margin: 0;
    font-size: 1.5rem;
    line-height: 1.2;
    color: var(--ink);
    min-width: 0;
    overflow-wrap: anywhere;
}
.wo-cap { text-transform: capitalize; }

.wo-flash {
    background: var(--gold-tint);
    border: 1px solid var(--gold-border);
    color: var(--gold-text);
    padding: var(--space-3) var(--space-4);
    border-radius: 12px;
}

.wo-card {
    background: white;
    border: 1px solid var(--gray-300);
    border-radius: 12px;
    padding: var(--space-4);
    min-width: 0;
}
.wo-card__title {
    margin: 0 0 var(--space-3);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ink-mute);
}

/* Details as a compact 2-column definition grid (never a tall skinny list). */
.wo-defgrid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-3) var(--space-4);
    margin: 0;
}
.wo-defgrid > div { min-width: 0; }
.wo-defgrid dt {
    margin: 0 0 2px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--ink-mute);
}
.wo-defgrid dd {
    margin: 0;
    font-size: 14px;
    color: var(--ink);
    overflow-wrap: anywhere;
}

.wo-desc-body { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); }

/* Form controls */
.wo-input {
    width: 100%;
    padding: 10px 12px;
    border: 1px solid var(--gray-400);
    border-radius: 8px;
    margin-bottom: var(--space-2);
    font-family: inherit;
    font-size: 14px;
    box-sizing: border-box;
    background: white;
    color: var(--ink);
}
.wo-textarea { resize: vertical; min-height: 60px; }
.wo-check {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    font-size: var(--text-sm);
    margin-bottom: var(--space-3);
    cursor: pointer;
    color: var(--ink-soft);
}
.wo-check input { margin-top: 2px; flex-shrink: 0; }
.wo-hint { margin: var(--space-2) 0 0; font-size: 12px; color: var(--ink-mute); }
.wo-muted { color: var(--ink-mute); }

/* BUTTON RULE: within any group, buttons share equal width and FILL the row —
   no auto-width, no left-cluster. 44px min height on this page (touch floor
   applied at every width, not just <=1024). */
.wo-detail .hub-btn { height: 44px; }
.wo-btnstack { display: flex; flex-direction: column; gap: var(--space-2); }
.wo-btnstack .wo-btn-block { width: 100%; }
.wo-btnrow { display: flex; gap: var(--space-2); }
.wo-btnrow .wo-btn-block { flex: 1 1 0; min-width: 0; }
.wo-btn-comment { width: 100%; }
/* Ghost buttons carry a 1.5px border; the gold primitive has none. In a
   flex:1 group Chrome adds that border ON TOP of the distributed size, so a
   bordered ghost lands ~2px wider than the borderless gold. A matching
   TRANSPARENT border on the WO gold buttons equalises the border-box geometry
   (no visual change) so every equal-width group is pixel-equal. */
.wo-detail .hub-btn--gold { border: 1.5px solid transparent; }

/* Activity: comments + history */
.wo-comment {
    background: white;
    border: 1px solid var(--gray-300);
    border-radius: 12px;
    padding: var(--space-3);
    margin-bottom: var(--space-2);
}
.wo-comment-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-2);
    font-size: 12px;
    color: var(--ink-mute);
}
.wo-history-title { margin-top: var(--space-5); }
.wo-history { border-left: 2px solid var(--gray-300); padding-left: var(--space-4); }
.wo-history-row { margin-bottom: var(--space-3); }
.wo-history-line { font-size: var(--text-sm); color: var(--ink); }

/* Pinned bottom action bar (mobile/tablet). Shown < 1024 in _responsive.css;
   hidden by default here so no-CSS / desktop never floats it. */
.wo-actionbar-wrap { display: none; }
.wo-actionbar { display: flex; gap: var(--space-2); }
.wo-actionbar .wo-bar-btn { flex: 1 1 0; min-width: 0; }
.wo-sheet { display: block; }
.wo-sheet[hidden] { display: none; }


/* ==========================================================================
   HUB-RANGECAL - the two-click date-range calendar (hub-rangecal.js).

   Richard, 2026-08-25: "you select it on calendar, then calendar stays you
   select the 2nd date and it creates a filled in path between." Endpoints are
   gold PILLS on the day buttons; the filled-in path is a BAND on the cells -
   full tint strictly between the endpoints, and a half-gradient under each
   endpoint so the band meets its pill without a white notch. The engine
   mirrors every state on both the button (__day--*) and its cell (__cell--*),
   and endpoints never carry --between (verified in paint()), which is why the
   halves are separate rules.

   Preview - after the first click, before the second - is the same band at
   half strength; those cells carry BOTH --between and --preview, so the
   preview rule sits after the band rule to win.
   ========================================================================== */

.hub-rangecal {
    margin-top: 4px;
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
    padding: 14px 16px 16px;
    background: var(--flow-surface, #fff);
}

.hub-rangecal__bar {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 8px;
}
.hub-rangecal__summary {
    font-weight: 600;
    color: var(--ink, #191712);
    min-height: 1.4em;
}
.hub-rangecal__clear { margin-left: auto; }

.hub-rangecal__head {
    display: flex;
    justify-content: space-between;
    margin-bottom: 2px;
}

.hub-rangecal__months {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 12px 24px;
}
.hub-rangecal__month-label {
    text-align: center;
    font-weight: 600;
    color: var(--ink, #191712);
    margin-bottom: 6px;
}

.hub-rangecal__dow,
.hub-rangecal__week {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
}
.hub-rangecal__dow-cell {
    text-align: center;
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ink-mute, #6F6A5C);
    padding: 4px 0 6px;
}

.hub-rangecal__cell { padding: 2px 0; }

.hub-rangecal__day {
    width: 100%;
    min-height: var(--btn-h-md, 36px);
    border: 0;
    background: transparent;
    border-radius: 999px;
    font: inherit;
    font-size: 14px;
    color: var(--ink, #191712);
    cursor: pointer;
    transition: background-color 0.15s ease;
}
.hub-rangecal__day[disabled] {
    color: var(--gray-400, #c8c6c4);
    cursor: default;
}
.hub-rangecal__day:not([disabled]):hover {
    background: var(--gold-tint, #F5EFD0);
}
/* Today: a quiet ring, only until it becomes an endpoint (the pill wins). */
.hub-rangecal__day--today {
    box-shadow: inset 0 0 0 1px var(--gold, #C4B454);
}

/* THE FILLED-IN PATH. Band strictly between the endpoints... */
.hub-rangecal__cell--between {
    background: var(--gold-tint, #F5EFD0);
}
/* ...half-width under each endpoint so it meets the pill (endpoints never
   carry --between; a single-day pick has no band at all)... */
.hub-rangecal__cell--start:not(.hub-rangecal__cell--single) {
    background: linear-gradient(to right, transparent 50%, var(--gold-tint, #F5EFD0) 50%);
}
.hub-rangecal__cell--end:not(.hub-rangecal__cell--single) {
    background: linear-gradient(to left, transparent 50%, var(--gold-tint, #F5EFD0) 50%);
}
/* ...capped cleanly at each week row's edges. */
.hub-rangecal__week > .hub-rangecal__cell--between:first-child { border-radius: 999px 0 0 999px; }
.hub-rangecal__week > .hub-rangecal__cell--between:last-child  { border-radius: 0 999px 999px 0; }

/* Preview: the path the second click WOULD commit, at half strength. After
   the band rule on purpose - preview cells carry both classes. */
.hub-rangecal__cell--preview {
    background: color-mix(in srgb, var(--gold-tint, #F5EFD0) 55%, transparent);
}

/* Endpoints and the single-day pick: gold pills on the button. */
.hub-rangecal__day--start,
.hub-rangecal__day--end,
.hub-rangecal__day--single {
    background: var(--gold, #C4B454);
    color: var(--btn-primary-text, #000000);
    font-weight: 700;
    box-shadow: none;
}
/* An endpoint stays a pill UNDER THE CURSOR. The generic hover tint above is
   (0,2,1) and the pill classes are (0,1,0), so hover won - and the pointer is
   parked on the exact day just tapped. Richard's report from his own
   screenshot: "they dont darken automatically when selected... Only after
   screen changes somehow" - the screen change was the pointer leaving. Same
   specificity as the hover rule, later in the file, so the pill wins. */
.hub-rangecal__day--start:not([disabled]):hover,
.hub-rangecal__day--end:not([disabled]):hover,
.hub-rangecal__day--single:not([disabled]):hover {
    background: var(--gold, #C4B454);
}

/* The would-be second endpoint under the cursor / focus while picking. */
.hub-rangecal__day--preview-end,
.hub-rangecal__day--preview-start {
    box-shadow: inset 0 0 0 2px var(--gold, #C4B454);
}

.hub-rangecal__day:focus-visible {
    outline: 2px solid var(--ink, #191712);
    outline-offset: 2px;
}

/* Touch: the density rule's 44px minimum for tap targets. */
@media (hover: none) {
    .hub-rangecal__day { min-height: var(--btn-h-touch, 44px); }
}

/* PHONES - the single-month layout (the engine shows two months from 768px,
   WIDE_QUERY in hub-rangecal.js). Richard, 2026-09-30, from his phone: "This
   on mobile expands too far vertical" - the last week sat under the Back /
   Next bar. The arrows had a row to themselves above the month name; here
   they join it (< September 2026 >), and the padding tightens. The day
   buttons keep their 44px touch height. The wrapper divs (__head, __months,
   __month) carry no role, so display: contents on them is safe and lets the
   arrows, the label and the grid share the calendar's own grid. */
@media (max-width: 767.98px) {
    .hub-rangecal {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        align-items: center;
        column-gap: 8px;
        padding: 10px 12px 12px;
    }
    .hub-rangecal__bar { grid-column: 1 / -1; margin-bottom: 4px; }
    .hub-rangecal__head,
    .hub-rangecal__months,
    .hub-rangecal__month { display: contents; }
    .hub-rangecal__nav--prev { grid-column: 1; grid-row: 2; }
    .hub-rangecal__nav--next { grid-column: 3; grid-row: 2; }
    .hub-rangecal__month-label { grid-column: 2; grid-row: 2; margin: 0; }
    .hub-rangecal__grid { grid-column: 1 / -1; grid-row: 3; margin-top: 4px; }
    .hub-rangecal__dow-cell { padding: 2px 0 4px; }
    .hub-rangecal__cell { padding: 0; }
}

/* The two native date fields while the calendar drives them: visually hidden,
   NOT display:none - they must keep submitting, and a focused native still
   scrolls into place. Only ever applied by the engine after a successful
   boot, so a failed script leaves them fully visible. */
.hub-rangecal-host .hub-rangecal-native {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    clip-path: inset(50%);
    overflow: hidden;
    white-space: nowrap;
}

/* The step's server-rendered hint ("Leave the last day blank if you only need
   the one day.") is the RIGHT instruction for the two native inputs and the
   WRONG one over the calendar, where a single day is the same date clicked
   twice. Hidden only when the calendar actually rendered - no JS, no
   -host class, hint stays. */
.flow-body:has(.hub-rangecal-host) .flow-info { display: none; }

@media (prefers-reduced-motion: reduce) {
    .hub-rangecal__day { transition: none; }
}

/* Two months side by side need ~540px of card; the wizard reading measure
   (--flow-maxw: 34rem, flow-wizard.css) is right for one question and a list
   of answers, and wrong for a calendar. Widened ONLY while a successfully
   rendered calendar is inside - no JS, no -host class, and the step keeps the
   reading measure its two native inputs are happy in. Wins over the base
   max-width on specificity (0,2,0 vs 0,1,0), not on source order. */
.flow-wizard:has(.hub-rangecal-host),
.stepper:has(.hub-rangecal-host) {
    max-width: 40rem;
}

/* ==========================================================================
   ACT AS BANNER (\Hub\Components\ActingAsBanner)
   ==========================================================================
   The bar a super admin sees the entire time they are wearing someone else's
   account. It is the safety mechanism of that feature: unmissable, on every
   page, carrying its own way out.

   PINNED TO THE BOTTOM, DELIBERATELY. The top edge is occupied three
   different ways - .admin-header/.mgmt-header are sticky top:0, the hub
   navbar is fixed, section pages bring their own - so a top bar would need an
   offset correction per layout and would be silently painted over by the next
   layout added.

   THE BOTTOM EDGE IS NOT EMPTY EITHER, and pretending it was is a bug this
   nearly shipped with. Staff pages run the BOTTOM CHROME MODEL (footer.css,
   user decision 2026-07-19): footer.footer is pinned at bottom:0 and the
   action bars - .flow-actions, .form-action-bar - stack flush above it off
   --footer-height / --footer-locked-h. A banner dropped at bottom:0 with a
   winning z-index does not join that stack, it covers it: the Save button on
   /profile.php goes under the bar and the page cannot be submitted. While
   acting as somebody, which is when you are most likely to be filling in
   their form.

   So the banner INSERTS ITSELF AS A NEW BOTTOM LAYER instead. It takes
   bottom:0 and everything that was anchored there moves up by --actas-h,
   which is the same one-number-in-one-place discipline the footer height
   already uses. Every rule is scoped to body.hub-actas-on, so a page where
   nobody is acting - which is all of them, nearly all of the time - computes
   exactly what it did before. The 0,2,0 selectors out-rank the 0,1,0 base and
   media-query rules on specificity, so this works from here and does not need
   to live in the responsive tail.

   DARK SURFACE, GOLD TEXT - the R1b case: gold AS TEXT on a dark surface is
   #C4B454 at full opacity (never --gold-text, which is the light-surface
   value). The stop button is a gold fill, so its label is BLACK per R1. No
   new color literals: every value here is an existing token. */
body.hub-actas-on {
    /* FALLBACK ONLY, and deliberately larger than the bar actually measures
       (50px at desktop). ActingAsBanner::measureScript() overwrites this with
       the real height as an inline style the moment the page loads. Erring
       high means a script failure leaves a visible gap; erring low would put
       the footer and the Save bar underneath the banner, which is the bug
       this whole variable exists to prevent. */
    --actas-h: 60px;
    padding-bottom: var(--actas-h);
}

.hub-actas {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 100000;
    min-height: var(--actas-h, 44px);
    display: flex;
    align-items: center;
    gap: var(--space-3, 12px);
    padding: 10px var(--space-4, 16px);
    padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    background: var(--ink, #191712);
    border-top: 2px solid var(--gold, #C4B454);
    color: var(--gold, #C4B454);
    font-size: var(--text-sm, 0.875rem);
    line-height: 1.35;
    box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.28);
}

/* The rest of the bottom stack, re-anchored on top of the new layer. Each
   restates its own base offset plus --actas-h rather than inventing one, so
   changing the footer height still changes them all from one place. */
body.hub-actas-on footer.footer {
    bottom: var(--actas-h);
}

body.hub-actas-on .flow-actions,
body.hub-actas-on .form-action-bar {
    bottom: calc(var(--footer-height, 40px) + var(--actas-h) + env(safe-area-inset-bottom, 0px));
}

/* !important to match the base .has-action-bar rule (section-shell.css marks
   it a safety invariant); 0,2,0 out-ranks it. */
body.hub-actas-on .has-action-bar {
    padding-bottom: calc(var(--footer-height, 40px) + var(--action-bar-height, 73px) + var(--actas-h) + env(safe-area-inset-bottom, 0px) + 12px) !important;
}

/* The console shell is a 100dvh grid; without this the body padding above
   would add a permanent 44px of scroll to every admin page. */
body.hub-actas-on .admin-shell {
    min-height: calc(100dvh - var(--actas-h));
}

/* A steady dot, not a pulse: this bar is present for the whole session, and
   something blinking in the corner of every page for ten minutes stops being
   read. prefers-reduced-motion has nothing to turn off here. */
.hub-actas__mark {
    flex: 0 0 auto;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--gold, #C4B454);
}

.hub-actas__text {
    flex: 1 1 auto;
    min-width: 0;
}

.hub-actas__text strong {
    color: var(--gold, #C4B454);
    font-weight: 700;
}

/* The email and the "signed in as" tail are supporting detail. They stay on
   the same gold rather than dimming to a mute: --ink-mute is a light-surface
   token and computes 2.2:1 here. Weight does the de-emphasis instead. */
.hub-actas__sub {
    opacity: 0.85;
}

.hub-actas__form {
    flex: 0 0 auto;
    margin: 0;
}

/* Gold fill, black label (R1). Stated rather than reached for via
   .hub-btn--gold because this button lives on a dark surface where the
   primitive's focus ring token would land at 1.4:1 - the ring is restated
   below in the banner's own terms. */
.hub-actas__stop {
    background: var(--gold, #C4B454);
    border-color: var(--gold, #C4B454);
    color: #000000;
    font-weight: 600;
    white-space: nowrap;
}

.hub-actas__stop:hover {
    background: var(--gold-press, #A89940);
    border-color: var(--gold-press, #A89940);
    color: #000000;
}

.hub-actas__stop:focus-visible {
    outline: 2px solid #FFFFFF;
    outline-offset: 2px;
}

/* Phones: the identity is what must survive the narrow measure, so the bar
   stacks and the email drops to its own line rather than truncating the name
   next to it. The button keeps its full width target. --actas-h grows with
   it, and every offset above follows because they all read the variable.
   768px, not 600px: that is where footer.css locks the footer to
   --footer-locked-h and the action bars re-anchor, so the whole bottom stack
   changes shape on ONE breakpoint. */
@media (max-width: 768px) {
    body.hub-actas-on {
        /* Fallback for the wrapped two-row layout; measures ~141px live. */
        --actas-h: 152px;
    }

    .hub-actas {
        flex-wrap: wrap;
        gap: var(--space-2, 8px);
        font-size: var(--text-xs, 0.8125rem);
    }

    .hub-actas__text {
        flex: 1 1 100%;
        order: 2;
    }

    .hub-actas__sub {
        display: block;
    }

    .hub-actas__form {
        order: 3;
        flex: 1 1 100%;
    }

    .hub-actas__stop {
        width: 100%;
    }

    body.hub-actas-on .flow-actions,
    body.hub-actas-on .form-action-bar {
        bottom: calc(var(--footer-locked-h, 30px) + var(--actas-h) + env(safe-area-inset-bottom, 0px));
    }

    body.hub-actas-on .main-content {
        padding-bottom: calc(var(--footer-locked-h, 30px) + var(--actas-h) + env(safe-area-inset-bottom, 0px) + 12px);
    }

    body.hub-actas-on .has-action-bar {
        padding-bottom: calc(var(--footer-locked-h, 30px) + var(--action-bar-height, 73px) + var(--actas-h) + env(safe-area-inset-bottom, 0px) + 12px) !important;
    }
}

/* Never on paper. A printed reimbursement or work order is a document about
   the record, not about the session that produced it. */
@media print {
    .hub-actas {
        display: none;
    }

    body.hub-actas-on {
        --actas-h: 0px;
        padding-bottom: 0;
    }
}

/* The same fact in the activity log's User column: the actor's name in
   normal weight, and beneath it, who they were standing in for. --gold-text
   is the LIGHT-surface gold (R1b) - this one sits on a white table row, so it
   is the correct end of the ramp, unlike the banner's. */
.log-acting-as {
    margin-top: 2px;
    font-size: var(--text-xs, 0.75rem);
    font-weight: 600;
    color: var(--gold-text, #75692E);
}

/* ========== shared/footer.css ========== */

/* ==========================================================================
   FOOTER STYLES
   Shared across Hub and Dashboard pages
   ========================================================================== */

/* Footer Container - shared visual recipe for the staff footer and the
   admin-shell footer. POSITIONING is deliberately split below: the two live
   in different layout models. */
footer.footer,
.admin-footer {
    background: var(--footer-bg-color, #F3F4F6);
    border-top: 1px solid var(--border-primary, #e0e0e0);
    display: flex;
    align-items: center;
    padding: 0 2rem;
    font-size: var(--footer-text-size, 0.875rem);
    color: var(--footer-text-color, #E5E7EB); /* dark-bar fallback */
    min-height: var(--footer-height, 40px);
    max-height: var(--footer-height, 40px);
    height: var(--footer-height, 40px);
    overflow: hidden;
    z-index: 10;
}

/* BOTTOM CHROME MODEL (user decision 2026-07-19, FINAL - supersedes the
   flex sticky-footer round): the staff footer is ALWAYS visible - pinned to
   the viewport at every width. Its MANDATORY companion is the content
   clearance on .main-content (shared/base.css): the last time this footer
   was pinned it hid content at full scroll, which is why it got unpinned -
   pin + padding is the fix, never pin alone. Action bars (.flow-actions,
   .form-action-bar) stack flush above it. */
footer.footer {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* GUARD: if any admin-console page ever includes the staff footer partial
   (Layout::renderFooter emits class="footer" for every pageType), it must
   NOT pin - the admin bundles carry no .main-content companion padding, so
   a pinned footer there would hide content (the historic bug). */
.admin-root footer.footer {
    position: static;
}

/* Print: a fixed footer repeats over the bottom of every printed sheet. */
@media print {
    footer.footer {
        position: static;
    }
}

/* Admin console: UNCHANGED - the admin-shell grid places its own footer. */
.admin-footer {
    grid-row: 3;
    flex: 0 0 auto;
    position: static;
}

/* Footer Content Layout */
.footer-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 100%;
    max-width: 100%;
}

/* Footer Sections */
.footer-left,
.footer-right {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
}

/* Footer Separator */
.footer-separator {
    color: var(--text-disabled, #9CA3AF);
    user-select: none;
}

/* Logged-in user name. The footer surface is DARK (#111827 via
   --footer-bg-color), so gold text here is the bright dark-surface gold
   (--gold-text-dark); #75692E is the LIGHT-surface gold and goes muddy
   on dark. Never opacity-dim it either. */
.footer-user-name {
    color: var(--gold-text-dark, #C4B454);
    font-weight: 600;
}

.footer-content a {
    color: var(--footer-text-color, #E5E7EB); /* dark-bar fallback */
    text-decoration: none;
    transition: color 0.2s ease;
}

.footer-content a:hover {
    color: var(--gold-text-dark, #C4B454); /* gold on the dark footer */
}

/* ==========================================================================
   RESPONSIVE
   ========================================================================== */

/* Phones: COMPACT, NOT STACKED - one tight row, left cluster left, right
   cluster right, graceful wrap only when truly needed. The old stack-and-pad
   block here (plus a column !important in shared-media.css, now removed) made
   the footer a ~59px block of dead space. */
@media (max-width: 768px) {
    /* Published where every rule can see it, not just the footer's own. */
    :root {
        --footer-locked-h: 30px;
    }

    footer.footer,
    .admin-footer {
        /* keep the notch/home-bar clearance under the compact row */
        padding: 5px 10px calc(5px + env(safe-area-inset-bottom, 0px));
        font-size: 0.875rem; /* 14px. 13px against a 16px body reads as fine
                                print on a phone. The locked row below is sized
                                to fit it rather than clip it. */
        /* LOCKED height (border-box): the action bars pin at exactly this
           offset (see the bottom-chrome sync block in shared/_responsive.css)
           - a content-driven height left a see-through gap above the footer.
           One row fits (1px border + 10px padding + ~16px line).

           --footer-locked-h is THE definition. _responsive.css reads this same
           property instead of repeating the number; it used to be written out
           four more times there under a comment asking for them to be kept in
           lockstep by hand, which is not a thing that survives contact. */
        --footer-locked-h: 30px;
        box-sizing: border-box;
        min-height: 0;
        max-height: none;
        height: calc(var(--footer-locked-h) + env(safe-area-inset-bottom, 0px));
        overflow: hidden;
    }

    .footer-content {
        flex-direction: row;
        flex-wrap: wrap;
        justify-content: space-between;
        align-items: center;
        gap: 2px 10px;
        text-align: left;
        padding: 0;
    }

    .footer-left,
    .footer-right {
        gap: 0.375rem;
        flex-wrap: wrap;
        line-height: 1.1; /* tight enough that 13px fits the 24px locked row */
    }
}

@media (max-width: 480px) {
    footer.footer,
    .admin-footer {
        /* Matches the 768px block. This rule exists at all because the size was
           once stepped down again here; leaving it at 13px meant every phone
           narrower than 480px silently kept the smaller text no matter what the
           768px block said. */
        font-size: 0.875rem; /* 14px */
    }

    .footer-separator {
        display: none;
    }

    /* The logged-in NAME stays visible at every width - it is useful. The words
       "Logged in as" do not: at 390px the two clusters measured 193px + 183px
       against 376px of usable width, so they overlapped and the locked row
       clipped the result. Dropping the label returns about 75px, which is what
       makes the row fit. */
    .label-loggedin {
        display: none;
    }
}

/* ========== shared/hub-components.css ========== */

/* ==========================================================================
   HEADER / NAVBAR STYLES
   Shared across Hub and Dashboard pages
   ========================================================================== */

/* Navbar Container */
.navbar {
    background: var(--header-bg-color, var(--navbar-color, var(--secondary-color))) !important;
    box-shadow: 0 2px 8px rgba(0, 0, 0, var(--shadow-heavy, 0.15));
    padding: 0;
    margin: 0;
    position: sticky;
    top: 0;
    z-index: 10000;
    grid-row: 1;
    overflow: visible;
    /* Allow dropdown menu to show */
    min-height: var(--header-height, 80px);
    max-height: var(--header-height, 80px);
    height: var(--header-height, 80px);
}

/* Nav Content Wrapper */
.nav-content {
    max-width: 100%;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0 2rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: relative;
    z-index: 10001;
}

/* Brand/Logo Section - THE ONE brand lockup (staff pages, flow pages, and
   the drawer header all render this via Layout::renderHeader). The wordmark
   gets real presence next to the logo: 21px/700 title, 13px/600 gold
   subtitle, and a >=20px gap so the logo's glow (blur capped at 10px in
   SiteSettings' theme CSS) never bleeds under the text. */
.nav-brand {
    display: flex;
    align-items: center;
    gap: 1.25rem; /* 20px: clears the capped logo glow */
    color: var(--header-text-color, white);
    text-decoration: none !important;
    transition: opacity 0.2s ease;
}

.nav-brand:hover {
    opacity: 0.9;
}

.nav-brand img {
    height: var(--logo-height, 50px) !important;
    width: auto;
}

.nav-brand-text {
    display: flex;
    flex-direction: column;
}

.nav-brand-title {
    /* LITERAL 21px: the brand lockup is a design constant, not a theme
       tunable - the stored --header-title-font-size (an old 1.0rem row)
       no longer drives it. */
    font-size: 1.3125rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    white-space: nowrap; /* the title NEVER wraps at any width */
    color: var(--header-text-color, white);
    font-family: var(--header-title-font, 'Roboto', sans-serif);
    line-height: 1.2;
}

.nav-brand-subtitle {
    font-size: 0.8125rem; /* 13px - literal, same reason as the title */
    /* Gold text on the DARK header = bright #C4B454 at full opacity.
       Never dim text with opacity - it muddies the gold (was 0.8). */
    color: var(--header-subtitle-color, #C4B454);
    font-weight: 600;
    font-family: var(--header-subtitle-font, 'Roboto', sans-serif);
    line-height: 1.2;
}

/* Navigation Links */
.nav-links {
    display: flex;
    align-items: center;
    gap: 0;
}

.nav-links>a {
    color: var(--header-text-color, white);
    text-decoration: none;
    padding: 0.5rem 20px;
    border-right: 1px solid rgba(255, 255, 255, 0.25);
    transition: all 0.2s ease;
    font-weight: 500;
    font-size: 0.95rem;
    display: flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
    line-height: 1;
}

.nav-links>a:first-of-type {
    border-left: 1px solid rgba(255, 255, 255, 0.25);
}

.nav-links>a:hover {
    opacity: 0.8;
}

.nav-links>a:last-of-type {
    border-right: 1px solid rgba(255, 255, 255, 0.25);
}

.nav-links>a i {
    font-size: 1rem;
    width: 16px;
    text-align: center;
}

/* Mobile Toggle Button — three-line hamburger → X animation */
.nav-toggle {
    display: none;
    background: transparent;
    border: none;
    color: var(--header-text-color, white);
    padding: 0.5rem;
    cursor: pointer;
    border-radius: 6px;
    /* 44px tap target - same rule the admin hamburger already follows */
    width: var(--tap-target, 2.75rem);
    height: var(--tap-target, 2.75rem);
    position: relative;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    transition: background 0.2s ease;
    z-index: 10051;
}

.nav-toggle span {
    display: block;
    width: 22px;
    height: 2px;
    background: var(--header-text-color, white);
    border-radius: 2px;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    transform-origin: center;
}

.nav-toggle:hover {
    background: rgba(255, 255, 255, 0.1);
}

/* Hamburger → X animation */
.nav-toggle.active span:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
}
.nav-toggle.active span:nth-child(2) {
    opacity: 0;
    transform: scaleX(0);
}
.nav-toggle.active span:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
}

/* User Profile Section */
.nav-user-profile {
    display: flex;
    align-items: center;
    gap: 12px;
    padding-left: 15px;
    color: var(--header-text-color, white);
}

.nav-user-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    object-fit: cover;
    border: 2px solid var(--primary-color);
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
}

/* Enterprise context - stronger border */
.admin-root .nav-user-avatar,
.mgmt-root .nav-user-avatar {
    border: 2.5px solid var(--gold); /* was --nd-gold (district-website gold); the ring is a fill-gold accent */
}

.nav-user-info {
    display: flex;
    flex-direction: column;
    line-height: 1.3;
}

.nav-user-name {
    font-weight: 600;
    font-size: 0.95rem;
    color: var(--header-subtitle-color, #C4B454); /* gold on dark = bright gold */
}

.nav-user-role {
    font-size: 0.8rem;
    /* Muted line under the name: bake the dim into an opaque color instead
       of opacity, which renders text at unpredictable computed colors. */
    color: color-mix(in srgb, var(--header-text-color, #FFFFFF) 80%, var(--header-bg-color, #000000));
    text-transform: capitalize;
}

/* ==========================================================================
   MOBILE MENU (Dashboard only)
   ========================================================================== */

/* Mobile menu overlay */
.mobile-menu-overlay {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 9999;
}

.mobile-menu-overlay.active {
    display: block;
}

/* Mobile menu container */
.mobile-menu {
    display: none;
    position: fixed;
    top: var(--header-height, 80px);
    left: 0;
    width: 250px;
    height: calc(100vh - var(--header-height, 80px));
    background: white;
    box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
    transform: translateX(-100%);
    transition: transform 0.3s ease;
    z-index: 10000;
    overflow-y: auto;
}

.mobile-menu.active {
    transform: translateX(0);
}

.mobile-menu-items {
    list-style: none;
    margin: 0;
    padding: 0;
}

.mobile-menu-items li {
    border-bottom: 1px solid var(--border-primary);
}

.mobile-menu-items a {
    display: flex;
    align-items: center;
    padding: 1rem 1.5rem;
    color: var(--text-primary);
    text-decoration: none;
    transition: all 0.2s ease;
    font-weight: 500;
}

.mobile-menu-items a:hover,
.mobile-menu-items a.active {
    background: var(--background-color, var(--gray-100));
    /* R1b: the staff drawer is WHITE (not dark) - light-surface gold here */
    color: var(--gold-text);
}

.mobile-menu-items i {
    margin-right: 12px;
    font-size: 1.1rem;
    width: 20px;
    text-align: center;
}

/* ==========================================================================
   USER PROFILE DROPDOWN (base styles)
   Used by both Hub and Dashboard pages. MUST stay ABOVE the RESPONSIVE
   section: the 992px drawer block overrides these at equal specificity,
   which only works if the base rules come first in source order.
   ========================================================================== */

.nav-user-dropdown {
    position: relative;
    display: inline-block;
    z-index: 10002 !important; /* Above nav-content (10001) */
    margin-left: auto;
}
.nav-user-trigger {
    display: flex;
    align-items: center;
    gap: 10px;
    background: none;
    border: none;
    cursor: pointer;
    padding: 5px 10px;
    border-radius: 8px;
    transition: background 0.3s ease;
}
.nav-user-trigger:hover {
    background: rgba(0, 0, 0, 0.05);
}
.nav-user-arrow {
    display: none !important;
}
.nav-user-dropdown.active .nav-user-arrow,
.nav-user-menu.show ~ .nav-user-trigger .nav-user-arrow {
    transform: rotate(180deg);
}

/* Dropdown Menu - Base Styles */
.nav-user-menu {
    position: absolute;
    top: 100%;
    right: -10px;
    margin-top: 10px;
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    min-width: 200px;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-10px);
    transition: all 0.3s ease;
    z-index: 10002;
    overflow: hidden;
}

/* Dropdown Menu - Visible State */
.nav-user-menu.show {
    opacity: 1 !important;
    visibility: visible !important;
    transform: translateY(0) !important;
    z-index: 10002 !important;
}
.user-menu-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    color: var(--primary-text, var(--gray-800));
    text-decoration: none;
    transition: background 0.2s ease;
}
.user-menu-item:hover {
    background: var(--hover-bg, var(--gray-100));
}
.user-menu-item:first-child {
    border-top-left-radius: 8px;
    border-top-right-radius: 8px;
}
.user-menu-item:last-child {
    border-bottom-left-radius: 8px;
    border-bottom-right-radius: 8px;
}
.user-menu-icon {
    font-size: 1.2em;
    width: 24px;
    text-align: center;
}
.user-menu-icon i {
    width: 16px;
    text-align: center;
}
.user-menu-divider {
    height: 1px;
    background: var(--border-color);
    margin: 5px 0;
}
/* User Info Display */
.nav-user-info {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
}
/* ==========================================================================
   NAVBAR OVERFLOW FIX
   Fix navbar overflow clipping dropdown - allow visible overflow
   ========================================================================== */

.navbar {
    overflow: visible !important;
}

/* ==========================================================================
   RESPONSIVE
   ========================================================================== */

/* Drawer item styling — breakpoint must match the slide-out panel
   (.nav-links fixed drawer) defined at 992px in shared/base.css */
@media (max-width: 992px) {
    .nav-user-info {
        display: none;
    }

    /* Slide-out panel link styling */
    .nav-links > a {
        padding: 0.875rem 1.25rem;
        font-size: 1rem;
        border-right: none;
        border-left: none;
        border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        width: 100%;
        display: flex;
        align-items: center;
        gap: 10px;
    }

    .nav-links > a:first-of-type {
        border-left: none;
        border-top: 1px solid rgba(255, 255, 255, 0.1);
    }

    .nav-links > a:last-of-type {
        border-right: none;
    }

    .nav-links > a:hover,
    .nav-links > a.active {
        background: rgba(255, 255, 255, 0.1);
        opacity: 1;
    }

    /* Push user profile/logout to bottom of slide-out panel */
    .nav-user-dropdown {
        width: 100%;
        margin-top: auto;
        border-top: 1px solid rgba(255, 255, 255, 0.15);
        padding: 1rem 1.25rem;
    }

    .nav-user-trigger {
        width: 100%;
        justify-content: flex-start;
    }

    /* Show user info in the slide-out panel */
    .nav-links .nav-user-info {
        display: flex !important;
    }

    .nav-user-menu {
        position: static;
        margin-top: 0.75rem;
        background: rgba(0, 0, 0, 0.15);
        border-radius: 6px;
        box-shadow: none;
        opacity: 1;
        visibility: visible;
        transform: none;
    }

    .nav-user-menu .user-menu-item {
        color: var(--header-text-color, white);
    }

    .nav-user-menu .user-menu-item:hover {
        background: rgba(255, 255, 255, 0.1);
    }

    .nav-toggle {
        display: flex;
        align-items: center;
        justify-content: center;
    }
}

@media (max-width: 768px) {
    .nav-content {
        padding: 0 1rem;
    }

    /* Mobile lockup: smaller logo (the MOBILE height var, not a scale of
       the 90px desktop one), 16px nowrap title, 11px subtitle - VISIBLE. */
    .nav-brand img {
        height: var(--logo-height-mobile, 50px) !important;
    }

    .nav-brand { gap: 0.875rem; }

    .nav-brand-title {
        font-size: 1rem; /* 16px; still nowrap from the base rule */
    }

    .nav-brand-subtitle {
        font-size: 0.6875rem; /* 11px */
    }

    .mobile-menu {
        display: block;
    }
}

/* LOGO-ONLY mobile brand (user decision 2026-07-19, supersedes the text
   lockup below 640px): the wordmark is hidden VISUALLY - the brand link's
   aria-label still announces "Woodson ISD - The Hub" to assistive tech
   (Layout::renderHeader) - and the logo sizes UP as the sole brand element.
   Desktop >640px keeps the full lockup. */
@media (max-width: 640px) {
    .nav-brand-text {
        display: none;
    }

    .nav-brand img {
        /* Larger than the 50px lockup logo; width capped so the hamburger
           keeps its 44px target + margins (glow blur is capped at 8px on
           mobile and cannot reach it). */
        height: 58px !important;
        max-width: calc(100vw - 120px);
        object-fit: contain;
    }
}

@media (max-width: 480px) {
    .nav-links > a span {
        display: none;
    }
}


/* ==========================================================================
   MOBILE RESPONSIVE
   ========================================================================== */

@media (max-width: 768px) {
    .nav-user-info {
        display: none;
    }
}

/**
 * ============================================================================
 * LOGIN PAGE STYLES
 * ============================================================================
 * All styles specific to the login page (public/login.php)
 * Includes: login container, auth cards, OAuth buttons, animations
 * ============================================================================
 */

/* ==========================================================================
   MODERN LOGIN PAGE - Theme-aware with animations
   ========================================================================== */

.login-page {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--secondary-color, #000000);
    position: relative;
    overflow: hidden;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}

/* Subtle static background pattern */
.login-page::before {
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    background-image:
        radial-gradient(circle at 20% 20%, rgba(201, 151, 0, 0.08) 0%, transparent 30%),
        radial-gradient(circle at 80% 80%, rgba(201, 151, 0, 0.08) 0%, transparent 30%);
    z-index: 0;
}

/* Remove floating animation - too distracting */
.login-page::after {
    display: none;
}

/* Login Container */
.login-container {
    width: 100%;
    max-width: 450px;
    padding: 2rem;
    position: relative;
    z-index: 1;
    animation: slideInUp 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

/* Login Card - Glassmorphism */
.login-card {
    background: rgba(255, 255, 255, 0.98);
    backdrop-filter: blur(20px) saturate(180%);
    border-radius: 24px;
    padding: 3rem;
    box-shadow:
        0 20px 60px rgba(0, 0, 0, 0.4),
        0 0 0 1px rgba(201, 151, 0, 0.2);
    border: 1px solid rgba(255, 255, 255, 0.3);
    position: relative;
    overflow: hidden;
}

/* Remove pulsing animation */
.login-card::before {
    display: none;
}

/* Login Content */
.login-content {
    position: relative;
    z-index: 1;
    animation: fadeIn 1s ease-out 0.6s both;
}

/* Login Note */
.login-note {
    text-align: center;
    margin-top: 1.5rem;
    color: var(--gray-500);
    font-size: 0.95rem;
    animation: fadeIn 1s ease-out 0.8s both;
}

/* Loading State */
.login-card.loading {
    pointer-events: none;
    opacity: 0.7;
}

.login-card.loading::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 50px;
    height: 50px;
    margin: -25px 0 0 -25px;
    border: 4px solid #f3f3f3;
    border-top: 4px solid var(--primary-color);
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

.login-card {
    padding: 2rem 1.5rem;
        border-radius: 20px;
}

.login-note {
    color: #aaa;
}

/* ==========================================================================
   MODERN LOGIN PAGE - Theme-aware with animations
   ========================================================================== */

.login-page {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--secondary-color, #000000);
    position: relative;
    overflow: hidden;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}

/* Subtle static background pattern */
.login-page::before {
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    background-image:
        radial-gradient(circle at 20% 20%, rgba(201, 151, 0, 0.08) 0%, transparent 30%),
        radial-gradient(circle at 80% 80%, rgba(201, 151, 0, 0.08) 0%, transparent 30%);
    z-index: 0;
}

/* Remove floating animation - too distracting */
.login-page::after {
    display: none;
}

/* Login Container */
.login-container {
    width: 100%;
    max-width: 450px;
    padding: 2rem;
    position: relative;
    z-index: 1;
    animation: slideInUp 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

@keyframes slideInUp {
    from {
        opacity: 0;
        transform: translateY(60px) scale(0.9);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

/* Login Card - Glassmorphism */
.login-card {
    background: rgba(255, 255, 255, 0.98);
    backdrop-filter: blur(20px) saturate(180%);
    border-radius: 24px;
    padding: 3rem;
    box-shadow:
        0 20px 60px rgba(0, 0, 0, 0.4),
        0 0 0 1px rgba(201, 151, 0, 0.2);
    border: 1px solid rgba(255, 255, 255, 0.3);
    position: relative;
    overflow: hidden;
}

/* Remove pulsing animation */
.login-card::before {
    display: none;
}

/* Logo Section */
.logo {
    text-align: center;
    margin-bottom: 2.5rem;
    position: relative;
    z-index: 1;
}

.logo img {
    max-width: 200px;
    margin-bottom: 1.5rem;
    filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.2));
    transition: transform 0.3s ease;
}

.logo img:hover {
    transform: scale(1.02);
}

.logo h1 {
    font-size: 2rem;
    font-weight: 800;
    margin-bottom: 0.5rem;
    /* R1b: the login card is WHITE (rgba 255,255,255,.98) - fill gold as a
       headline here was 1.9:1; light-surface gold reads. */
    color: var(--gold-text);
    animation: fadeInDown 0.8s ease-out 0.2s both;
}

.logo h2 {
    font-size: 1.2rem;
    color: var(--gray-500);
    font-weight: 400;
    animation: fadeInDown 0.8s ease-out 0.4s both;
}

@keyframes fadeInDown {
    from {
        opacity: 0;
        transform: translateY(-20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Login Content */
.login-content {
    position: relative;
    z-index: 1;
    animation: fadeIn 1s ease-out 0.6s both;
}

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* Google Login Button - Modern Design */
.google-login-btn,
.microsoft-login-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    padding: 1rem 2rem;
    background: white;
    color: #444;
    border: 2px solid #dadce0;
    border-radius: 50px;
    font-size: 1rem;
    font-weight: 600;
    text-decoration: none;
    transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
    cursor: pointer;
    position: relative;
    overflow: hidden;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}

.google-login-btn::before,
.microsoft-login-btn::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0;
    height: 0;
    border-radius: 50%;
    background: rgba(201, 151, 0, 0.1);
    transform: translate(-50%, -50%);
    transition: width 0.5s ease, height 0.5s ease;
}

.google-login-btn:hover::before,
.microsoft-login-btn:hover::before {
    width: 400px;
    height: 400px;
}

.google-login-btn:hover,
.microsoft-login-btn:hover {
    transform: translateY(-3px) scale(1.02);
    border-color: var(--primary-color);
    box-shadow:
        0 15px 35px rgba(201, 151, 0, 0.3),
        0 5px 15px rgba(0, 0, 0, 0.1);
}

.google-login-btn:active,
.microsoft-login-btn:active {
    transform: translateY(-1px) scale(0.98);
}

.google-icon,
.microsoft-icon {
    display: flex;
    align-items: center;
    margin-right: 12px;
    position: relative;
    z-index: 1;
    transition: transform 0.3s ease;
}

.google-login-btn:hover .google-icon,
.microsoft-login-btn:hover .microsoft-icon {
    transform: rotate(360deg) scale(1.1);
}

/* Microsoft button specific styling */
.microsoft-login-btn {
    margin-top: 1rem;
    background: #2f2f2f;
    color: white;
    border-color: #2f2f2f;
}

.microsoft-login-btn:hover {
    background: #000;
    border-color: var(--primary-color);
}

/* Login Note */
.login-note {
    text-align: center;
    margin-top: 1.5rem;
    color: var(--gray-500);
    font-size: 0.95rem;
    animation: fadeIn 1s ease-out 0.8s both;
}

/* Error Message */
.error-message {
    background: var(--error);
    color: white;
    padding: 1rem;
    border-radius: 12px;
    text-align: center;
    margin-bottom: 1rem;
    box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
    animation: shake 0.5s ease-out;
}

@keyframes shake {
    0%, 100% { transform: translateX(0); }
    10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
    20%, 40%, 60%, 80% { transform: translateX(5px); }
}

/* Success Message */
.success-message {
    background: var(--success);
    color: white;
    padding: 1rem;
    border-radius: 12px;
    text-align: center;
    margin-bottom: 1rem;
    box-shadow: 0 4px 15px rgba(64, 192, 87, 0.3);
    animation: slideInDown 0.5s ease-out;
}

@keyframes slideInDown {
    from {
        opacity: 0;
        transform: translateY(-20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Loading State */
.login-card.loading {
    pointer-events: none;
    opacity: 0.7;
}

.login-card.loading::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 50px;
    height: 50px;
    margin: -25px 0 0 -25px;
    border: 4px solid #f3f3f3;
    border-top: 4px solid var(--primary-color);
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

/* Responsive Design */
@media (max-width: 768px) {
    .login-container {
        max-width: 90%;
        padding: 1rem;
    }

    .login-card {
        padding: 2rem 1.5rem;
        border-radius: 20px;
    }

    .logo h1 {
        font-size: 1.5rem;
    }

    .logo h2 {
        font-size: 1rem;
    }

    .logo img {
        max-width: 150px;
    }

    .google-login-btn,
    .microsoft-login-btn {
        padding: 0.875rem 1.5rem;
        font-size: 0.95rem;
    }
}

/* Accessibility: Reduce motion */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* Dark mode support */
@media (prefers-color-scheme: dark) {
    .login-card {
        background: rgba(30, 30, 30, 0.95);
        color: white;
    }

    .logo h2 {
        color: #aaa;
    }

    .login-note {
        color: #aaa;
    }
}

/* ========== shared/modals.css ========== */

/**
 * ============================================================================
 * MODAL STYLES - MICROMODAL EDITION
 * ============================================================================
 * Migrated from Bootstrap 5 to MicroModal.js (1.9KB gzipped)
 * - No CSS conflicts with framework defaults
 * - Full control over modal behavior and styling
 * - Accessibility-first (WAI-ARIA compliant)
 *
 * Modals defined in: /public/admin/partials/modals.php
 * MicroModal docs: https://micromodal.vercel.app/
 * ============================================================================
 */

/* ==========================================================================
   MICROMODAL BASE STRUCTURE
   ========================================================================== */

/**
 * Modal container - Hidden by default
 */
.modal {
    display: none;
}

.modal.is-open {
    display: block;
    z-index: 10050 !important; /* Above header (10002) but below SweetAlert/admin overlays (100000+) */
}

/**
 * Modal overlay - Semi-transparent backdrop
 */
.modal__overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.6);
    display: flex;
    justify-content: center;
    align-items: center;
}

/**
 * Modal container - The actual modal box
 */
.modal__container {
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
    overflow-y: auto;
    max-height: 95vh;
    min-height: 60vh;
    max-width: 600px;
    width: 90%;
}

/**
 * Wide modals - 90vw width + taller (our requirement!)
 */
.modal__container.modal-wide {
    max-width: 90vw !important;
    width: 90vw !important;
    max-height: 98vh !important;
    min-height: 90vh !important;
}

/**
 * Modal header
 */
.modal__header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1.5rem;
    border-bottom: 1px solid var(--gray-100);
    background: var(--gray-50);
}

.modal-title {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 600;
    color: #212529;
}

/**
 * Modal close button (X button) - Modern SVG icon with smooth animation
 */
.modal__close {
    background: transparent !important;
    border: none !important;
    padding: 8px !important;
    border-radius: 6px !important;
    transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    position: relative !important;
    cursor: pointer !important;
    color: #6c757d !important;
    transform: rotate(0deg) scale(1) !important;
    margin-left: auto !important;
    width: 40px !important;
    height: 40px !important;
}

.modal__close:hover,
.modal__close:focus {
    transform: rotate(180deg) scale(1.15) !important;
    background: rgba(220, 53, 69, 0.1) !important;
    color: var(--error-dark) !important;
}

/* Keyboard focus must stay visible (WCAG 2.4.7) - the old outline:none
   !important here defeated the global :focus-visible baseline. */
.modal__close:focus-visible {
    outline: 2px solid var(--border-focus, #C4B454) !important;
    outline-offset: 2px !important;
}

.modal__close svg {
    width: 24px !important;
    height: 24px !important;
    stroke: currentColor !important;
    stroke-width: 2 !important;
    pointer-events: none !important;
}

/**
 * Modal content area
 */
.modal__content {
    padding: 1.5rem;
    overflow-y: auto;
    max-height: calc(95vh - 140px);
    min-height: calc(60vh - 140px);
}

/**
 * Taller content for wide modals (e.g., package discovery)
 */
.modal-wide .modal__content {
    max-height: calc(98vh - 140px);
    min-height: calc(90vh - 140px);
}

/**
 * Modal footer
 */
.modal__footer {
    padding: 1rem 1.5rem;
    border-top: 1px solid var(--gray-100);
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
}

/* ==========================================================================
   MICROMODAL ANIMATIONS
   ========================================================================== */

/**
 * Slide animation for modal appearance
 */
@keyframes mmfadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes mmslideIn {
    from {
        transform: translateY(15%);
        opacity: 0;
    }
    to {
        transform: translateY(0);
        opacity: 1;
    }
}

.micromodal-slide {
    display: none;
}

.micromodal-slide.is-open {
    display: block;
}

.micromodal-slide[aria-hidden="false"] .modal__overlay {
    animation: mmfadeIn 0.3s cubic-bezier(0.0, 0.0, 0.2, 1);
}

.micromodal-slide[aria-hidden="false"] .modal__container {
    animation: mmslideIn 0.3s cubic-bezier(0, 0, .2, 1);
}

.micromodal-slide[aria-hidden="true"] .modal__overlay {
    animation: mmfadeOut 0.3s cubic-bezier(0.0, 0.0, 0.2, 1);
}

.micromodal-slide[aria-hidden="true"] .modal__container {
    animation: mmslideOut 0.3s cubic-bezier(0, 0, .2, 1);
}

@keyframes mmfadeOut {
    from { opacity: 1; }
    to { opacity: 0; }
}

@keyframes mmslideOut {
    from {
        transform: translateY(0);
        opacity: 1;
    }
    to {
        transform: translateY(-10%);
        opacity: 0;
    }
}

/* ==========================================================================
   RESPONSIVE DESIGN
   ========================================================================== */

@media (max-width: 768px) {
    .modal__container.modal-wide {
        max-width: 95vw !important;
        width: 95vw !important;
    }

    .modal__header,
    .modal__content,
    .modal__footer {
        padding: 1rem;
    }
}

/* ==========================================================================
   LEGACY BOOTSTRAP CLASSES (For Backwards Compatibility)
   ========================================================================== */

/* Keep Bootstrap utility classes working inside modals */.modal-content {
    background: white;
    margin: 0;
    padding: 2rem;
    border-radius: 8px;
    width: 90%;
    max-width: 600px;
    max-height: 90vh;
    overflow-y: auto;
    box-shadow: 0 4px 20px rgba(0, 0, 0, var(--shadow-heavy));
    position: relative;
}

/* ==========================================================================
   MODAL HEADER
   ========================================================================== */

.modal-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 0.5rem;
    padding-right: 2.5rem;
}

.modal-header h2,
.modal-header h3 {
    margin: 0;
    flex: 1;
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--text-color);
}

/* ==========================================================================
   MODAL CLOSE BUTTON
   ========================================================================== */

.modal-close {
    position: absolute;
    top: 1rem;
    right: 1rem;
    width: 40px;
    height: 40px;
    background: transparent;
    border: 0;
    padding: 0;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 4px;
    font-size: 20px;
    color: var(--gray-500);
    transition: transform 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55), color 0.3s ease;
}

.modal-close::before {
    content: '';
    position: absolute;
    width: 40px;
    height: 40px;
    background: rgba(211, 47, 47, 0.2);
    border-radius: 4px;
    transform: scale(0);
    transition: transform 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
    z-index: 0;
}

.modal-close i {
    position: relative;
    z-index: 1;
}

.modal-close:hover {
    transform: rotate(180deg) scale(1.2);
    color: var(--error-dark);
}

.modal-close:hover::before {
    transform: scale(1);
}

/* Alternative close button style */
.modal-close-btn {
    background: rgba(255, 255, 255, 0.02);
    border: 1px solid rgba(0, 0, 0, 0.06);
    color: var(--gray-700);
    width: 36px;
    height: 36px;
    border-radius: 8px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform 0.2s ease;
}

.modal-close-btn:hover {
    background: rgba(220, 53, 69, 0.1);
    color: var(--error-dark);
    transform: translateY(-2px) scale(1.02);
}

/* When used in modal header (not absolute positioned) */
.modal-header .modal-close-btn {
    position: relative;
    top: auto;
    right: auto;
}

/* Bootstrap close button - fix offset from .btn i:last-child */
.btn-close,
button.btn-close {
    padding: calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);
    margin: auto;
    width: 36px;
    height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    opacity: 0.5;
    transition: all 0.3s ease;
    line-height: 1;
    font-size: 1.5rem;
    color: var(--gray-500);
    border-radius: 4px;
}

.btn-close:hover,
button.btn-close:hover {
    opacity: 1;
    background: rgba(220, 53, 69, 0.1);
    transform: rotate(180deg);
    color: var(--error-dark);
}

/* Fix icon offset in close buttons - override .btn i:last-child */
.btn-close i,
.modal-close i,
.modal-close-btn i,
button[data-bs-dismiss="modal"] i {
    margin: 0 !important;
    line-height: 1;
    display: block;
}

/* ==========================================================================
   MODAL BODY & CONTENT
   ========================================================================== */

.modal-body {
    padding: 1rem 0;
    color: var(--text-color);
}

.modal h2 {
    margin-top: 0;
    margin-bottom: 1.5rem;
}

/* ==========================================================================
   MODAL FOOTER & ACTIONS
   ========================================================================== */

.modal-footer {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 0.75rem;
    padding-top: 1.5rem;
    margin-top: 1.5rem;
    border-top: 1px solid var(--gray-100);
}

.modal-actions {
    display: flex;
    gap: 1rem;
    justify-content: flex-end;
    margin-top: 2rem;
    padding-top: 1rem;
    border-top: 2px solid var(--gray-200);
}

.modal-actions button {
    min-width: 100px;
}

.modal-actions button:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

/* ==========================================================================
   MODAL OVERLAY
   ========================================================================== */

.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: var(--bg-modal-overlay);
    z-index: 10000;
    display: none;
}

.modal-overlay.show {
    display: block;
}

.modal-overlay.hiding {
    animation: fadeOut 0.3s ease-out forwards;
}

/* ==========================================================================
   SPECIFIC MODAL TYPES
   ========================================================================== */

/* Package Discovery Modal - Large, Centered, Full-Screen Experience */
.package-discovery-modal {
    display: flex !important;
    align-items: center;
    justify-content: center;
}

.package-discovery-modal .modal-dialog {
    max-width: 90vw;
    width: 90vw;
    margin: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

.package-discovery-modal .modal-content {
    max-width: 90vw;
    width: 90vw;
    height: 90vh;
    max-height: 90vh;
    margin: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.package-discovery-modal .modal-header {
    flex-shrink: 0;
    padding: 1.5rem 2rem 1rem 2rem;
    border-bottom: 2px solid var(--gray-100);
}

.package-discovery-modal .modal-body {
    flex: 1;
    padding: 1rem 2rem 2rem 2rem;
    overflow-y: auto;
    overflow-x: hidden;
    min-height: 0;
}

.package-discovery-modal .modal-footer {
    flex-shrink: 0;
    padding: 1rem 2rem;
    border-top: 2px solid var(--gray-100);
    background: var(--gray-50);
}

/* Validation Report Modal */
.validation-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1.25rem 1.5rem;
    background: #ffffff;
    color: var(--gray-800);
    border-radius: 16px 16px 0 0;
    border-bottom: 1px solid var(--gray-200);
}

.validation-modal-header .validation-report-title {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.validation-modal-header .btn-close {
    margin: 0;
    padding: calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);
}

/* ==========================================================================
   MODAL ANIMATIONS
   ========================================================================== */

@keyframes modalSlideIn {
    from {
        opacity: 0;
        transform: translateY(-50px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes fadeOut {
    from {
        opacity: 1;
    }

    to {
        opacity: 0;
    }
}

/* ==========================================================================
   RESPONSIVE - MOBILE & TABLET
   ========================================================================== */

/* Tablets (Portrait) - 768px and below */
@media (max-width: 768px) {
    .modal-content {
        width: 95%;
        max-width: none;
        padding: 1.5rem;
        max-height: 95vh;
    }

    .modal-header {
        padding-right: 2rem;
    }

    .modal-close {
        top: 0.75rem;
        right: 0.75rem;
        width: 36px;
        height: 36px;
    }

    .modal-actions {
        flex-direction: column;
        gap: 0.75rem;
    }

    .modal-actions button {
        width: 100%;
    }
}

/* Mobile Phones - 480px and below */
@media (max-width: 480px) {
    .modal-content {
        width: 100%;
        height: 100%;
        max-height: 100vh;
        border-radius: 0;
        padding: 1rem;
    }

    .modal-header h2,
    .modal-header h3 {
        font-size: 1.25rem;
    }

    .package-discovery-modal .modal-content {
        max-width: none;
        width: 100%;
    }
}

/* Small Mobile - 375px and below */
@media (max-width: 375px) {
    .modal-content {
        padding: 0.75rem;
    }

    .modal-header {
        margin-bottom: 1rem;
        padding-right: 1.5rem;
    }

    .modal-close {
        width: 32px;
        height: 32px;
        font-size: 18px;
    }

    .modal-actions {
        margin-top: 1rem;
        padding-top: 0.75rem;
    }
}

/* ========== hub/hub-pages.css ========== */

/* ==========================================================================
   HUB PAGES - Supplemental styles
   ==========================================================================
   hub-animations.css is the primary source for section card styles.
   This file adds: tooltip, module page, and reduced-motion support.
   ========================================================================== */

/* ---------- Section tooltip (info icon on truncated descriptions) ---------- */
.section-title-tooltip {
    font-size: 0.9rem;
    color: var(--gold-text); /* R1b: glyph on the white section card = light-surface gold */
    cursor: help;
    margin-left: 0.35rem;
    position: relative;
    display: inline-block;
    z-index: 10;
}

.section-title-tooltip:hover::after {
    content: attr(data-full-text);
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    background: rgba(44, 44, 44, 0.98);
    color: white;
    padding: 1rem;
    border-radius: 8px;
    font-size: 0.85rem;
    font-weight: normal;
    white-space: normal;
    min-width: 250px;
    max-width: 400px;
    z-index: 10002;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
    line-height: 1.6;
    border: 1px solid rgba(255, 255, 255, 0.15);
    pointer-events: none;
    backdrop-filter: blur(10px);
}

/* ==========================================================================
   MODULES PAGE
   ========================================================================== */
.modules-page {
    min-height: 100vh;
    background: var(--secondary-color, #000000);
    position: relative;
    overflow: hidden;
}

.modules-page::before {
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    background-image:
        radial-gradient(circle at 10% 20%, rgba(201, 151, 0, 0.06) 0%, transparent 20%),
        radial-gradient(circle at 90% 80%, rgba(201, 151, 0, 0.06) 0%, transparent 20%),
        radial-gradient(circle at 50% 50%, rgba(201, 151, 0, 0.03) 0%, transparent 30%);
    animation: particleFloat 20s ease-in-out infinite;
}

@keyframes particleFloat {
    0%, 100% { transform: translateY(0) scale(1); }
    50% { transform: translateY(-20px) scale(1.05); }
}

.modules-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 3rem 1rem;
    position: relative;
    z-index: 1;
}

/* Modules Header */
.modules-header {
    text-align: center;
    margin-bottom: 4rem;
    opacity: 0;
    animation: fadeInDown 0.8s ease-out forwards;
}

.modules-header h1 {
    font-size: 3rem;
    color: white;
    margin-bottom: 1rem;
    font-weight: 800;
    text-shadow: 0 4px 20px rgba(0,0,0,0.3);
}

.modules-header p {
    font-size: 1.3rem;
    color: rgba(255, 255, 255, 0.9);
    text-shadow: 0 2px 10px rgba(0,0,0,0.2);
}

/* Modules Grid */
.modules-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: 2.5rem;
    margin-bottom: 3rem;
}

/* Module Cards */
.module-card {
    background: rgba(255, 255, 255, 0.95);
    backdrop-filter: blur(20px);
    border-radius: 24px;
    padding: 2.5rem;
    box-shadow:
        0 8px 32px rgba(0,0,0,0.2),
        0 0 0 1px rgba(255,255,255,0.3);
    text-decoration: none;
    transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
    border: 2px solid transparent;
    display: flex;
    flex-direction: column;
    position: relative;
    overflow: hidden;
    opacity: 0;
    transform: translateY(20px);
    animation: cardEntrance 0.6s ease-out forwards;
}

.module-card:nth-child(1) { animation-delay: 0.1s; }
.module-card:nth-child(2) { animation-delay: 0.2s; }
.module-card:nth-child(3) { animation-delay: 0.3s; }
.module-card:nth-child(4) { animation-delay: 0.4s; }
.module-card:nth-child(5) { animation-delay: 0.5s; }
.module-card:nth-child(6) { animation-delay: 0.6s; }

@keyframes cardEntrance {
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Module card hover */
.module-card:hover {
    transform: translateY(-6px);
    box-shadow:
        0 12px 32px rgba(0,0,0,0.15),
        0 0 0 1px rgba(102, 126, 234, 0.3);
}

/* Module Icon */
.module-icon {
    font-size: 4rem;
    margin-bottom: 1.5rem;
    transition: all 0.4s ease;
    display: inline-block;
    filter: drop-shadow(0 4px 12px rgba(0,0,0,0.15));
}

.module-card:hover .module-icon {
    transform: scale(1.1);
    filter: drop-shadow(0 6px 16px rgba(201, 151, 0, 0.3));
}

/* Module Title */
.module-title {
    font-size: 1.75rem;
    font-weight: 700;
    color: var(--secondary-color, #2c3e50);
    margin-bottom: 1rem;
    transition: color 0.3s ease;
}

.module-card:hover .module-title {
    color: #222;
}

/* Module Description */
.module-description {
    color: var(--gray-500);
    line-height: 1.8;
    flex: 1;
    margin-bottom: 1.5rem;
    transition: color 0.3s ease;
}

.module-card:hover .module-description {
    color: #444;
}

/* Module Role Badge */
.module-role {
    display: inline-block;
    padding: 0.5rem 1.25rem;
    background: var(--gold-gradient, linear-gradient(135deg, var(--gold, #C4B454) 0%, var(--gold-press, #A89940) 100%)); /* --nd-gold-light endpoint retired */
    color: var(--secondary-color, #000);
    border-radius: 25px;
    font-size: 0.9rem;
    font-weight: 600;
    text-transform: capitalize;
    box-shadow: 0 4px 12px rgba(201, 151, 0, 0.3);
    transition: all 0.3s ease;
}

.module-card:hover .module-role {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(201, 151, 0, 0.4);
}

/* Empty state */
.modules-empty-state {
    text-align: center;
    padding: 4rem 2rem;
    background: rgba(255, 255, 255, 0.95);
    backdrop-filter: blur(20px);
    border-radius: 24px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}

.modules-empty-state p:first-of-type {
    font-size: 1.3rem;
    color: var(--gold-text); /* R1b: headline on the white empty-state card */
    font-weight: 600;
    margin-bottom: 1rem;
}

.modules-empty-state p:last-of-type {
    color: var(--gray-500);
    font-size: 1.1rem;
}

/* Module responsive */
@media (max-width: 768px) {
    .modules-header h1 { font-size: 2rem; }
    .modules-header p { font-size: 1.1rem; }
    .modules-grid { grid-template-columns: 1fr; gap: 1.5rem; }
    .module-card { padding: 2rem; }
    .module-icon { font-size: 3rem; }
    .module-title { font-size: 1.5rem; }
}

/* Loading state */
.modules-loading {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 400px;
}

.modules-loading::after {
    content: '';
    width: 60px;
    height: 60px;
    border: 4px solid rgba(255, 255, 255, 0.3);
    border-top-color: white;
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* ==========================================================================
   ACCESSIBILITY
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* ==========================================================================
   INSTALL BAR ("Add The Hub to your phone", PWA). COMPACT one-liner (the
   always-open card read as "massive and overwhelming" - owner, 2026-08-30):
   [app icon] title ......... [Install App | Show Me How] [x]
   The iOS Share -> Add to Home Screen steps live COLLAPSED under the bar,
   expanded by [Show Me How] (hub-install.js). A PHONE affordance -
   suppressed on fine-pointer desktop even if the browser is installable.
   Tokens only; no gold outside var() fallbacks.
   ========================================================================== */
.hub-install-card {
    max-width: 480px;
    margin: 0 auto 16px;
    padding: 8px 10px;
    background: var(--surface, #FFFFFF);
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
    box-shadow: 0 1px 2px rgba(23, 21, 15, 0.06);
    text-align: left;
}
.hub-install-card[hidden] { display: none; }
.hub-install-row {
    display: flex;
    align-items: center;
    gap: 10px;
}
.hub-install-icon {
    flex: none;
    width: 28px;
    height: 28px;
    border-radius: 7px;
    overflow: hidden;
    border: 1px solid var(--gold-border, #D9CD8A);
}
.hub-install-icon img { width: 100%; height: 100%; display: block; }
.hub-install-title {
    flex: 1;
    min-width: 0;
    margin: 0;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.25;
    color: var(--ink, #191712);
}
.hub-install-actions {
    flex: none;
    display: flex;
    align-items: center;
    gap: 6px;
}
/* The variant wrappers are layout-neutral (JS toggles their hidden attr). */
.hub-install-actions span[data-install] { display: contents; }
.hub-install-actions span[data-install][hidden] { display: none; }
.hub-install-steps {
    margin: 8px 0 2px;
    padding: 8px 0 0 20px;
    border-top: 1px solid var(--line, #E6E2D2);
    font-size: 13px;
    color: var(--ink-soft, #4A4638);
    line-height: 1.55;
}
.hub-install-steps[hidden] { display: none; }
.hub-install-steps li { margin-bottom: 4px; }
.hub-install-share {
    vertical-align: -3px;
    margin: 0 2px;
    color: var(--ink, #191712);
}
/* Phone affordance only: desktop fine-pointer never sees the bar. */
@media (min-width: 1025px) and (pointer: fine) {
    .hub-install-card { display: none; }
}

/* ========== hub/hub-animations.css ========== */

/* ==========================================================================
   HUB STYLES — Quick-Action Launcher (Mobile-First)
   Architecture: docs/HUB_ARCHITECTURE.md
   ========================================================================== */

/* Sticky footer: page-wrapper fills viewport */
.page-wrapper {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

/* Main content grows to fill space, pushing footer down */
.main-content {
    flex: 1;
}

.main-content.hub-page {
    background: var(--hub-page-bg, #f8f9fa);
    position: relative;
}

/* Hub Container */
.hub-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 24px 16px 40px;
    position: relative;
    z-index: 1;
}

/* Hub Header — compact, personal */
.hub-header {
    text-align: center;
    margin-bottom: 32px;
}

.hub-header-icon {
    margin-bottom: 12px;
    line-height: 1;
}

.hub-greeting {
    font-size: 0.95rem;
    color: var(--hub-subtitle-color, #888);
    margin-bottom: 4px;
    font-weight: 400;
}

.hub-header h1 {
    font-size: 2rem;
    margin-bottom: 6px;
    color: var(--hub-title-color, #111);
    font-weight: 700;
    letter-spacing: -0.025em;
}

.hub-subtitle {
    font-size: 1rem;
    color: var(--hub-subtitle-color, #666);
    margin: 0;
}

/* ==========================================================================
   ACTION CARDS — Tappable, mobile-first quick-action buttons
   ========================================================================== */

.sections-grid {
    /* (display is declared below, with the reasoning - this rule used to say
       `display: grid` here and the flex that supersedes it further down.) */
    /* auto-fit + a CAPPED max, and the pair matters.
       Richard, 2026-08-24: "can these auto center?" - three cards sat hard left
       with two empty columns holding the right side open (measured: five
       236.8px tracks, three of them filled).
       auto-fill keeps empty tracks alive; auto-fit collapses them. The comment
       further down this file used to argue for auto-fill because auto-fit
       "would stretch five cards across the full width into letterboxes" - which
       is true of auto-fit with `1fr` as the track MAX, and the `1fr` is the part
       that stretches. Cap the max at a card-sized width and auto-fit collapses
       the empties WITHOUT stretching, then justify-content centres what is left.
       Declared here once: the breakpoints below override only the track sizing,
       so the centring holds at every width. */
    /* FLEX, declared ONCE here, for the reason the paragraph above gives:
       the breakpoints below override only sizing, so whatever layout model
       this rule picks is the one that holds at every width.

       It is flex rather than grid because of the LAST ROW. justify-content on
       a grid centres the track group, but items in a short final row still
       begin at track one - eleven cards left Vehicle Maintenance hard left
       under four empty columns. Flex wrap centres it.

       This was briefly declared in the 1200px block alone, which centred the
       desktop and left 390/768/1024 looking untouched. */
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 16px;
}

/* Card measures, per breakpoint below. flex-basis reproduces the max of the
   minmax() each width used to carry; min-width its floor. */
.sections-grid > * {
    flex: 0 1 200px;
    min-width: 160px;
}

/* ARRANGE YOUR OWN HUB ------------------------------------------------
   The tile wraps the card so the customize controls can be its siblings
   (a button inside an anchor is invalid, and would navigate). The tile
   inherits the flex measures above; the card fills it. */
.hub-tile { display: flex; flex-direction: column; min-width: 0; }
.hub-tile > .action-card { flex: 1 1 auto; width: 100%; }

/* PHONE FIRST: the control lives in the header's top-right corner and adds
   no row. The header is already greeting + title + subtitle before a phone
   shows a card; a fourth block was the complaint. Icon only here - the
   label is clipped off-screen, not display:none, so it stays the button's
   accessible name. */
.hub-header { position: relative; }
.hub-customize {
    position: absolute; top: 0; right: 0;
    display: flex; align-items: center; gap: 8px;
}
.hub-customize__btn { display: inline-flex; align-items: center; gap: 6px; }
/* Icon only at EVERY width - Richard liked the corner on desktop too, and a
   fixed-size button is one that words can never grow sideways. The label
   is clipped, not removed: still the accessible name. */
.hub-customize__label {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap;
}

/* THE TOAST. Fixed to the bottom of the viewport, outside the layout, so a
   message can never move the button or run across the greeting - which is
   what the inline status line did on a phone. Inverted colours so it reads
   over cards; safe-area inset for phones with a home bar. */
.hub-toast {
    position: fixed; left: 50%; bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    transform: translateX(-50%);
    max-width: calc(100% - 32px);
    padding: 10px 14px;
    border-radius: var(--radius-control);
    background: var(--hub-tile-text, #333); color: var(--hub-tile-bg, #fff);
    font-size: 14px; line-height: 1.35; text-align: center;
    box-shadow: 0 6px 20px rgba(0,0,0,0.22);
    z-index: 1000;
}
.hub-toast--error { background: var(--error); }

/* Hidden by default; the script shows them in customize mode. Present in
   the markup so the tray is one <div hidden> away, not a second request. */
.hub-tile__tools { display: none; }
.is-customizing .hub-tile__tools {
    display: flex; gap: 6px; justify-content: center; margin-top: 6px;
}
/* The buttons are ordinary .hub-btn.hub-btn--ghost.hub-btn--sm (the same
   classes the install card on this page uses), so colour, border, radius
   and focus ring all come from the button system and nothing is restated
   here. This rule only sizes them: 44px is the touch floor, and a ghost
   --sm button is shorter than that on its own. */
.hub-tile__btn { min-width: 44px; min-height: 44px; }
/* Hiding a card focuses its Show button in the drawer and the browser scrolls
   that into view itself. The page's bottom clearance normally keeps it above
   the fixed footer (measured: 66px clear); this states it outright, so the
   button stays clear if that padding changes or anything is ever rendered
   after the drawer. */
.hub-tile__btn { scroll-margin: 16px 0 64px; }
.hub-tile__btn--toggle { flex: 1 1 auto; }

/* In customize mode the card is a thing you move, not a thing you open. */
.is-customizing .hub-tile > .action-card { cursor: grab; }
.is-customizing .hub-tile > .action-card:hover { transform: none; }
.is-customizing .hub-tile.is-dragging { opacity: 0.5; }

/* The tray of hidden cards: same tiles, quieter. */
/* THE HIDDEN-CARDS DRAWER. A <details> at the foot of the Hub: collapsed by
   default, open on a tap, cards at FULL opacity inside - hiding a card is
   about getting it out of the main grid, not greying it out. */
.hub-cards-drawer {
    margin-top: 28px; /* fills .hub-container, exactly as the grid above it does - the container carries the desktop widths (1120 / 1320) */
    border-top: 1px solid var(--gray-300, #edebe9);
}
.hub-cards-drawer__summary {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    padding: 14px; cursor: pointer; list-style: none;
    font-size: 13px; letter-spacing: .06em; text-transform: uppercase;
    color: var(--ink-mute, #6F6A5C); user-select: none;
}
/* the default disclosure triangle, gone on both engines */
.hub-cards-drawer__summary::-webkit-details-marker { display: none; }
.hub-cards-drawer__summary::marker { content: ''; }
/* our own caret, which turns when the drawer opens */
.hub-cards-drawer__summary::after {
    content: ''; width: 8px; height: 8px; margin-left: 2px;
    border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
    transform: rotate(45deg); transition: transform 0.15s ease;
}
.hub-cards-drawer[open] .hub-cards-drawer__summary::after { transform: rotate(-135deg); }
.hub-cards-drawer__summary:hover { color: var(--hub-tile-text, #333); }
.hub-cards-drawer__summary:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.hub-cards-drawer__count {
    min-width: 20px; height: 20px; padding: 0 6px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 10px; font-size: 12px; letter-spacing: 0;
    background: var(--gray-200, #f3f2f1); color: var(--hub-tile-text, #333);
}
.sections-grid--hidden { padding-bottom: 8px; }

.action-card {
    background: var(--hub-tile-bg, #fff);
    border-radius: 16px;
    padding: 24px 16px 20px;
    text-align: center;
    text-decoration: none;
    color: var(--hub-tile-text, #333);
    display: flex;
    flex-direction: column;
    align-items: center;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 0 0 1px rgba(0,0,0,0.04);
    cursor: pointer;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
    position: relative;
}

.action-card:hover {
    transform: translateY(-2px);
    box-shadow:
        0 4px 12px rgba(0,0,0,0.1),
        0 0 0 1px var(--hub-card-hover-border, rgba(201, 151, 0, 0.25));
}

.action-card:active,
.action-card.tapped {
    transform: scale(0.97);
    box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}

/* Action Icon — prominent, colored */
.action-icon {
    font-size: 2.5rem;
    margin-bottom: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    color: var(--hub-icon-color, #C4B454);
}

.action-icon img {
    width: 48px;
    height: 48px;
    object-fit: contain;
}

.action-icon .svg-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
}

.action-icon .svg-icon svg {
    width: 40px;
    height: 40px;
}

/* Action Label — bold verb */
.action-label {
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--hub-tile-text, #222);
    line-height: 1.3;
    margin-bottom: 4px;
}

/* Action Hint — short description */
.action-hint {
    font-size: 0.78rem;
    color: var(--hub-card-description, #999);
    line-height: 1.3;
}

/* Empty state */
.no-sections {
    text-align: center;
    padding: 48px 20px;
    background: var(--hub-no-sections-bg, white);
    border-radius: 16px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}

.no-sections h3 {
    color: var(--primary-color, #C4B454);
    font-size: 1.3rem;
    margin-bottom: 8px;
}

.no-sections p {
    color: var(--text-muted, #666);
    font-size: 0.95rem;
}

/* ==========================================================================
   RESPONSIVE — Mobile-first breakpoints
   ========================================================================== */

/* Small phone: 2 columns still */
@media (max-width: 480px) {
    .hub-container {
        /* Density (bottom chrome round): first content high on phones */
        padding: 8px 12px 32px;
    }

    .hub-header {
        margin-bottom: 24px;
    }

    .hub-header h1 {
        font-size: 1.6rem;
    }

    .sections-grid {
        gap: 12px;
    }

    /* Two up, by basis rather than by track count - so the odd last card
       centres instead of sitting in the left column. calc subtracts half the
       gap, which is what repeat(2, 1fr) did implicitly. */
    .sections-grid > * {
        flex: 0 1 calc(50% - 6px);
        min-width: 0;
    }

    .action-card {
        padding: 20px 12px 16px;
        border-radius: 14px;
    }

    .action-icon {
        font-size: 2rem;
        width: 44px;
        height: 44px;
        margin-bottom: 10px;
    }

    .action-label {
        font-size: 0.85rem;
    }

    .action-hint {
        font-size: 0.72rem;
    }
}

/* Tablet+: wider cards */
@media (min-width: 600px) {
    .sections-grid {
        gap: 20px;
    }

    .sections-grid > * {
        flex: 0 1 220px;
        min-width: 180px;
    }

    .action-card {
        padding: 28px 20px 24px;
    }

    .action-icon {
        font-size: 3rem;
        width: 64px;
        height: 64px;
    }

    .action-label {
        font-size: 1.05rem;
    }
}

/* Desktop: 4 col max, more breathing room */
@media (min-width: 900px) {
    .hub-container {
        padding: 40px 24px 60px;
    }

    .hub-header {
        margin-bottom: 40px;
    }

    .hub-header h1 {
        font-size: 2.5rem;
    }

    .sections-grid {
        gap: 24px;
    }

    .sections-grid > * {
        flex: 0 1 220px;
        min-width: 200px;
    }

    /* Subtle hover effect only on desktop */
    .action-card:hover .action-icon {
        transform: scale(1.05);
    }

    .action-icon {
        transition: transform 0.2s ease;
    }
}

/* ---------------------------------------------------------------------------
   LARGE SCREENS (Richard, 2026-08-17): "good for small screens, but wastes
   valuable real estate for large screens... more columns and smaller icons
   could easily assist people's views."

   Measured at 1920x1080 before this block: .hub-container was a FLAT 800px with
   no media query at all, so the grid sat at 752px and rendered 3 columns of
   235px - 58% of the screen was empty margin. The responsive ladder stopped at
   900px, so every screen from a laptop to a 4K monitor got the same layout.

   The container stays 800px below 1200px on purpose: that is a comfortable
   reading measure for the narrow content on section pages, and only the tile
   grid benefits from going wider.
   --------------------------------------------------------------------------- */
@media (min-width: 1200px) {
    /* SCOPED TO THE HUB LANDING ONLY.
       .hub-container is shared: the landing page has no data-section, every
       section page carries data-section="<key>". Widening it unscoped stretched
       the section pages too - measured, an 89px-tall .section-panel on
       /sections/work_orders/ became 1272px wide, a thin strip of nothing.
       Only the tile grid benefits from the extra width. */
    .hub-container:not([data-section]) {
        max-width: 1120px;
    }

    .sections-grid {
        gap: 20px;
    }

    /* Sizing only. display/flex-wrap/justify-content are declared once on the
       base rule now - they were briefly here alone, which centred the desktop
       and left every narrower width still using grid. */
    .sections-grid > * {
        flex: 0 1 220px;
        min-width: 200px;
    }

    /* Smaller icon, so a denser grid does not read as a wall of glyphs. The
       tile keeps its tap target; only the decoration shrinks. */
    .hub-container:not([data-section]) .action-icon {
        font-size: 2.25rem;
        width: 52px;
        height: 52px;
    }
}

@media (min-width: 1600px) {
    .hub-container:not([data-section]) {
        max-width: 1320px;
    }

    .sections-grid {
        gap: 22px;
    }
}

/* ---------------------------------------------------------------------------
   WIDE SECTION PAGES - OPT-IN (Richard, 2026-08-25): "the work ordre page is
   too narrow on a large desktop." Measured live before this block: the content
   column on /sections/work_orders/ was ~610px inside a 1568px window, so about
   60% of the screen was margin on a page that is about to become a register
   table with a status column and a row-actions kebab.

   Why the ladder above is written `.hub-container:not([data-section])`: when
   the landing grid was first widened (2026-08-17), widening .hub-container
   unscoped stretched the SECTION pages with it - an 89px-tall .section-panel
   on /sections/work_orders/ became a 1272px strip of nothing. Excluding every
   data-section page was the cheap fix, and it was the right call then, because
   every section page at the time was narrow content.

   It is not the right call now, because "section page" is not one kind of
   page. REGISTER pages (work_orders, substitute_requests, student_directory)
   are lookup tables and want the screen. FORM and WIZARD pages
   (work_orders/new.php, substitute_requests/new.php) want the ~800px reading
   measure and MUST keep it. So the fix is NOT to widen or drop the
   :not([data-section]) - that would drag the wizards wide along with the
   registers. The fix is an OPT-IN that a page adds to itself:

       <div class="hub-container hub-container--wide" data-section="work_orders">

   Same max-widths at the same breakpoints as the landing ladder above, so the
   platform keeps ONE width story instead of two. The landing page is
   untouched: it carries no data-section and does not carry this class, so
   nothing in this block can reach it.

   (Student Directory reached the same goal earlier with a page-local <style>
   uncapping .hub-container at >=1025px - see the SD-17 note in
   public/sections/student_directory/index.php. This is that idea promoted out
   of one page's <style> block and into the shared source, so the next register
   does not have to copy-paste a media query.)
   --------------------------------------------------------------------------- */
@media (min-width: 1025px) {
    /* DEFAULT, not opt-in. Richard, 2026-08-24: "make sure any future hub
       sections have this same layout when in desktop... we compact in mobile,
       but desktop is big and full of data."

       The first cut of this was an opt-in `.hub-container--wide` that each page
       had to remember to add. That is the same shape as the legacy table
       toolbar, where an opt-OUT nobody remembered meant every new register was
       born carrying the bug (see the note in public/assets/js/hub-table.js).
       A default you have to opt into is not a default. So the register layout
       is what a section page GETS, and the narrow reading measure is what a
       page asks for.

       max-width:none rather than a 1120/1320 ladder because the brief was to
       mirror Student Directory, and SD is genuinely full-bleed from 1025px -
       it reached that with a page-local <style> (the SD-17 note in
       public/sections/student_directory/index.php). This is that idea promoted
       out of one page and into the shared source, so the next register does not
       copy-paste a media query. SD's own override still applies and is
       identical, so nothing there changes.

       Scoped to [data-section] so the Hub landing grid keeps the
       :not([data-section]) ladder above and is untouched. */
    .hub-container[data-section] {
        max-width: none;
        padding-inline: 24px;
    }

    /* The opt-OUT. Forms, wizards and single-record pages keep a readable
       measure - a 1500px-wide text input is worse than a narrow one, which is
       exactly why the :not([data-section]) shape existed in the first place.
       0,3,0 beats the 0,2,0 above, so it wins regardless of source order. */
    .hub-container[data-section].hub-container--narrow {
        max-width: 800px;
        padding-inline: 16px;
    }
}

/* Touch devices: disable hover lift */
@media (hover: none) {
    .action-card:hover {
        transform: none;
        box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 0 0 1px rgba(0,0,0,0.04);
    }
}

/* Dark mode */
@media (prefers-color-scheme: dark) {
    .main-content.hub-page {
        background: var(--hub-page-bg, #1a1a1a);
    }

    .action-card {
        background: var(--hub-tile-bg, #2a2a2a);
        box-shadow: 0 1px 3px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.06);
    }

    .action-label {
        color: var(--hub-tile-text, #e0e0e0);
    }

    .action-hint {
        color: var(--hub-card-description, #888);
    }
}

/* Accessibility: Reduce motion */
@media (prefers-reduced-motion: reduce) {
    .action-card {
        transition: none;
    }
    .action-icon {
        transition: none;
    }
}

/* ==========================================================================
   SIDEBAR & MENU
   ========================================================================== */

.menu-group.collapsed .menu-group-arrow {
    transform: rotate(-90deg);
}

.menu-group-items {
    list-style: none;
    padding-left: 15px;
    margin: 0;
    max-height: 500px;
    overflow: hidden;
    transition: max-height 0.3s ease, opacity 0.3s ease;
    opacity: 1;
}

.menu-group.collapsed .menu-group-items {
    max-height: 0;
    opacity: 0;
}

.menu-group-items li {
    margin: 0;
}

.menu-group-items li a {
    padding: 10px 20px;
    font-size: 0.95em;
}

/* Menu spacer to push items to bottom */
.menu-spacer {
    flex: 1;
    min-height: 20px;
}

/* Make sidebar flex column to support spacer */
.admin-sidebar .admin-menu {
    display: flex;
    flex-direction: column;
    height: 100%;
}

/* Mobile responsive - menu group header */
@media (max-width: 768px) {
    .menu-group-header {
        font-size: 0.9em;
        padding: 10px 15px;
    }
}

/* Icon spacing and sizing for sidebar and dropdown */
.menu-group-header i,
.admin-menu>li>a i {
    margin-right: 8px;
    width: 16px;
    text-align: center;
}

/* ==========================================================================
   MODULES PAGE - Clean & Functional
   ========================================================================== */

.modules-page {
    min-height: 100vh;
    background: var(--secondary-color, #000000);
    position: relative;
}

.modules-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 3rem 1rem;
    position: relative;
    z-index: 1;
}

/* Modules Header */
.modules-header {
    text-align: center;
    margin-bottom: 4rem;
}

.modules-header h1 {
    font-size: 3rem;
    color: white;
    margin-bottom: 1rem;
    font-weight: 800;
    text-shadow: 0 4px 20px rgba(0,0,0,0.3);
}

.modules-header p {
    font-size: 1.3rem;
    color: rgba(255, 255, 255, 0.9);
    text-shadow: 0 2px 10px rgba(0,0,0,0.2);
}

/* Modules Grid */
.modules-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: 2.5rem;
    margin-bottom: 3rem;
}

/* Module Cards */
.module-card {
    background: rgba(255, 255, 255, 0.95);
    backdrop-filter: blur(20px);
    border-radius: 24px;
    padding: 2.5rem;
    box-shadow:
        0 8px 32px rgba(0,0,0,0.2),
        0 0 0 1px rgba(255,255,255,0.3);
    text-decoration: none;
    transition: transform 0.3s ease, box-shadow 0.3s ease;
    border: 2px solid transparent;
    display: flex;
    flex-direction: column;
    position: relative;
    overflow: hidden;
}

.module-card:hover {
    transform: translateY(-8px);
    box-shadow:
        0 20px 60px rgba(0,0,0,0.3),
        0 0 0 1px rgba(201, 151, 0, 0.4);
}

/* Module Icon */
.module-icon {
    font-size: 4rem;
    margin-bottom: 1.5rem;
    transition: transform 0.3s ease;
    display: inline-block;
    filter: drop-shadow(0 4px 12px rgba(0,0,0,0.15));
}

.module-card:hover .module-icon {
    transform: scale(1.1);
}

/* Module Title */
.module-title {
    font-size: 1.75rem;
    font-weight: 700;
    color: var(--secondary-color, #2c3e50);
    margin-bottom: 1rem;
    transition: color 0.3s ease;
}

.module-card:hover .module-title {
    color: var(--primary-color, #C4B454);
}

/* Module Description */
.module-description {
    color: #666;
    line-height: 1.8;
    flex: 1;
    margin-bottom: 1.5rem;
    transition: color 0.3s ease;
}

.module-card:hover .module-description {
    color: #444;
}

/* Module Role Badge */
.module-role {
    display: inline-block;
    padding: 0.5rem 1.25rem;
    background: var(--gold-gradient, linear-gradient(135deg, #C4B454 0%, #A89940 100%)); /* old light nd-gold endpoint retired to the pressed gold */
    color: var(--secondary-color, #000);
    border-radius: 25px;
    font-size: 0.9rem;
    font-weight: 600;
    text-transform: capitalize;
    box-shadow: 0 4px 12px rgba(201, 151, 0, 0.3);
}

/* Empty state */
.modules-empty-state {
    text-align: center;
    padding: 4rem 2rem;
    background: rgba(255, 255, 255, 0.95);
    backdrop-filter: blur(20px);
    border-radius: 24px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}

.modules-empty-state p:first-of-type {
    font-size: 1.3rem;
    color: var(--primary-color, #C4B454);
    font-weight: 600;
    margin-bottom: 1rem;
}

.modules-empty-state p:last-of-type {
    color: #666;
    font-size: 1.1rem;
}

/* Responsive Design */
@media (max-width: 768px) {
    .modules-header h1 {
        font-size: 2rem;
    }

    .modules-header p {
        font-size: 1.1rem;
    }

    .modules-grid {
        grid-template-columns: 1fr;
        gap: 1.5rem;
    }

    .module-card {
        padding: 2rem;
    }

    .module-icon {
        font-size: 3rem;
    }

    .module-title {
        font-size: 1.5rem;
    }
}

/* Loading state */
.modules-loading {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 400px;
}

.modules-loading::after {
    content: '';
    width: 60px;
    height: 60px;
    border: 4px solid rgba(255, 255, 255, 0.3);
    border-top-color: white;
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* Accessibility: Reduce motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* ==========================================================================
   DEV-ONLY CARDS - Richard, 2026-08-24: "I as super need to have the rest
   with a dev card across it"

   A dev-mode section is hidden from everyone but a super admin. Until now it
   was hidden from the super admin's own Hub too whenever "show on Hub" was
   off, which made dev mode useless for the one thing it exists for: reaching
   the thing you are still building. Those cards now appear for super admins
   only, and this band is how you tell at a glance that the tile you are
   looking at is NOT live for anybody else.

   Warning chip tokens rather than a new colour, so it matches the "Dev only"
   chip on the Sections admin list and stays inside the colour gate.

   These rules sit at the END of the file deliberately: the breakpoint blocks
   above re-declare `padding` as a shorthand, which would reset the top padding
   this band needs. Equal specificity, so the later rule wins.
   ========================================================================== */

.action-card--dev {
    /* Clip the band to the card's 16px radius. */
    overflow: hidden;
    /* Room for the band so it never crowds the icon, kept as tight as it can
       be: the band is ~23px tall, so 36px leaves 13px of breathing room under
       it and drops the icon only 8px below its neighbours'. At 42px the row
       read as misaligned. Overrides the shorthand padding set in the
       breakpoints above. */
    padding-top: 36px;
}

.action-card__dev {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    padding: 5px 8px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-align: center;
    background: var(--chip-warning-bg, #F8EFDE);
    color: var(--chip-warning-text, #8A5A17);
    border-bottom: 1px solid var(--chip-warning-border, #EAD6AE);
}

/* A PICKER of a few LARGE tiles, as opposed to the Hub landing's dozen small
   ones. The shared .sections-grid caps cards at 200px and centres them, which
   is right for a wall of tiles and leaves two of them marooned in a wide empty
   page - which is exactly what Richard saw on Reimbursements ("It does look
   empty"). A modifier, never a change to the shared rule: the Hub landing
   depends on that geometry. */
/* THE PICKER TILES - a section's two or three "what do you want to do" cards.
   Unlike the Hub landing tiles, these carry three lines each: a title, a live
   status line, and a sentence of help. That is why they need more width than
   half a phone screen, and why the shared two-up rule is wrong for them.

   This block used to say grid-template-columns: repeat(auto-fit, minmax(240px,
   320px)) - on a parent that is display:flex, where it could never apply. The
   flex equivalent below is what that line was asking for, and it needs no
   media query: a 240px basis puts one tile per row on a phone, two on a
   tablet, and the cap holds them at 320px on a desktop.

   The padding override that used to live here is GONE on purpose. It restated
   the desktop padding at (0,2,0), which outranked the phone rule at (0,1,0),
   so these tiles kept 20px side padding while being squeezed to half width -
   about 137px of usable text at 375px. The base .action-card already handles
   both ends (20px 12px 16px below 480, 28px 20px 24px above 600), so deleting
   the override fixes the phone without changing the desktop. */
.sections-grid--picker {
    gap: 20px;
}
.sections-grid--picker > * {
    flex: 1 1 240px;
    max-width: 320px;
    min-width: 0;
}

/* The number the section behind this tile exists to track. Sits under the
   label and above the description: name, then how much, then what it is. */
.action-stat {
    margin-top: 6px;
    font-size: 0.95rem;
    font-weight: 700;
    color: var(--gold-text, #75692E);
    line-height: 1.3;
}

/* ========== shared/shared-media.css ========== */

/**
 * ============================================================================
 * Shared Responsive Styles
 * ============================================================================
 * Mobile-first media queries for common components (navbar, footer, modals)
 *
 * Breakpoints:
 *   - Extra-small:  up to 360px
 *   - Mobile:       up to 767px
 *   - Tablet:       768px to 1024px
 *   - Desktop:      1025px and up
 *   - Print:        @media print
 *
 * IMPORTANT: Every section MUST be wrapped in a proper @media query.
 * Do NOT place styles outside of @media blocks (except global utilities at bottom).
 * ============================================================================
 */

/* ==========================================================================
   TABLET STYLES (768px - 1024px)
   ========================================================================== */
@media (min-width: 768px) and (max-width: 1024px) {
    /* (The .admin-sidebar off-canvas rules that lived here moved to the
       canonical <=992px drawer block in enterprise-components.css - keeping
       them here pulled the sidebar out of the grid at 993-1024px while the
       shell still reserved its 280px column.) */
    .hamburger-menu {
        display: flex !important;
        flex-direction: column;
        justify-content: space-around;
        width: 30px;
        height: 25px;
        background: transparent;
        border: none;
        cursor: pointer;
        padding: 0;
        position: fixed;
        left: 15px;
        top: 15px;
        z-index: 10000;
    }
    .hamburger-menu span {
        width: 30px;
        height: 3px;
        background: var(--header-text-color, #fff);
        border-radius: 2px;
        transition: all 0.3s ease;
        transform-origin: center;
    }
    .hamburger-menu.active span:nth-child(1) {
        transform: translateY(11px) rotate(45deg);
    }
    .hamburger-menu.active span:nth-child(2) {
        opacity: 0;
    }
    .hamburger-menu.active span:nth-child(3) {
        transform: translateY(-11px) rotate(-45deg);
    }
    .sidebar-overlay {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.5);
        z-index: 9998;
    }
    .sidebar-overlay.active {
        display: block;
    }
    .admin-content {
        margin-left: 0 !important;
        padding: 20px 15px;
        width: 100%;
    }
    /* Only apply hamburger padding on admin pages that actually have the sidebar */
    .admin-root .navbar {
        padding-left: 60px !important;
    }
    .color-grid {
        grid-template-columns: repeat(2, 1fr) !important;
        gap: 15px;
    }
    .table-container {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
    }
    /* Scope min-width to admin tables only */
    .admin-root table {
        min-width: 600px;
    }
    #themesContainer .theme-card {
        margin-bottom: 15px;
    }
    .admin-root .form-group input,
    .admin-root .form-group select,
    .admin-root .form-group textarea {
        width: 100% !important;
    }
    .modal-content {
        width: 90%;
        max-width: 600px;
        margin: 10% auto;
    }
}

/* ==========================================================================
   MOBILE STYLES (up to 767px)
   ========================================================================== */
@media (max-width: 767px) {
    /* Typography - Scale down admin headings only */
    .admin-root h1 {
        font-size: 1.5rem !important;
    }
    .admin-root h2 {
        font-size: 1.3rem !important;
    }
    .admin-root h3 {
        font-size: 1.1rem !important;
    }
    .admin-root h4 {
        font-size: 1rem !important;
    }

    .navbar {
        padding: 10px 15px !important;
        height: auto !important;
        min-height: 60px;
        max-height: none !important;
    }
    /* Brand lockup mobile sizes - keep in sync with hub-components.css
       (these !important overrides win by bundle order). Title 16px NOWRAP,
       subtitle 11px and VISIBLE. */
    .nav-brand-title {
        font-size: 1rem !important;
        white-space: nowrap !important;
    }
    .nav-brand-subtitle {
        font-size: 0.6875rem !important;
    }
    /* (Removed the .logo height:50px/max-height:50px cap: .logo is ONLY the
       login page's mascot+title+subtitle stack - this !important cap crushed
       the ~82px image + h1 + h2 into a 50px box on phones so the title/subtitle
       rendered under the image. The header brand uses .nav-brand, not .logo.) */
    /* (.admin-sidebar phone overrides removed - the <=992px drawer in
       enterprise-components.css is the single source of truth.) */
    .sidebar-menu a {
        padding: 15px 20px !important;
        font-size: 1rem !important;
    }
    .admin-content {
        padding: 15px 10px !important;
    }
    .color-grid {
        grid-template-columns: 1fr !important;
        gap: 12px;
    }
    .color-pair {
        flex-direction: column !important;
        gap: 12px;
    }
    .color-pair .color-item {
        width: 100% !important;
    }
    .color-input-group {
        width: 100% !important;
    }
    .color-input-group input[type="color"] {
        width: 60px !important;
        height: 50px !important;
    }
    .color-input-group input[type="text"] {
        flex: 1;
        font-size: 1rem !important;
    }
    .btn-group {
        flex-direction: column !important;
        gap: 10px;
    }
    .theme-card .hub-btn {
        width: 100% !important;
        margin-bottom: 8px;
    }
    .table-responsive {
        border: 0;
    }
    .table-responsive thead {
        display: none;
    }
    .table-responsive tr {
        display: block;
        margin-bottom: 15px;
        border: 1px solid var(--border-primary, #e5e7eb);
        border-radius: 8px;
        padding: 10px;
        background: white;
    }
    .table-responsive td {
        display: block;
        text-align: right;
        padding: 8px;
        border: none;
        position: relative;
        padding-left: 50%;
    }
    .table-responsive td:before {
        content: attr(data-label);
        position: absolute;
        left: 10px;
        font-weight: bold;
        text-align: left;
    }
    .form-row {
        flex-direction: column !important;
    }
    .admin-root .form-group {
        width: 100% !important;
        margin-bottom: 15px;
    }
    .color-section-header {
        padding: 15px !important;
        cursor: pointer;
    }
    .color-section-header h3 {
        font-size: 1.1rem !important;
    }
    .badge {
        padding: 6px 12px !important;
        font-size: 0.875rem !important;
    }
    .role-badge-preview {
        padding: 8px 16px !important;
        font-size: 0.875rem !important;
    }
    .modal-content {
        width: 95%;
        margin: 5% auto;
        max-height: 90vh;
        overflow-y: auto;
    }
    .tab-buttons {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        white-space: nowrap;
        margin-bottom: 20px;
    }
    .tab-button {
        padding: 10px 15px !important;
        font-size: 0.875rem !important;
    }
    .card {
        margin-bottom: 15px;
    }
    /* (.footer-content column-stack removed - the footer is a compact single
       row on phones now; see shared/footer.css.) */
    .filter-group {
        flex-direction: column !important;
        gap: 10px;
    }
    .filter-group input,
    .filter-group select {
        width: 100% !important;
    }
    .hub-tiles,
    .module-grid {
        grid-template-columns: 1fr !important;
        gap: 15px;
    }
    .section-card {
        width: 100% !important;
    }
    .vehicle-card {
        width: 100% !important;
    }
    .stats-grid {
        grid-template-columns: 1fr !important;
    }
    .header-actions {
        flex-direction: column !important;
        align-items: stretch !important;
    }
    .color-preview {
        flex-wrap: wrap !important;
    }
}

/* ==========================================================================
   LANDSCAPE PHONE (up to 667px height in landscape orientation)
   ========================================================================== */
@media (max-height: 667px) and (orientation: landscape) {
    .sidebar-menu a {
        padding: 10px 15px !important;
    }
    .modal-content {
        max-height: 85vh;
        overflow-y: auto;
    }
}

/* ==========================================================================
   EXTRA SMALL DEVICES (up to 360px)
   ========================================================================== */
@media (max-width: 360px) {
    .navbar {
        padding-left: 15px !important;
    }
    .hamburger-menu {
        left: 10px;
        width: 25px;
        height: 20px;
    }
    .hamburger-menu span {
        width: 25px;
        height: 2px;
    }
    .admin-root h1 {
        font-size: 1.3rem !important;
    }
    .admin-root h2 {
        font-size: 1.1rem !important;
    }
}

/* ==========================================================================
   PRINT STYLES
   ========================================================================== */
@media print {
    .admin-content {
        margin: 0 !important;
        padding: 0 !important;
    }
    .color-section-body {
        display: block !important;
    }
    .collapsed {
        display: block !important;
    }
}

/* ==========================================================================
   UTILITY CLASSES (always global)
   ========================================================================== */
.mobile-only {
    display: none !important;
}
.desktop-only {
    display: block !important;
}

@media (max-width: 767px) {
    .mobile-only {
        display: block !important;
    }
    .desktop-only {
        display: none !important;
    }
}

.admin-sidebar::-webkit-scrollbar {
    width: 5px;
}
.admin-sidebar::-webkit-scrollbar-thumb {
    background: var(--scrollbar-thumb, #d1d5db);
    border-radius: 3px;
}

/* ========== section-shell.css ========== */

/*
 * Shared section-page chrome: breadcrumb + back button and the SVG section
 * header. Scoped classes; safe alongside existing hub styles. Mobile-first:
 * the Back button is a full-size (44px) tap target; on desktop the breadcrumb
 * trail sits inline with it.
 */
/* Renders inside .hub-container, which owns the column width and padding -
   no max-width/side padding of its own, so the back link shares the ONE
   left edge with everything else (section-landing rebuild, 2026-07-31). */
.section-back {
    padding: 4px 0 0;
}
/* Density (bottom chrome round): the band occupies ~28px of flow while the
   link keeps its full 44px tap target via negative vertical margins. */
.section-back .crumb-back {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    min-height: 36px; /* 44px tap target at touch widths (block just below) */
    margin: -10px 0;
    padding: 0 8px 0 2px;
    color: var(--gray-600);
    font-size: 0.9rem;
    text-decoration: none;
    border-radius: 8px;
}
@media (max-width: 1024px) {
    .section-back .crumb-back {
        min-height: var(--tap-target, 2.75rem);
    }
}
.section-back .crumb-back svg { width: 1.15rem; height: 1.15rem; opacity: 0.7; }
.section-back .crumb-back:hover { color: var(--gray-900); text-decoration: underline; }
.section-back .crumb-back:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold, #C4B454) 45%, transparent);
}

/* ==========================================================================
   COMPACT SECTION HEADER ROW (section-landing rebuild, 2026-07-31): replaces
   the centered hero. 36px icon square + title + subtitle on one line, primary
   action right-aligned in the row at desktop / full-width 48px below at touch
   widths. The glyph takes the section palette (tint square + accent glyph,
   the same pairing as the hub tiles - a white glyph on bright gold computes
   2.1:1 and fails R2, so the spec's "white glyph" is deliberately NOT used).
   ========================================================================== */
.section-head {
    display: flex;
    /* flex-start + the text pad below: with a wrapped two-line subtitle,
       align-items:center floated the icon beside the SUBTITLE while the
       title sat alone above it (2026-07-31 review). Anchoring to the top
       and padding the text 5px centers the 21px title line against the
       36px icon in both the one-line and wrapped cases. */
    align-items: flex-start;
    gap: 12px;
    flex-wrap: wrap;
    margin: 12px 0 16px;
    min-width: 0;
}
.section-head-icon { display: inline-flex; flex: none; }
.section-head-icon .svg-icon {
    display: inline-flex;
    width: 36px;
    height: 36px;
    padding: 8px; /* border-box: 20px glyph inside the 36px square */
    background: var(--section-tint, var(--gold-tint, #F5EFD0));
    border-radius: 10px;
    color: var(--section-accent, var(--gold-text, #75692E));
}
/* fill/stroke come from the SVG's own attributes: legacy solid glyphs carry
   fill="currentColor", the Lucide set carries fill="none" stroke="currentColor";
   a CSS fill here would blob the stroked set. */
.section-head-icon .svg-icon svg { width: 100%; height: 100%; }
.section-head-text {
    display: flex;
    align-items: baseline;
    gap: 10px;
    flex-wrap: wrap;
    min-width: 0;
    padding-top: 5px; /* centers the title line on the 36px icon (see above) */
    /* Grow-with-a-small-basis: the parent row wraps, so without a basis the
       text block JUMPS below the icon on phones instead of shrinking beside
       it (the subtitle then wraps within this block, which is fine). */
    flex: 1 1 220px;
}
.section-head h1 {
    font-size: 21px;
    font-weight: 800;
    line-height: 1.2;
    margin: 0;
    color: var(--hub-title-color, var(--ink, #191712));
}
.section-head-sub {
    font-size: 13px;
    color: var(--ink-mute, #6F6A5C);
    margin: 0;
}
@media (min-width: 1024px) {
    .section-head-action { margin-left: auto; flex: none; align-self: center; }
}
/* Touch widths keep today's mobile CTA treatment: full-width, 48px. */
@media (max-width: 1023px) {
    .section-head-action { width: 100%; }
    .section-head-action .hub-btn { width: 100%; height: 48px; }
}

/* Inline-SVG icons inside action / drill-down cards: same treatment sized to
   the .action-icon box (56px on desktop; 12px padding keeps the existing
   32px glyph). */
.action-icon .svg-icon {
    display: inline-flex;
    width: 100%;
    height: 100%;
    padding: 12px;
    background: var(--section-tint, var(--gold-tint, #F5EFD0));
    border-radius: 12px;
    color: var(--section-accent, var(--gold-text, #75692E));
}
.action-icon .svg-icon svg { width: 100%; height: 100%; }

/* The .action-icon box shrinks to 44px on small phones (hub-animations); keep
   the glyph proportionate there (28px) instead of a 20px speck. */
@media (max-width: 480px) {
    .action-icon .svg-icon { padding: 8px; }
}

/* ==========================================================================
   "YOUR ITEMS" PANEL (section-landing rebuild, 2026-07-31): a bordered card
   spanning the full content column - ONE left edge with the header row.
   Head row = h2 + count chip; rows = title / muted meta / status chip on a
   single line (density law); the empty state lives INSIDE the panel.
   ========================================================================== */
.section-panel {
    margin: 20px 0 0;
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
    background: var(--surface, #FFFFFF);
    overflow: hidden;
}
.section-panel-head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 16px;
    background: var(--gray-100, #faf9f8);
    border-bottom: 1px solid var(--line, #E6E2D2);
}
.section-panel-head h2 {
    font-size: 15px;
    font-weight: 700;
    margin: 0;
}
.section-count {
    font-size: 12px;
    font-weight: 700;
    line-height: 1.4;
    padding: 1px 8px;
    border-radius: 99px;
    background: var(--gold-tint, #F5EFD0);
    border: 1px solid var(--gold-border, #D9CD8A);
    color: var(--chip-pending-text, #75692E);
}
.section-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 16px;
    border-bottom: 1px solid var(--gray-200, #f3f2f1);
    min-width: 0;
}
.section-row:last-child { border-bottom: 0; }
.section-row-title {
    font-weight: 600;
    flex: 0 1 auto;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.section-row-meta {
    font-size: 13px;
    color: var(--gray-600, #8a8886);
    flex: 1 1 auto;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.section-row .status-chip { margin-left: auto; flex: none; }
.section-row form { margin: 0; flex: none; }
/* A decision note (denial reason) is STATUS COMMUNICATION, not decorative
   subtext: it gets its own wrapping line in the semantic error color - the
   ellipsized meta line would clip it invisible (2026-07-31 review). */
.section-row { flex-wrap: wrap; }
.section-row-note {
    flex-basis: 100%;
    font-size: 12px;
    color: var(--error, #A4262C);
    white-space: normal;
}

/* In-panel empty state: icon in a gold-tint circle + bold line + muted
   explanation - never a bare sentence under a heading. */
.section-empty { text-align: center; padding: 28px 16px 32px; }
.section-empty-icon {
    display: inline-flex;
    width: 44px;
    height: 44px;
    padding: 10px;
    border-radius: 50%;
    background: var(--gold-tint, #F5EFD0);
    color: var(--gold-text, #75692E);
}
.section-empty-icon .svg-icon { display: inline-flex; width: 100%; height: 100%; }
.section-empty-icon .svg-icon svg { width: 100%; height: 100%; }
.section-empty-title { font-weight: 700; margin: 10px 0 2px; }
.section-empty-sub { font-size: 13px; color: var(--ink-mute, #6F6A5C); margin: 0; }

/* Section landing density: content begins high (the hero used to push the
   h1 ~185px down on phones) and the column does not trail 60px of padding
   into the pinned-footer gap. Scoped to section containers ([data-section])
   so the hub dashboard's own greeting spacing is untouched. */
@media (min-width: 900px) {
    .hub-container[data-section] { padding-top: 16px; padding-bottom: 40px; }
}
/* 481-899px: the base container keeps 24px top padding, which put first
   content at ~102px against the <=90px mobile budget (review caveat). */
@media (min-width: 481px) and (max-width: 899px) {
    .hub-container[data-section] { padding-top: 12px; }
}
/* The PO landing's dense form capped itself at 720px and centered - a second
   left edge 16px inboard of the column's. Inside a section container the
   form spans the column like everything else. */
.hub-container[data-section] .flow-dense { max-width: none; }
/* ...and on a wide screen its fields SHARE that width instead of each running
   the whole column: a one-line vendor box 1,815px wide is hard to read and
   harder to aim at (desktop sweep, 2026-09-28). The first multi-line field
   takes the left column across two rows, the short fields stack on the right,
   and anything that is not a field - notes, errors, the Submit row - keeps the
   full width. Still one left edge. Specificity (0,3,0) beats flow-wizard.css's
   .flow-dense, which the build concatenates after this file. */
@media (min-width: 1100px) {
    .hub-container[data-section] .flow-dense {
        display: grid;
        grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
        column-gap: 24px;
        align-items: start;
    }
    .hub-container[data-section] .flow-dense > :not(.dense-field) {
        grid-column: 1 / -1;
    }
    .hub-container[data-section] .flow-dense > .dense-field:has(> .flow-textarea) {
        grid-row: span 2;
    }
}

/* FORM PAGE rule 3 on a section page: the header row that carries Save
   (form=) is the page's sticky header - it stays in view as the form scrolls,
   pinned under the navbar. THE SHELL TRAP (measured 2026-09-14): shared/
   base.css guards horizontal page scroll with overflow-x:hidden on html AND
   body AND .page-wrapper AND .main-content. `hidden` makes an element a
   scroll container, and because html carries the value too, body's copy no
   longer propagates to the viewport - so body, .page-wrapper and
   .main-content are each a scroll container that never scrolls, and a sticky
   descendant pins to the nearest one's own box instead of the viewport:
   nothing stuck in the hub shell, the navbar included. overflow: clip clips
   exactly the same and creates NO scroll container, so on a form page (and
   only there - scoped via :has, the precedent being
   .admin-main-content:has(> .form-page-header) in enterprise-components.css)
   the three swap to clip (both axes on body: clip beside auto computes back
   to hidden) and document scroll carries sticky as designed: the navbar
   (position: sticky, top: 0, shared/header.css) holds and this row pins
   right under it at --header-height. html keeps its guard - the viewport is
   the scroll container we want. Ground-coloured so it is invisible at rest. */
html:has(.form-page-sticky) body { overflow: clip; }
.page-wrapper:has(.form-page-sticky),
.page-wrapper:has(.form-page-sticky) > .main-content { overflow-x: clip; }
.form-page-sticky {
    position: sticky;
    top: var(--header-height, 80px);
    z-index: 20;
    background: var(--background-color, #FFFFFF);
    /* The brief's header recipe: opaque ground + a bottom rule so fields
       pass cleanly behind it while pinned. (The rule also stops the title
       row's 16px bottom margin collapsing through, so the band ends 16px
       under the buttons - the same spacing as before, now painted.) */
    border-bottom: 1px solid var(--line, #E6E2D2);
    /* Seam cover. On phones the navbar is content-sized (shared-media.css:
       height:auto at <= 767px; 78px with the default logo) while the token
       stays 80px, so the pinned row sits 2px shy of the navbar and scrolled
       fields showed through the gap. A ground-coloured band painted above
       the box (shadows paint outside it, under the z-index:10000 navbar)
       closes any shortfall up to 12px; at rest it lies in the row's own top
       margin, ground on ground, invisible. */
    box-shadow: 0 -12px 0 var(--background-color, #FFFFFF);
}
.form-page-sticky .section-head-action { display: flex; gap: 8px; }

/* PHONE: THE NAVBAR IS CONTENT-SIZED, SO THE TOKEN IS THE WRONG NUMBER.
   --header-height is a theme SETTING and describes the DESKTOP header; on this
   district's Hub it is 122px. Below 768px shared-media.css gives .navbar
   height:auto (min-height 60px), which measures 78px with the default logo -
   so pinning to the token left 44px of open air between the header and this
   row, with the form's own fields scrolling visibly through the gap.

   Measured on /sections/reimbursements/fuel/edit at 390x844, 2026-09-14:
   token 122px, navbar 78px, gap 44px.

   The seam cover above was written for this and sized for a 2px shortfall
   against an assumed 80px token; it could never close 44. Pinning to the real
   phone height fixes the cause, and the cover keeps absorbing the small drift
   it was meant for - a taller logo, a second line of district name.

   78px is the measured content height, not a guess at a design value: if the
   logo changes, this is the line to re-measure. */
@media (max-width: 767px) {
    .form-page-sticky {
        /* 78 is the navbar's BOX; its shadow (0 2px 8px) paints 10px
           below that, so 88 is where the header visually ends. Pinning
           at 78 put this row inside the fade. */
        top: 88px;
        /* Wider tolerance than the desktop 12px, because down here the thing
           we are pinning under sizes itself to its contents. */
        box-shadow: 0 -24px 0 var(--background-color, #FFFFFF);
    }
}
/* Phones: the pinned band must stay small (the stacked icon / title /
   subtitle / two 48px buttons measured 138px at 375px, 216px with the
   navbar - 27% of the viewport never scrolling away). One title row:
   title left, the two actions right at the 44px touch height and their
   natural width (no full-width CTA here - Save is a header action, not a
   page CTA); the subtitle - which trip this is - keeps its own line under
   them, one line, ellipsised; the decorative icon goes. The text block is
   display:contents so its h1 and subtitle place on the grid directly. */
@media (max-width: 1023px) {
    .form-page-sticky .section-head {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: center;
        column-gap: 12px;
        row-gap: 2px;
    }
    .form-page-sticky .section-head-icon { display: none; }
    .form-page-sticky .section-head-text { display: contents; }
    .form-page-sticky .section-head h1 { grid-column: 1; grid-row: 1; }
    .form-page-sticky .section-head-sub {
        grid-column: 1 / -1;
        grid-row: 2;
        min-width: 0;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
    .form-page-sticky .section-head-action { grid-column: 2; grid-row: 1; width: auto; }
    .form-page-sticky .section-head-action .hub-btn {
        flex: none;
        width: auto;
        height: var(--btn-h-touch, 44px);
    }
}

/* Success flash shown after a wizard submits. */
.section-flash {
    max-width: 1100px;
    margin: 0 auto 12px;
    padding: 12px 16px;
    background: #dff6dd;
    border: 1px solid var(--success);
    border-radius: 8px;
    color: #0b5c0b;
    font-weight: 600;
}

/* The same flash for a FAILURE (2026-09-28). The base above is the success
   style, and pages had been painting red text over its green inline - red on
   green, 4.22:1, below AA. The danger chip's own tint, border and text
   (6.22:1). */
.section-flash--error {
    background: var(--chip-danger-bg);
    border-color: var(--chip-danger-border);
    color: var(--chip-danger-text);
}
/* Primary action button for section pages (submits/CTAs outside a wizard).
   Wired to the district theme tokens from /api/theme-css.php; hover matches
   the theme's darkened gold (--scrollbar-thumb-hover). Text on gold must be
   black - white fails WCAG contrast. */
.hub-btn-primary {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--tap-target, 2.75rem);
    padding: 12px;
    border: none;
    border-radius: 6px;
    background: var(--btn-primary-bg, #C4B454);
    color: var(--btn-primary-text, #000000);
    font-size: 1rem;
    font-weight: 600;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.2s ease;
}
.hub-btn-primary:hover {
    background: var(--scrollbar-thumb-hover, #A89940);
    color: var(--btn-primary-text, #000000);
}
.hub-btn-primary:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold, #C4B454) 45%, transparent), 0 0 0 1px #fff inset;
}


/* ==========================================================================
   FORM ACTION BAR - the shared sticky save bar (bottom chrome model).
   Same visual recipe as the wizard's .flow-actions: white, top border,
   upward shadow, pinned flush above the always-visible footer; the form
   scrolls behind it. Emitted by SectionShell::actionBar(); pair the page
   content with .has-action-bar so nothing hides behind the stack.
   ========================================================================== */
/* DERIVED bar height - single source for the bar's own size and the content
   clearance below it: 0.75rem padding x2 + 2.75rem button + 1px border.
   (Phones: buttons grow to 3rem - see the <=640px override.) */
:root {
    --action-bar-height: calc(1.5rem + 2.75rem + 1px);
}
.form-action-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(var(--footer-height, 40px) + env(safe-area-inset-bottom, 0px));
    z-index: 60;
    box-sizing: border-box;
    min-height: var(--action-bar-height);
    display: flex;
    gap: 0.75rem;
    justify-content: flex-end;
    padding: 0.75rem 16px;
    background: var(--surface, #FFFFFF);
    border-top: 1px solid var(--line, #E6E2D2);
    box-shadow: 0 -1px 2px rgba(23, 21, 15, 0.06), 0 -8px 24px rgba(23, 21, 15, 0.08);
}
.form-action-bar .flow-btn { min-height: 2.75rem; }
@media (max-width: 640px) {
    :root { --action-bar-height: calc(1.5rem + 3rem + 1px); }
    .form-action-bar .flow-btn { flex: 1; min-height: 3rem; }
}
/* Content clearance for the bar+footer stack. Self-sufficient on purpose:
   it must protect pages WITHOUT a .main-content ancestor (profile.php is
   exactly that case); inside .main-content it simply over-clears a little.
   !important ON PURPOSE: this is the bottom-chrome safety invariant and a
   page-local "padding: 20px" shorthand has already beaten it once (the
   profile ?tab=contact regression - fields unreachable behind the chrome).
   Fully DERIVED: footer + bar + notch + 12px, nothing hardcoded. */
.has-action-bar {
    padding-bottom: calc(var(--footer-height, 40px) + var(--action-bar-height, 69px) + env(safe-area-inset-bottom, 0px) + 12px) !important;
}

/* Desktop: align the bar's buttons with the centered form column instead of
   the viewport edge (--action-bar-align = the form column's max-width;
   + 20px = the column's own horizontal padding). 100% not 100vw: 100vw
   includes the scrollbar, which pushed the button ~8-15px inboard of the
   form's true right edge. */
@media (min-width: 769px) {
    .form-action-bar {
        padding-right: max(16px, calc((100% - var(--action-bar-align, 900px)) / 2 + 20px));
    }
}

/* ==========================================================================
   CHECK-PRINTING LINE ITEMS - the 4-col row (description / account / amount /
   delete) is fine on desktop but crushes at phone widths (amount cell ~49px).
   At <=576px each line becomes a mini-card: description full-width, then
   account + amount side by side, delete a real 44px target.
   ========================================================================== */
@media (max-width: 576px) {
    .check-lines thead { display: none; }
    .check-lines,
    .check-lines tbody { display: block; }
    .check-lines tr {
        display: grid;
        grid-template-columns: 1fr minmax(90px, 0.7fr) 44px;
        grid-template-areas:
            "desc desc desc"
            "acct amt  del";
        gap: 8px;
        align-items: center;
        margin-bottom: 10px;
        padding: 8px;
        border: 1px solid var(--gray-300, #edebe9);
        border-radius: 8px;
    }
    .check-lines td { padding: 0 !important; } /* beat the inline cell padding */
    .check-lines td:nth-child(1) { grid-area: desc; }
    .check-lines td:nth-child(2) { grid-area: acct; }
    .check-lines td:nth-child(3) { grid-area: amt; }
    .check-lines td:nth-child(4) { grid-area: del; text-align: center; }
    .check-lines .li-amt { min-width: 90px; }
    .check-lines .li-del {
        min-width: 44px;
        min-height: 44px;
        line-height: 1;
    }
}

/* ========== flow-wizard.css ========== */

/*
 * Step-wizard component styles (DESIGN-BRIEF section 2 WIZARD/STEPPER: the
 * mobile flagship). Rules stay scoped under .flow-wizard, but the --flow-*
 * tokens are declared on :root: section landing pages reuse .flow-btn
 * standalone (e.g. "Start a report" on bullying_reports), and scoping the
 * tokens to .flow-wizard left those buttons unstyled/transparent. The names
 * are namespaced, so collision risk is nil.
 *
 * Palette maps to the district theme tokens from /api/theme-css.php (gold with
 * black text - white on #C4B454 fails WCAG contrast; the theme loads after the
 * bundle, and var() resolves at computed-value time, so the indirection works).
 * Brief-token names (--ink, --line, --gold-tint...) are referenced with their
 * canonical fallback values so they hook up automatically as the tokenization
 * program lands them.
 *
 * REUSABLE PRIMITIVES: .stepper (container), .choice-cards / .choice-card
 * (+-icon/-label) are aliases of the wizard classes so any page can use the
 * accessible big-label card-choice pattern (hidden input, 44px target, focus
 * ring) without a flow. Markup: <label class="choice-card"><input type="radio"
 * ...><span class="choice-card-label">...</span></label> in a .choice-cards.
 *
 * Mobile-first. Identical markup centers on desktop. No emoji; icons are inline
 * SVG that inherit currentColor. WCAG 2.2 AA: >=44px targets, >=3:1 focus ring.
 */
:root {
    /* Tier: local semantic tokens (per-section accent overrides --flow-accent) */
    --flow-accent: var(--btn-primary-bg, #C4B454);
    --flow-accent-surface: var(--gold-tint, #F5EFD0);
    --flow-accent-on: var(--btn-primary-text, #000000);
    --flow-surface: var(--surface, #FFFFFF);
    --flow-surface-sunken: var(--page, #FAF9F4);
    --flow-text: var(--ink, #191712);
    --flow-text-soft: var(--ink-soft, #4A4638);
    --flow-text-muted: var(--ink-mute, #6F6A5C);
    --flow-border: var(--line, #E6E2D2);
    --flow-border-strong: var(--gold-border, #D9CD8A);
    --flow-error: #A4262C;
    /* The wizard keeps its own name for this - that indirection is the
       right shape and predates the global token being trustworthy - but
       it now resolves from one place instead of restating the number. */
    --flow-radius: var(--radius-control);
    --flow-gap: 12px;
    --flow-pad: 16px;
    --flow-target: 2.75rem; /* 44px floor; bar buttons go 48px on phones */
    --flow-focus: 0 0 0 3px color-mix(in srgb, var(--gold, #C4B454) 45%, transparent);
    --flow-maxw: 34rem;
    /* One shadow recipe (brief SHAPE & DEPTH), plus its upward flip for the bar */
    --flow-shadow: 0 1px 2px rgba(23, 21, 15, 0.06), 0 8px 24px rgba(23, 21, 15, 0.08);
    --flow-shadow-up: 0 -1px 2px rgba(23, 21, 15, 0.06), 0 -8px 24px rgba(23, 21, 15, 0.08);
}

.flow-wizard,
.stepper {
    box-sizing: border-box;
    max-width: var(--flow-maxw);
    margin: 0 auto;
    /* Density: minimal top padding so the question starts high on phones;
       bottom clearance = the fixed bar (48px buttons + bar padding + 12px) -
       the pinned footer's own clearance comes from .main-content. */
    padding: 0.25rem var(--flow-pad);
    padding-bottom: calc(3rem + 12px); /* bar clearance only - the hub-container and .main-content paddings stack under this */
    color: var(--flow-text);
    font-family: var(--font-family, inherit); /* platform stack (Roboto stays) */
}
.flow-wizard *, .flow-wizard *::before, .flow-wizard *::after,
.stepper *, .stepper *::before, .stepper *::after { box-sizing: border-box; }

/* Progress: "STEP X OF N" caps counter + named gold segment trail ---------- */
/* 8px clears the back crumb above (wizard-scoped: .flow-progress never renders
   outside a flow step, so non-wizard crumb pages are untouched); 16px below
   separates the progress block from the question title. */
.flow-progress { margin: 0.5rem 0 1rem; padding: 0; }
.flow-progress-counter {
    margin: 0 0 0.875rem; /* 14px: clear air above the phase-tab trail so the
                             counter and tabs read as two elements, not one
                             stuck-together clump (verified live). Colour stays
                             --flow-text-soft per the small-caps floor - the
                             muted tone is barred on caps-micro labels. */
    line-height: 1.2;
    font-size: 0.8125rem; /* 13px - clears the 12px caps floor */
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--flow-text-soft);
}
.flow-progress ol,
.flow-trail { display: flex; gap: 0.4rem; list-style: none; margin: 0; padding: 0; }
.flow-seg {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    text-align: center;
    font-size: 0.75rem; /* 12px caps floor - never smaller, never muted */
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--flow-text-soft);
    line-height: 1.2;
    padding: 0 0.375rem 0.5rem; /* 6px inter-name air */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.flow-seg::after {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 4px;
    border-radius: 99px;
    background: var(--flow-border);
}
/* Fills are gold; names stay ink/muted (gold is never text on light - R3). */
.flow-seg.is-done::after,
.flow-seg.is-current::after { background: var(--flow-accent); }
.flow-seg.is-current { color: var(--flow-text); }

/* Question ----------------------------------------------------------------- */
/* FLOW STEP LAYOUT rule 1: EVERY question on a screen carries this exact
   style - the step title and any question-phrased sub-field label are peers
   (same size, same weight, same left edge). .flow-question below mirrors
   these values; change them together or the peer rule breaks. */
.flow-title {
    font-size: 1.5rem; /* 24px/800 step question */
    line-height: 1.3;
    margin: 0 0 0.625rem; /* 10px question -> control (rule 5) */
    font-weight: 800;
    color: var(--flow-text);
}
.flow-help { font-size: 0.8125rem; color: var(--flow-text-muted); margin: 0 0 1rem; }
.flow-error {
    background: var(--chip-danger-bg, #F9EAEB); color: var(--flow-error);
    border: 1px solid var(--chip-danger-border, #EBC8CA);
    border-radius: 8px; padding: 0.6rem 0.75rem; margin: 0 0 1rem; font-size: 0.9rem;
}
/* The odometer question. NOT an error - the sink is asking for a second
   look, so it takes the notice treatment rather than the danger one. */
.flow-confirm-ask {
    background: var(--flow-accent-surface); color: var(--flow-text-soft);
    border: 1px solid var(--flow-border-strong);
    border-left-width: 4px; border-left-color: var(--flow-accent);
    border-radius: 8px; padding: 0.6rem 0.75rem; margin: 0 0 1rem;
    font-size: 0.9rem; line-height: 1.45;
}
.flow-body { margin-bottom: 1rem; animation: flowStepIn 250ms ease-out both; }
@keyframes flowStepIn {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: none; }
}

/* Option cards (single/multi select) -------------------------------------- */
.flow-options,
.choice-cards { display: grid; gap: var(--flow-gap); }
.flow-option,
.choice-card {
    display: flex; align-items: center; gap: 0.85rem;
    min-height: 4rem; /* 64px cards (content was driving ~55px past the 44px floor) */ padding: 0.85rem 1rem;
    background: var(--flow-surface); border: 1px solid var(--flow-border);
    border-radius: var(--flow-radius); cursor: pointer;
    transition: border-color 150ms ease, background 150ms ease,
        box-shadow 150ms ease, transform 150ms ease;
}
.flow-option input,
.choice-card input { position: absolute; opacity: 0; width: 1px; height: 1px; }
/* Glyphs take the section accent (the wizard wrapper carries data-section),
   falling back to --gold-text - never --flow-accent: that resolves to the
   theme --btn-primary-bg (#C4B454), which computes 2.1:1 on the white card
   and 1.8:1 on the selected gold-tint - both fail the 3:1 non-text floor.
   #75692E computes ~5.5:1 on white and ~4.8:1 on the tint; the section
   accents all compute 4.5:1+ on white and their tints. */
.flow-option-icon,
.choice-card-icon { display: inline-flex; width: 1.6rem; height: 1.6rem; color: var(--section-accent, var(--gold-text, #75692E)); flex: none; }
.flow-option-icon svg, .flow-option-icon .svg-icon, .choice-card-icon svg, .choice-card-icon .svg-icon { width: 100%; height: 100%; }
/* No CSS fill: legacy glyphs carry fill="currentColor" inline, Lucide glyphs
   carry fill="none" stroke="currentColor" - forcing fill would blob them. */
.flow-option-label,
.choice-card-label { font-size: 1rem; font-weight: 500; color: var(--flow-text); }
/* Selection = gold border (doubled via inset ring, no layout shift) + tint. */
.flow-option.is-selected,
.choice-card.is-selected {
    border-color: var(--flow-accent);
    box-shadow: inset 0 0 0 1px var(--flow-accent);
    background: var(--flow-accent-surface);
}
/* Live selection feedback: the highlighted state must track the radio/checkbox
   itself, not just the server-rendered .is-selected class - without this,
   tapping a card gave no visible response at all. */
.flow-option:has(input:checked),
.choice-card:has(input:checked) {
    border-color: var(--flow-accent);
    box-shadow: inset 0 0 0 1px var(--flow-accent);
    background: var(--flow-accent-surface);
}
.flow-option:has(input:checked) .flow-option-label,
.choice-card:has(input:checked) .choice-card-label { font-weight: 600; }
.flow-option:has(input:focus-visible),
.choice-card:has(input:focus-visible) { box-shadow: var(--flow-focus); outline: none; }
@media (hover: hover) {
    .flow-option:hover, .choice-card:hover {
        border-color: var(--flow-border-strong);
        transform: translateY(-2px);
        box-shadow: var(--flow-shadow);
    }
}

/* Toggle: RETIRED. Yes/no questions - step-level AND inline - render as the
   two-card .flow-bigchoice pattern (FLOW STEP LAYOUT rule 2: one control
   pattern per question type). The .flow-toggle switch styles were removed
   with the renderer's switch markup. */

/* Text / number / date inputs --------------------------------------------- */
.flow-input, .flow-textarea {
    width: 100%; min-height: var(--flow-target); padding: 0.7rem 0.85rem;
    font-size: 0.9375rem; color: var(--flow-text);
    background: var(--flow-surface); border: 1px solid var(--flow-border);
    border-radius: var(--flow-radius);
}
.flow-textarea { resize: vertical; line-height: 1.45; }
.flow-input:focus, .flow-textarea:focus {
    outline: none;
    border-color: var(--flow-accent);
    /* the ring supplies the focused 1.5px-gold weight without layout shift */
    box-shadow: inset 0 0 0 0.5px var(--flow-accent), var(--flow-focus);
}
/* Number field: the WRAPPER is the field (border, radius, focus ring); the
   input sits borderless inside and the unit suffix (mi, gal, USD) is a
   bordered adornment segment on the field's right - muted text on a subtle
   fill, INSIDE the field, never floating text beside it (FLOW STEP LAYOUT
   rule 3). ~200px wide, 17px tabular digits (rule 6: real presence). */
.flow-number {
    display: inline-flex; align-items: stretch;
    width: 12.5rem; /* ~200px primary numeric field */ max-width: 100%;
    background: var(--flow-surface);
    border: 1px solid var(--flow-border);
    border-radius: var(--flow-radius);
    overflow: hidden;
}
.flow-number .flow-input {
    flex: 1 1 auto; width: 100%; min-width: 0;
    border: 0; border-radius: 0;
}
.flow-number .flow-input:focus { outline: none; box-shadow: none; }
.flow-number:focus-within {
    border-color: var(--flow-accent);
    box-shadow: inset 0 0 0 0.5px var(--flow-accent), var(--flow-focus);
}
.flow-input-number { font-size: 1.0625rem; /* 17px digits */ font-variant-numeric: tabular-nums; }
/* ONE WIDTH FOR EVERY UNIT, OR THE FIELDS COME OUT RAGGED. .flow-number is a
   fixed 12.5rem, so the two controls end level - but this chip was sized by its
   own text, and "gal" is 14px wider than "$". That difference came straight off
   the white input beside it, so the gallons box and the cost box were visibly
   different widths down one column. Measured on a phone: 58px against 44px.

   min-width, not width: it is the floor that makes "$" as wide as "gal", and a
   longer unit (USD) may still grow past it rather than being clipped. 3.75rem
   clears the widest unit in any flow config today, so nothing narrows. */
.flow-unit {
    display: inline-flex; align-items: center; justify-content: center; flex: none;
    min-width: 3.75rem;
    padding: 0 0.6rem;
    border-left: 1px solid var(--flow-border);
    background: var(--flow-surface-sunken);
    color: var(--flow-text-muted);
    font-size: 0.9375rem; font-weight: 500;
    text-align: center;
}

/* The question, and the box that switches its control off, on one line.
   The box is rendered INSIDE the field it controls (renderFields), so this
   is an ordinary row - no sibling to size around, no width cap, and the
   control below is free to be narrow on a desktop and full-width on a
   phone. It wraps if a long question and the box will not fit. */
.flow-field-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-bottom: 0.625rem; /* rule 5: question -> control */
}
/* The row owns the spacing now, so the label inside it must not add its
   own or the box sits off the question's baseline. */
.flow-field-head > label { margin-bottom: 0; }
/* The box takes everything the question does not, and centres in it - so
   it sits midway between the end of the question and the end of the row
   rather than tight against the words. Narrower row, less left over, box
   closer to the question: the same rule, no second case for phones.

   Scoped to this row on purpose. .flow-check is the checkbox widget
   wherever it is used, and a standalone one belongs at the left margin,
   not adrift in the middle of a step. */
.flow-field-head > .flow-check {
    flex: 1;
    justify-content: center;
}

/* Full-width controls on a phone ---------------------------------------- */
/* Opt-in per step ("inputs": "fill"). A 12.5rem box on a 375px screen
   leaves a dead margin down one side and reads as unfinished; filling the
   width is the cleaner phone form. Desktop is untouched - the control
   keeps its natural width at the left, like every other step.

   .flow-number sets width: 12.5rem at (0,1,0), so this is written with two
   classes to beat it. The priced field's 13rem cap is (0,3,1) and needs
   (0,4,1) to clear, or the box stops half way. */
@media (max-width: 559px) {
    .flow--fill .flow-number { width: 100%; }
}
/* ALONE GETS THE ROOM, SHARING KEEPS THE PLATFORM WIDTH.
   A wizard step is one question on a ~512px column, where the 12.5rem
   default reads as undersized; a field with company has to leave room for
   it. So: the step's own control, and a sub-field that is the only one in
   its group, run wider - and two number fields in one group both fall back
   to the default rather than fighting for the width.

   The dense desktop forms (purchase orders, fuel-edit) are the real
   several-to-a-row case and are NOT touched: they render .flow-dense /
   .dense-field and emit no .flow-body, so nothing here can reach them.
   Checked rather than assumed - it is the whole reason this is safe to
   apply platform-wide. */
@media (min-width: 560px) {
    .flow-body > .flow-number,
    .flow-body .flow-fields > .flow-field:only-child .flow-number { width: 18rem; }
}


/* SMALL AND DISCREET. A quiet modifier beside a field, not a question of its
   own: no title styling, no help paragraph, and a 44px row so a thumb can
   still hit a 16px box. */
.flow-check {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 44px;
    cursor: pointer;
    font-size: 0.9375rem;
    color: var(--flow-text-soft);
    white-space: nowrap;
}
.flow-check input[type="checkbox"] {
    width: 1.125rem; height: 1.125rem;
    margin: 0; flex: none;
    accent-color: var(--flow-accent);
}

/* Switched off by the box beside it. Dimmed and inert, never removed: a field
   that vanishes reads as a bug, and the person needs to see WHY there is
   nowhere to type a price.

   THE BOX ITSELF STAYS BRIGHT. This was one rule on the whole field, and since
   the switch now lives INSIDE the field it controls (it moved onto the
   question's own line), dimming the field dimmed the switch too - so the one
   control still worth clicking looked as dead as the thing it had just turned
   off. Richard, 2026-09-24: "school still appears greyed out, even though it
   is in fact clickable... will be confusing."

   It cannot be fixed by brightening the box back up: opacity composites a
   subtree as a GROUP, so a child can never exceed its parent's opacity. The
   dimming therefore has to be applied to the siblings instead, and .flow-check
   is simply never one of them. */
.flow-field[data-disabled-by].is-off > .flow-field-head > .flow-field-label,
.flow-field[data-disabled-by].is-off > .flow-number,
.flow-field[data-disabled-by].is-off > .flow-input,
.flow-field[data-disabled-by].is-off > .flow-field-help { opacity: 0.45; }
.flow-field[data-disabled-by].is-off .flow-input { cursor: not-allowed; }

/* Info notice -------------------------------------------------------------- */
.flow-info {
    background: var(--flow-accent-surface); border: 1px solid var(--flow-border-strong);
    border-left-width: 4px; border-left-color: var(--flow-accent);
    border-radius: 8px; padding: 0.9rem 1rem; font-size: 1rem; line-height: 1.45;
    color: var(--flow-text-soft);
}

/* Receipt (review step): one card per answer + per-item Edit --------------- */
.flow-review,
.flow-receipt { margin: 0; display: grid; gap: 0.6rem; }
.flow-receipt-card {
    display: flex; align-items: center; gap: 0.75rem;
    background: var(--flow-surface);
    border: 1px solid var(--flow-border);
    border-radius: var(--flow-radius);
    padding: 0.75rem 0.9rem;
    box-shadow: 0 1px 2px rgba(23, 21, 15, 0.06);
}
.flow-receipt-main { min-width: 0; flex: 1; display: grid; gap: 2px; }
.flow-receipt-label {
    font-size: 0.75rem; /* 12px caps floor */ font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--flow-text-soft);
}
.flow-receipt-value {
    font-size: 0.9375rem; font-weight: 700; color: var(--flow-text);
    overflow-wrap: anywhere;
}
.flow-receipt-value.is-skipped { font-weight: 500; color: var(--flow-text-muted); }
/* A note left by an info step (reviewCard): a statement about what happens
   next, not an answer - so no Edit, and the value sits in the soft tone
   rather than the answer weight. */
.flow-receipt-card.is-note { background: var(--gold-tint, #F5EFD0); border-color: var(--gold-border, #D9CD8A); }
.flow-receipt-card.is-note .flow-receipt-value { font-weight: 500; color: var(--flow-text-soft); }
.flow-receipt-edit {
    flex: none; min-height: var(--flow-target); min-width: var(--flow-target);
    padding: 0 0.9rem; border-radius: var(--flow-radius);
    border: 1.5px solid #CFC9B0; background: var(--flow-surface);
    color: var(--flow-text); font-size: 0.875rem; font-weight: 600; cursor: pointer;
    transition: border-color 150ms ease, background 150ms ease;
}
.flow-receipt-edit:hover { border-color: var(--flow-border-strong); background: var(--flow-accent-surface); }
.flow-receipt-edit:focus-visible { outline: none; box-shadow: var(--flow-focus); }
/* Legacy flat review rows (kept for any external .flow-review dl usage) */
.flow-review-row {
    display: flex; justify-content: space-between; gap: 1rem;
    padding: 0.6rem 0; border-bottom: 1px solid var(--flow-border);
}
.flow-review-row dt { color: var(--flow-text-muted); font-size: 0.9rem; margin: 0; }
.flow-review-row dd { margin: 0; font-weight: 600; text-align: right; }

/* Success screen: animated gold check, reference chip, two exits ----------- */
.flow-success { text-align: center; padding: 2.25rem 0 0.5rem; }
.flow-success .flow-title { margin: 1.1rem 0 0.4rem; }
.flow-success .flow-help { margin-bottom: 0; }
.flow-success-check { width: 88px; height: 88px; margin: 0 auto; }
.flow-success-check svg { width: 100%; height: 100%; display: block; }
.flow-success-disc {
    fill: var(--flow-accent);
    transform-origin: center;
    animation: flowSuccessPop 500ms cubic-bezier(0.22, 1.2, 0.36, 1) both;
}
/* The check glyph is WHITE on the gold disc (icons on gold = white, R2).
   Pop 500ms + draw 700ms from 500ms = ~1.2s total per the motion spec. */
.flow-success-mark {
    stroke: #FFFFFF;
    stroke-width: 4.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-dasharray: 34;
    stroke-dashoffset: 34;
    animation: flowSuccessDraw 700ms ease-out 500ms forwards;
}
@keyframes flowSuccessPop { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes flowSuccessDraw { to { stroke-dashoffset: 0; } }
.flow-success-ref { margin: 1rem 0 0; }
.flow-ref-chip {
    display: inline-block;
    padding: 0.35rem 0.85rem;
    border-radius: 99px;
    background: var(--gold-tint, #F5EFD0);
    border: 1px solid var(--gold-border, #D9CD8A);
    color: var(--chip-pending-text, #75692E);
    font-size: 0.8125rem;
    font-weight: 700;
}
/* RULE 2 (footer clearance): the success exits are IN-FLOW, stacked
   full-width with a bottom margin - never a fixed bar crossing the footer.
   column-reverse puts the gold primary on top (markup is ghost-then-primary
   for the row bar). 0-2-0 specificity beats the .flow-actions fixed bar. */
.flow-wizard .flow-success-actions {
    position: static;
    flex-direction: column-reverse;
    gap: 8px;
    margin: 1.5rem 0 16px;
    padding: 0;
    border-top: 0;
    box-shadow: none;
    background: transparent; /* not the fixed bar's white strip */
}
.flow-wizard .flow-success-actions .flow-btn { width: 100%; }
/* No fixed bar on the success screen, so no bar clearance either. */
.flow-wizard:has(.flow-success) { padding-bottom: var(--flow-pad); }

/* Resume screen: same card language as the receipt ------------------------- */
.flow-resume .flow-body {
    background: var(--flow-surface);
    border: 1px solid var(--flow-border);
    border-radius: var(--flow-radius);
    padding: 1.25rem;
    box-shadow: var(--flow-shadow);
}
.flow-resume .flow-help { margin-bottom: 0; }

/* Action bar (thumb zone on mobile, inline on desktop) -------------------- */
.flow-actions {
    /* Bottom chrome model: the bar sits flush ABOVE the always-visible
       pinned footer (which carries the safe-area inset itself). */
    position: fixed; left: 0; right: 0; z-index: 60;
    bottom: calc(var(--footer-height, 40px) + env(safe-area-inset-bottom, 0px));
    display: flex; gap: 0.75rem; padding: 0.75rem var(--flow-pad);
    background: var(--flow-surface); border-top: 1px solid var(--flow-border);
    box-shadow: var(--flow-shadow-up);
}
.flow-btn {
    display: inline-flex; align-items: center; justify-content: center;
    min-height: 3rem; /* 48px phone target; 44px from 768px up */
    padding: 0 1.4rem; border-radius: var(--flow-radius);
    font-size: 1rem; font-weight: 600; cursor: pointer;
    border: 1.5px solid transparent; text-decoration: none;
    transition: background 150ms ease, border-color 150ms ease;
}
/* Held while an upload is in flight (hub-upload-sheet.js): the scrim takes
   the taps; this is for the eye and the keyboard. */
.flow-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* 600 (semibold), not 700: with Inter loaded the heavier weight reads as
   chunky/generic on the gold fill. Declared refinement of the brief's
   "primary 700+" - Inter 600 carries the same visual strength (design owner's
   live call, 2026-07-21). */
.flow-btn-next { flex: 1; background: var(--flow-accent); color: var(--flow-accent-on); font-weight: 600; }
.flow-btn-next:hover, .flow-btn-next:active { background: var(--gold-press, #A89940); }
.flow-btn-back { background: var(--flow-surface); color: var(--flow-text); border-color: #CFC9B0; }
.flow-btn-back:hover { border-color: var(--flow-border-strong); background: var(--flow-accent-surface); }
.flow-btn:focus-visible { outline: none; box-shadow: var(--flow-focus); }
@media (min-width: 768px) {
    /* Static bar: drop the fixed-bar clearance too, or ~100px of dead space
       sits between the buttons and the footer on every desktop step. */
    .flow-wizard,
    .stepper { padding-bottom: var(--flow-pad); }
    /* The solid fill (+ border/shadow) exists to occlude scrolling content
       under the FIXED bar on touch widths; on the static desktop bar it
       painted a stray white band across the #FAF9F4 page. Transparent here,
       with Back/Next as a tight right-aligned group. */
    .flow-actions {
        position: static; border-top: 0; box-shadow: none;
        background: transparent;
        padding: 1rem 0 0; justify-content: flex-end; gap: 8px;
    }
    .flow-btn { min-height: var(--flow-target); }
    .flow-btn-next { flex: 0 0 auto; min-width: 10rem; }
}

@media (prefers-reduced-motion: reduce) {
    .flow-wizard *,
    .flow-wizard *::before,
    .flow-wizard *::after {
        transition: none !important;
        /* Keyframes collapse to their end state instantly (all wizard
           animations declare forwards/both), including delayed ones. */
        animation-duration: 0.01ms !important;
        animation-delay: 0s !important;
        animation-iteration-count: 1 !important;
    }
}

/* ==========================================================================
   INLINE SUB-FIELDS - a step's secondary questions (schema "fields"), so a
   modifier (trailer on the miles step) or a review-screen extra (optional
   note, claim toggle) never costs a whole screen. Label above, control,
   hint below - the dense-form FORMS recipe, inside the wizard.
   ========================================================================== */
/* FLOW STEP LAYOUT rule 5 rhythm: ~22px between question blocks, ~10px
   question -> control, ~9px control -> hint. */
.flow-fields { display: grid; gap: 1.375rem; margin-top: 1.375rem; }
.flow-field-label {
    display: block;
    font-size: 0.9375rem; /* 15px STATEMENT label (modifier/attribute), FORMS spec */
    font-weight: 600;
    color: var(--flow-text);
    margin-bottom: 6px;
}
/* Rule 1: a question-phrased sub-field label is a PEER of the step title -
   identical size/weight/edge to .flow-title (keep the two in lockstep). */
.flow-field-label.flow-question {
    font-size: 1.5rem; /* 24px/800, = .flow-title */
    line-height: 1.3;
    font-weight: 800;
    margin-bottom: 0.625rem; /* 10px question -> control */
}
.flow-field-help { font-size: 0.8125rem; color: var(--flow-text-muted); margin: 0.5625rem 0 0; /* 9px control -> hint */ }
/* The wrapping <label> that gives a single-control sub-field its accessible
   name (question = control name, no for/id plumbing). Block, cursor default:
   these are labels-above-inputs, not tap rows. */
.flow-field-group, .dense-field-group { display: block; }
.flow-field[hidden] { display: none; }
/* On the review screen the extras sit under the receipt cards. */
.flow-receipt + .flow-fields { margin-top: 1.375rem; }

/* ==========================================================================
   BIG_CHOICE - a prominent two-up choice row (inspection "Looks good" /
   "Flag an issue"), sharing the .flow-option card widget. On roomy space the
   two cards sit side by side; they stack on the narrowest phones.
   ========================================================================== */
.flow-bigchoice { grid-template-columns: 1fr 1fr; }
.flow-bigchoice .flow-option {
    /* 48px floor (FLOW STEP LAYOUT rule 2) - padding/content set the natural
       height, identical for step-level and inline yes/no cards. */
    min-height: 3rem;
    justify-content: center;
    text-align: center;
    font-weight: 600;
}
@media (max-width: 480px) {
    .flow-bigchoice { grid-template-columns: 1fr; }
}

/* ==========================================================================
   DENSE DESKTOP FORM (FlowDenseRenderer) - the whole flow as one flat form,
   the desktop half of the adaptive engine. Brief FORMS spec: labels above,
   hints below, 12px radius, gold focus ring, plain-language errors under the
   field. Reuses the wizard's own .flow-input / .flow-textarea / .flow-options
   widgets, so the two surfaces never drift on how an input type looks.
   ========================================================================== */
.flow-dense {
    max-width: 720px;
    margin: 0 auto;
    background: var(--flow-surface, #fff);
    border: 1px solid var(--flow-border);
    border-radius: var(--flow-radius, 12px);
    padding: 1.25rem 1.25rem 1rem;
    color: var(--flow-text);
    font-family: var(--font-family, inherit);
}
.flow-dense .dense-intro { margin: 0 0 1rem; color: var(--flow-text-muted); }
.flow-dense .dense-note {
    margin: 0 0 1rem;
    padding: 0.6rem 0.75rem;
    background: var(--gold-tint, #F5EFD0);
    border: 1px solid var(--gold-border, #D9CD8A);
    border-radius: 8px;
}
.flow-dense .dense-field { margin-bottom: 1.1rem; }
.flow-dense .dense-label {
    display: block;
    font-weight: 600;
    font-size: 0.9375rem; /* 15px labels */
    margin-bottom: 6px;
    color: var(--flow-text);
}
.flow-dense .dense-required { color: var(--flow-error); }
.flow-dense .dense-optional { font-weight: 400; color: var(--flow-text-muted); }
.flow-dense .dense-help {
    margin: 0 0 6px;
    font-size: 0.8125rem; /* 13px hint */
    color: var(--flow-text-muted);
}
.flow-dense .dense-field-error {
    margin: 6px 0 0;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--flow-error);
}
.flow-dense .dense-field.has-error .flow-input,
.flow-dense .dense-field.has-error .flow-textarea { border-color: var(--flow-error); }
.flow-dense .dense-actions { margin-top: 1.25rem; }
.flow-dense .dense-actions .flow-btn-next { min-width: 12rem; }
@media (max-width: 640px) {
    .flow-dense .dense-actions .flow-btn-next { width: 100%; }
}

/* ==========================================================================
   COMPACT DENSE FORM (FlowDenseRenderer compact mode, 2026-09-14): the
   brief's FORM PAGE scaffold (.form-grid/.form-field/.form-input, shared/
   enterprise-components.css) driven by the same flow JSON. First consumer:
   the fuel trip edit page. Only what the scaffold does not already say:
   the flow tokens for colour, the 44px floor at touch widths, the segmented
   control and the switch row.
   ========================================================================== */
.flow-compact { row-gap: 8px; } /* 375px budget lever: 10px measured 1312px, the target is <= 1300 */
.flow-compact .form-field__label { font-size: 0.9375rem; line-height: 1.3; color: var(--flow-text); }
.flow-compact .form-input { font-size: 0.9375rem; line-height: 1.3; }
/* One control height per row. 42px = .form-input's own box (10px padding +
   15px/1.3 + 1px borders) pinned explicitly, because a native <select> sizes
   its box from the font's metrics (43px measured) and would sit 1px proud of
   the input beside it in a 2-up row; the pills and the switch row take the
   same number. The touch floor below out-ranks it (min-height beats height). */
.flow-compact input.form-input,
.flow-compact select.form-input { height: 2.625rem; }
.flow-compact .form-seg__opt { min-height: 2.625rem; }
.flow-compact textarea.form-input { min-height: 5rem; resize: vertical; }
@media (max-width: 1024px), (pointer: coarse) {
    .flow-compact .form-input,
    .flow-compact .form-seg__opt,
    .flow-compact .form-field__switch { min-height: var(--tap-target, 2.75rem); }
}
/* 2-up rows: a switch cell has no label line, so it seats at the row's foot,
   level with the input beside it (Trailer beside Miles) instead of floating
   up beside that input's label. 1024px = the grid's own 2-up breakpoint. */
@media (min-width: 1024px) {
    .flow-compact .form-field:has(> .form-field__switch) { align-self: end; }
}
.flow-compact__heading {
    grid-column: 1 / -1;
    margin: var(--space-2, 8px) 0 0;
    padding-bottom: 4px;
    border-bottom: 1px solid var(--flow-border);
    font-size: 1rem;
    font-weight: 700;
    color: var(--flow-text);
}
.flow-compact__heading:first-of-type { margin-top: 0; }
/* .form-field { display: block } beats the UA's [hidden] rule. */
.flow-compact .form-field[hidden],
.flow-compact__heading[hidden] { display: none; }
.flow-compact .form-field__req { color: var(--flow-error); }
.flow-compact .form-field__opt { font-weight: 400; color: var(--flow-text-muted); }
/* The receipt's own label (UploadSheet) reads like the labels around it. */
.flow-compact .hub-upload__label { font-size: 0.9375rem; line-height: 1.3; color: var(--flow-text); }
.flow-compact .form-field__error {
    margin: 6px 0 0;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--flow-error);
}
.flow-compact .form-field.has-error .form-input,
.flow-compact .form-field.has-error .form-seg__opt { border-color: var(--flow-error); }
/* Segmented control: a 2-3 option single_select as one row of pills. The
   border is drawn on the pills, not the group, so the visible pill IS the
   control's full height: 44px on touch, and on desktop the same padding and
   line-height as .form-input (10px + 15px/1.3 + 1px borders) so a pill and
   the input beside it in a 2-up row compute the same 42px. */
.form-seg {
    display: flex;
    border-radius: var(--flow-radius);
    background: var(--flow-surface);
    overflow: hidden;
}
.form-seg__opt {
    flex: 1 1 0;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 10px 12px;
    border: 1px solid var(--flow-border);
    border-left-width: 0;
    font-size: 0.9375rem;
    line-height: 1.3;
    font-weight: 600;
    color: var(--flow-text);
    cursor: pointer;
}
.form-seg__opt:first-child { border-left-width: 1px; border-radius: var(--flow-radius) 0 0 var(--flow-radius); }
.form-seg__opt:last-child { border-radius: 0 var(--flow-radius) var(--flow-radius) 0; }
.form-seg__opt input { position: absolute; opacity: 0; width: 1px; height: 1px; } /* the documented 1x1 exception */
.form-seg__opt:has(input:checked) { background: var(--flow-accent-surface); box-shadow: inset 0 0 0 1.5px var(--flow-accent); }
.form-seg__opt:has(input:focus-visible) { box-shadow: inset 0 0 0 2px var(--gold-text, #75692E); }
/* The switch row: .hub-toggle (the one switch primitive) with its label beside it. */
.form-field__switch {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 10px;
    width: 100%;
    min-height: 2.625rem; /* 42px: .form-input's desktop height, so the row pairs level */
    font-size: 0.9375rem;
    font-weight: 600;
    color: var(--flow-text);
}
.flow-compact__actions { margin-top: var(--space-2, 8px); }

/* Device-adaptive flow entry: the phone CTA jumps to the stepper; desktop
   shows the same JSON as the dense form. Exactly one is visible per width. */
.flow-adaptive-cta { display: none; }
.flow-adaptive-dense { display: block; }
@media (max-width: 640px) {
    .flow-adaptive-cta {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        text-decoration: none;
        width: 100%;
        /* Full column width - the old 480px cap re-centered the CTA at
           513-640px viewports, breaking the one-left-edge law. */
        margin: 0 0 16px;
    }
    .flow-adaptive-dense { display: none; }
}

/* COMPACT OPTION GRID - "optionsLayout": "compact" on a step. Richard,
   2026-08-27, on the nine stacked period cards: "Thats a lot of vertical
   scrolling that I feel like we can collapse into even a 3 column (like a
   phone) appearance with buttons." Three columns like a phone keypad; an
   option carrying "wide": true (the exclusive "All day") spans the full row
   and keeps its icon, while the keypad cells drop theirs - eight copies of
   the same hash glyph is noise, not information. Cell height stays on the
   list's 3rem floor, comfortably above the 44px touch rule the density gate
   enforces on real touch media via the .flow-option base sizing. */
.flow-options--compact {
    grid-template-columns: repeat(3, minmax(0, 1fr));
}
.flow-options--compact .flow-option {
    min-height: 3rem;
    justify-content: center;
    text-align: center;
    padding: 0.6rem 0.5rem;
}
.flow-options--compact .flow-option .flow-option-icon {
    display: none;
}
.flow-options--compact .flow-option--wide {
    grid-column: 1 / -1;
}
.flow-options--compact .flow-option--wide .flow-option-icon {
    display: inline-flex;
}

/* TWO-UP OPTION GRID - "optionsLayout": "pairs". Richard, 2026-08-27: "a 2
   column system there on what type of request... along with the type of sub
   request... Helps compact it for phones too."

   Unlike the compact keypad above, this KEEPS the icons: every option here has
   its own meaningful glyph, and those glyphs are what make a 2-up grid
   scannable instead of a wall of words. Cards keep the 4rem floor and stretch
   to match their row, so a wrapped two-line label never leaves a short
   neighbour. Tighter padding and a slightly smaller label on narrow phones
   buy back the width the second column costs. */
.flow-options--pairs {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}
.flow-options--pairs .flow-option {
    padding: 0.75rem 0.8rem;
    gap: 0.6rem;
}
.flow-options--pairs .flow-option-label {
    overflow-wrap: break-word;
}
/* An odd count would leave an orphan gap; the catch-all spans the last row
   instead, which also puts its follow-up question directly beneath it. */
.flow-options--pairs .flow-option--wide {
    grid-column: 1 / -1;
}
@media (max-width: 420px) {
    .flow-options--pairs .flow-option-label { font-size: 0.9375rem; }
}

/* ========== fuel-tracker.css ========== */

/* My Fuel Data: year-to-date gallon balance + trip history. Scoped, mobile-first.
 *
 * THE BALANCE IS AN EQUATION - carried in + earned - received = remaining -
 * and an equation must never wrap. It used to be a flex row with
 * `flex-wrap:wrap` and `flex:1 1 150px` tiles (set INLINE in fuel.php, which
 * is why the grid below never applied). On a phone two tiles fit per line, so
 * the row broke mid-sum and stranded the minus sign at the right edge of line
 * one with nothing after it: "165 + 28 -" / "0 = 193" (owner, 2026-09-01).
 *
 * So the two widths get two SHAPES rather than one shape that reflows:
 *   phone  -> a ledger. One row per figure, label left, signed number right,
 *             the total ruled off underneath. No operator glyph can dangle
 *             because there are none: the sign rides on the number.
 *   >=641  -> the equation, on ONE line (flex-wrap:nowrap), which is what the
 *             operator glyphs are for.
 */
.ft-balance {
    display: grid;
    grid-template-columns: 1fr;
    /* Full column width - the 640px auto-centering broke the one-left-edge
       law once the header went left-flush (2026-07-31 landing rebuild). */
    margin: 0 0 20px;
    background: #faf9f8;
    border: 1px solid #edebe9;
    border-radius: 12px;
    overflow: hidden;
}
/* Phone: a ledger row. Label reads first, figure sits right, tabular so the
   digits line up down the column. */
.ft-stat {
    display: flex;
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    padding: 11px 14px;
    border-bottom: 1px solid #edebe9;
}
/* The operators are the wide-screen device; on the ledger the signs are part
   of the numbers (below), so these are not rendered at all. */
.ft-op { display: none; }
.ft-stat--earned .ft-num::before { content: "+"; }
.ft-stat--received .ft-num::before { content: "\2212"; }
/* The answer: ruled off from the workings, never a hairline of its own. */
.ft-stat--total {
    border-bottom: 0;
    border-top: 2px solid #edebe9;
}
/* District gold accent on the surface/border; the number itself stays
   near-black - gold text on a pale surface fails WCAG contrast. */
.ft-stat-remaining { background: var(--gold-tint, #F5EFD0); border-color: var(--btn-primary-bg, #C4B454); }
.ft-stat-flagged { background: #fdf3f4; border-color: #a4262c; }
.ft-num { font-size: 1.25rem; font-weight: 700; line-height: 1.1; color: #201f1e; font-variant-numeric: tabular-nums; }
.ft-stat--total .ft-num { font-size: 1.5rem; }
.ft-stat-remaining .ft-num { color: #201f1e; }
.ft-stat-flagged .ft-num { color: #a4262c; }
.ft-flag {
    margin: 0 0 16px;
    padding: 12px 16px;
    background: #fdf3f4;
    border: 1px solid #f3d6d8;
    border-left: 4px solid #a4262c;
    border-radius: 8px;
    color: #a4262c;
    font-weight: 600;
}
/* The amber tier: over the warning, not over the limit. Same shape, the
   platform's warning chip tones. */
.ft-flag--warn {
    background: var(--chip-warning-bg, #F8EFDE);
    border-color: var(--chip-warning-border, #EAD6AE);
    border-left-color: var(--warning, #D97706);
    color: var(--chip-warning-text, #8A5A17);
}
/* Ledger: the label leads the row, so it comes first in the visual order even
   though the number is first in the DOM (the number is the datum). */
.ft-label {
    order: -1;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: #605e5c;
    text-align: left;
}

/* Trip history now renders as the shared .section-panel/.section-row (landing
   rebuild 2026-07-31); the old .ft-actions/.ft-history block markup is gone. */
.ft-row-gal { margin-left: auto; font-weight: 700; color: #201f1e; white-space: nowrap; }
.ft-claimed { color: #a4262c; font-weight: 600; margin-left: 6px; }

/* >=641px: back to the equation, on one line. flex-wrap:nowrap is the whole
   point - it is what guarantees an operator can never fall at a line break
   again, at any width in this range. */
@media (min-width: 641px) {
    .ft-balance {
        display: flex;
        flex-wrap: nowrap;
        align-items: stretch;
        gap: 8px;
        background: transparent;
        border: 0;
        border-radius: 0;
        overflow: visible;
    }
    .ft-op {
        display: flex;
        align-items: center;
        flex: none;
        padding: 0 2px;
        font-size: 20px;
        color: #605e5c;
    }
    .ft-stat {
        flex: 1 1 0;
        min-width: 0;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 18px 8px;
        background: #faf9f8;
        border: 1px solid #edebe9;
        border-radius: 12px;
    }
    .ft-stat--total { border-top-width: 1px; }
    /* The tiles carry their own sign in the operator glyphs between them. */
    .ft-stat--earned .ft-num::before,
    .ft-stat--received .ft-num::before { content: none; }
    .ft-num { font-size: 1.9rem; line-height: 1; }
    .ft-stat--total .ft-num { font-size: 1.9rem; }
    /* Half the previous 6px. The number and its label are one unit; at 1.9rem
       with line-height 1 the digits already carry their own descender slack,
       so 6px on top of that read as a gap between two things rather than as
       one labelled figure. */
    .ft-label { order: 0; margin-top: 3px; text-align: center; }

    /* RE-ASSERT THE STATE COLOURS. `.ft-stat` above sets background and a
       border SHORTHAND, and scores the same (0,1,0) as .ft-stat-remaining /
       .ft-stat-flagged in the base block - but comes later in source order, so
       from 641px up it silently won and the gold answer tile rendered grey.
       The shorthand also reset the gold border-color. Same specificity, later
       position: no !important required. The number stays near-black on the
       gold tint on purpose - gold text on a pale surface fails WCAG contrast,
       which is why only the surface and border carry the accent. */
    .ft-stat-remaining {
        background: var(--gold-tint, #F5EFD0);
        border-color: var(--btn-primary-bg, #C4B454);
    }
    .ft-stat-flagged {
        background: #fdf3f4;
        border-color: #a4262c;
    }
}

/* ========== shared/drawer.css ========== */

/* ============================================================================
   HUB DRAWER - the ONE content drawer primitive (right-side by default;
   .hub-drawer--sheet anchors it bottom) (no-drift rule)
   ============================================================================
   Class contract (markup shell):

     <div class="hub-drawer" id="...">                    fixed overlay root
         <div class="hub-drawer-backdrop"></div>          click-to-close scrim
         <div class="hub-drawer-panel" role="dialog"      the sliding sheet
              aria-modal="true" aria-labelledby="...">
             <div class="hub-drawer-header">
                 <div class="hub-drawer-title-area">h2 + .hub-drawer-subtitle</div>
                 <button class="hub-drawer-close" aria-label="Close">icon</button>
             </div>
             <div class="hub-drawer-tabs">                optional tab strip
                 <button class="hub-drawer-tab active" data-tab="x">
                     label <span class="tab-badge">0</span>
                 </button>
             </div>
             <div class="hub-drawer-body active" data-tab="x">
                 <div class="hub-drawer-search">icon + input</div>   (optional)
                 ...scrolling content...
             </div>
         </div>
     </div>

   Open state = .hub-drawer.open, toggled ONLY by public/assets/js/hub-drawer.js
   (HubDrawer.create), which owns the behavior contract: backdrop click,
   Escape with focus return to the trigger, Tab focus trap, body scroll lock,
   role=dialog/aria-modal, aria-expanded on the trigger.

   The left-side NAV drawer (.admin-sidebar <=1024px in shared/_responsive.css
   + EnterpriseHeader's toggle script) is a separate, position-left primitive;
   this one is for right-side CONTENT sheets. Do not fork either.

   Consumers: admin/users-groups (group detail drawer); reimbursements/receipt +
   receipts (upload sheet via Hub\Components\UploadSheet). Add new ones here.
   ============================================================================ */

.hub-drawer {
    position: fixed;
    inset: 0;
    /* Same layer as the nav drawer: above the sticky admin header (20) and
       the user dropdown (10002); backdrop scrim sits inside this root. */
    z-index: 10010;
    display: none;
}

.hub-drawer.open {
    display: flex;
}

.hub-drawer-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.45); /* same scrim as .admin-drawer-backdrop */
    animation: hub-drawer-fade 200ms ease;
}

@keyframes hub-drawer-fade {
    from { opacity: 0; }
    to { opacity: 1; }
}

.hub-drawer-panel {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 560px;
    max-width: 90vw;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: var(--surface, #FFFFFF);
    border-left: 1px solid var(--line, #E6E2D2);
    border-radius: 12px 0 0 12px;
    /* The one shadow recipe (DESIGN-BRIEF: SHAPE & DEPTH) */
    box-shadow: 0 1px 2px rgba(23, 21, 15, 0.06), 0 8px 24px rgba(23, 21, 15, 0.08);
    animation: hub-drawer-slide 250ms ease;
}

@keyframes hub-drawer-slide {
    from { transform: translateX(100%); }
    to { transform: translateX(0); }
}

@media (prefers-reduced-motion: reduce) {
    .hub-drawer-backdrop,
    .hub-drawer-panel,
    .hub-drawer--sheet .hub-drawer-panel {   /* (0,2,0): named here or the sheet keeps its rise */
        animation: none;
    }
}

/* ---------- Header ---------- */

.hub-drawer-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 1.25rem 1.5rem;
    border-bottom: 1px solid var(--line, #E6E2D2);
    flex-shrink: 0;
}

.hub-drawer-title-area {
    min-width: 0;
}

.hub-drawer-header h2 {
    margin: 0;
    font-size: 1.125rem;
    font-weight: 600;
    color: var(--ink, #191712);
}

.hub-drawer-subtitle {
    display: block;
    margin-top: 0.125rem;
    font-size: 0.8125rem;
    color: var(--ink-mute, #6F6A5C); /* tertiary text, >=13px per token rule */
}

.hub-drawer-close:not(.hub-btn) {
    /* 36px desktop; the touch block at file end lifts to --tap-target -
       44px is a touch requirement, not a desktop one (brief section 2) */
    width: 36px;
    height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    margin: -0.375rem -0.5rem 0 0;
    border: none;
    background: none;
    border-radius: 8px;
    color: var(--ink-soft, #4A4638);
    cursor: pointer;
    transition: background 150ms ease, color 150ms ease;
}

.hub-drawer-close:not(.hub-btn):hover {
    background: var(--gray-100, #F3F4F6);
    color: var(--ink, #191712);
}

.hub-drawer-close:focus-visible,
.hub-drawer-tab:focus-visible {
    outline: 2px solid var(--border-focus, #C4B454); /* gold focus ring */
    outline-offset: 2px;
}

/* ---------- Tabs (optional) ---------- */

.hub-drawer-tabs {
    display: flex;
    padding: 0 1.5rem;
    border-bottom: 1px solid var(--line, #E6E2D2);
    flex-shrink: 0;
}

.hub-drawer-tab {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    min-height: 36px; /* 44px at touch widths (block at file end) */
    padding: 0.625rem 1rem;
    border: none;
    background: none;
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--ink-soft, #4A4638);
    cursor: pointer;
    border-bottom: 2px solid transparent;
    transition: color 150ms ease, border-color 150ms ease;
    white-space: nowrap;
}

.hub-drawer-tab:hover {
    color: var(--ink, #191712);
}

.hub-drawer-tab.active {
    color: var(--gold-text, #75692E); /* gold-as-text (DESIGN-BRIEF R3) */
    border-bottom-color: var(--gold, #C4B454);
}

.hub-drawer-tab .tab-badge {
    font-size: 0.6875rem;
    font-weight: 600;
    padding: 0.05rem 0.375rem;
    border-radius: 8px;
    background: var(--gray-200, #E5E7EB);
    color: var(--ink-soft, #4A4638);
}

.hub-drawer-tab.active .tab-badge {
    background: var(--gold-tint, #F5EFD0);
    color: var(--gold-text, #75692E);
}

/* ---------- Body (one per tab; .active shows it) ---------- */

.hub-drawer-body {
    display: none;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 1rem 1.5rem;
}

.hub-drawer-body.active {
    display: flex;
    flex-direction: column;
}

/* ---------- In-drawer search (optional) ---------- */

.hub-drawer-search {
    position: relative;
    margin-bottom: 0.75rem;
    flex-shrink: 0;
}

.hub-drawer-search i,
.hub-drawer-search .svg-icon {
    position: absolute;
    left: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    color: var(--gray-400, #9CA3AF);
    font-size: 0.875rem;
    pointer-events: none;
}

.hub-drawer-search input {
    width: 100%;
    padding: 0.5rem 0.75rem 0.5rem 2.25rem;
    border: 1.5px solid var(--line, #E6E2D2);
    border-radius: 12px;
    font-size: 0.8125rem;
    background: var(--gray-50, #F9FAFB);
    color: var(--ink, #191712);
    transition: border-color 150ms ease, box-shadow 150ms ease;
}

.hub-drawer-search input:focus {
    outline: none;
    border-color: var(--gold, #C4B454);
    background: var(--surface, #FFFFFF);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold, #C4B454) 20%, transparent);
}

/* ---------- Touch widths: 44px tap targets ---------- */

@media (max-width: 1024px) {
    .hub-drawer-close:not(.hub-btn) {
        width: var(--tap-target, 2.75rem);
        height: var(--tap-target, 2.75rem);
    }
    .hub-drawer-tab {
        min-height: var(--tap-target, 2.75rem);
    }
}

/* ---------- Phone: full-width sheet ---------- */

@media (max-width: 768px) {
    .hub-drawer-panel {
        width: 100%;
        max-width: 100%;
        border-radius: 0;
        border-left: none;
    }
}

/* ---------- Bottom sheet: .hub-drawer--sheet (no-drift: one drawer, one more anchor) ----------
   The owner's reference (2026-09-07) is the Claude app's "Add context" sheet:
   anchored bottom, three equal tiles in a row, X top-left, title centred.
   Everything below is scoped under the modifier; the right-side default and
   its console consumers are untouched. HubDrawer supplies the behavior.
   DROPPED vs the reference, on purpose: the drag handle (~20px; no drag-to-
   dismiss exists - backdrop, X and Escape close), the header divider (1px),
   any subtitle, and any in-sheet hint (the field's own 13px hint already says
   "photo or PDF, up to 15 MB"; paying it twice costs 31px).
   Measured on the hub bundle at 320/375/390/414: panel 121px + safe-area,
   header 44, body 76, tiles 60 tall / 93-125 wide, close 44x44. */
.hub-drawer--sheet .hub-drawer-panel {
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    max-width: 560px;           /* coarse-pointer tablets: the right drawer's own width, centred */
    margin: 0 auto;
    max-height: 85vh;
    max-height: 85dvh;          /* a taller future sheet scrolls inside .hub-drawer-body, never off-screen */
    border-left: 0;
    border-top: 1px solid var(--line, #E6E2D2);
    border-radius: 12px 12px 0 0;   /* (0,2,0) beats the 768px block's border-radius:0 */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    animation: hub-sheet-rise 250ms ease;               /* the brief's step-transition figure */
}

@keyframes hub-sheet-rise {
    from { transform: translateY(100%); }
    to { transform: translateY(0); }
}

/* Header = the 44px close row; the title rides it, so it costs 0px
   (the default drawer header would be 79px here). */
.hub-drawer--sheet .hub-drawer-header {
    display: grid;
    grid-template-columns: 44px 1fr 44px;   /* X left, title centred, empty right keeps it optically centred */
    align-items: center;
    gap: 0;
    min-height: 44px;
    padding: 0 4px;
    border-bottom: 0;
}

/* 44px UNCONDITIONALLY, not via the 1024px touch block above: a coarse-pointer
   1366px iPad Pro opens this sheet too, and the brief scopes 44px to
   "<=1024px OR pointer:coarse". */
.hub-drawer--sheet .hub-drawer-close:not(.hub-btn) {
    grid-column: 1;
    width: 44px;
    height: 44px;
    margin: 0;
}

.hub-drawer--sheet .hub-drawer-close .svg-icon svg {
    width: 20px;
    height: 20px;
}

/* Inset ring in --gold-text (5.5:1 on white; the menu-item recipe). Inset
   because the panel is overflow:hidden and this button sits on its top edge -
   the base 2px-offset ring would lose its top 2px. */
.hub-drawer--sheet .hub-drawer-close:focus-visible {
    outline: 2px solid var(--gold-text, #75692E);
    outline-offset: -2px;
}

.hub-drawer--sheet .hub-drawer-title-area {
    grid-column: 2;
    min-width: 0;
    text-align: center;
}

.hub-drawer--sheet .hub-drawer-header h2 {
    margin: 0;
    font-size: 15px;            /* card-title scale (brief: titles 15/700); the base drawer's 18/600 is legacy */
    font-weight: 700;
    line-height: 1.2;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 4px top is enough because the tile focus ring is INSET: nothing reaches the
   scroll container's padding edge, so nothing is clipped. */
.hub-drawer--sheet .hub-drawer-body {
    padding: 4px 12px 12px;     /* 12px sides = the phone card gutter */
}

/* ---------- Upload sheet tiles: Camera / Photos / Files ----------
   Not .hub-btn: an icon-over-label block at 60px is off the 28/36/48 scale by
   design (the hub-switch reasoning); listed in check-button-primitive.py
   EXEMPT_CLASSES. The exemption never waives accessibility: 60px tall, >=93px
   wide at 320, inset 2px --gold-text focus ring (5.27:1 on --gray-50, 4.75:1
   on --gold-tint), measured by check-glyph-contrast.py. Markup + behavior:
   Hub\Components\UploadSheet + public/assets/js/hub-upload-sheet.js. */
.hub-sheet-tiles {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));   /* a CHILD of the flex body: .active's flex-direction cannot stack it */
    gap: 8px;                                            /* chip-scale spacing */
}

.hub-sheet-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    min-height: 60px;           /* 1 + 7 + 24 icon + 4 + 16 label + 7 + 1 */
    padding: 7px 4px;
    box-sizing: border-box;
    border: 1px solid var(--line, #E6E2D2);
    border-radius: 12px;
    background: var(--gray-50, #F9FAFB);   /* reads as a tile on the white sheet without a heavier border */
    color: var(--ink, #191712);
    font: inherit;
    font-size: 13px;            /* the .hub-btn label scale */
    font-weight: 600;
    line-height: 16px;
    white-space: nowrap;
    cursor: pointer;
    transition: background 150ms ease, border-color 150ms ease;
}

.hub-sheet-tile .svg-icon {
    display: block;
    width: 24px;
    height: 24px;
    line-height: 0;
}

.hub-sheet-tile .svg-icon svg {
    display: block;
    width: 24px;                /* the Lucide grid; --ink on --gray-50 computes 17:1 */
    height: 24px;
}

.hub-sheet-tile:hover,
.hub-sheet-tile:active {
    background: var(--gold-tint, #F5EFD0);
    border-color: var(--gold-border, #D9CD8A);
}

.hub-sheet-tile:focus-visible {
    outline: 2px solid var(--gold-text, #75692E);
    outline-offset: -2px;
    box-shadow: none;
}

/* ========== shared/table-tools.css ========== */

/*
 * TABLE TOOLS - the shared sort/filter chrome that hub-table.js injects
 * above every register table (DESIGN-BRIEF REGISTERS law: every table is
 * sortable and filterable; status tabs are the page's server-side job).
 *
 * Sort carets are CHROME, not section identity (2026-07-28 caret ruling):
 * gray at rest, ink when active. Drawn with border triangles - no glyph
 * fonts, no unicode arrows - so the icon gate stays meaningful.
 */

.table-tools {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    margin: 0 0 8px;
    min-width: 0;
    flex-wrap: wrap;
}

.table-tools-filter {
    width: 220px;
    max-width: 100%;
    min-width: 0;
    padding: 6px 12px;
    border: 1px solid var(--gray-400, #c8c6c4);
    border-radius: 99px;
    font-size: 13px;
    font-family: inherit;
    background: var(--surface, #fff);
    color: var(--ink, #191712);
}
.table-tools-filter:focus {
    outline: none;
    border-color: var(--gold, #C4B454);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold, #C4B454) 45%, transparent);
}

.table-tools-empty {
    font-size: 12px;
    color: var(--ink-soft, #4A4638);
}

/* Card-mode sort select: headers are visually hidden at <=640px (the
   .table-cards recipe), so sorting moves into the toolbar there. Desktop
   sorts by clicking headers - the select would be a second control for the
   same job, so it stays hidden above the card breakpoint. */
.table-tools-sort {
    display: none;
    min-height: 2.75rem; /* 44px touch target - only ever visible on touch widths */
    padding: 6px 28px 6px 12px;
    border: 1px solid var(--gray-400, #c8c6c4);
    border-radius: 8px;
    font-size: 13px;
    font-family: inherit;
    background: var(--surface, #fff);
    color: var(--ink, #191712);
}
@media (max-width: 640px) {
    .table-tools-sort { display: inline-block; }
    .table-tools { justify-content: stretch; }
    .table-tools-filter { flex: 1 1 140px; min-height: 2.75rem; }
}

/* Sortable headers: pointer + reserved caret slot. The caret is a border
   triangle (up = asc) in chrome colors - gray-400 at rest (unsorted headers
   show it only on hover), ink when that column drives the sort. */
th[data-sortable] {
    cursor: pointer;
    user-select: none;
    position: relative;
    padding-right: 18px;
}
th[data-sortable]::after {
    content: "";
    position: absolute;
    right: 6px;
    top: 50%;
    margin-top: -2px;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-bottom: 5px solid var(--gray-400, #c8c6c4);
    opacity: 0;
}
th[data-sortable]:hover::after,
th[data-sortable]:focus-visible::after { opacity: 1; }
th[data-sortable].is-sorted-asc::after {
    opacity: 1;
    border-bottom-color: var(--ink, #191712);
}
th[data-sortable].is-sorted-desc::after {
    opacity: 1;
    border-bottom: 0;
    border-top: 5px solid var(--ink, #191712);
    margin-top: -3px;
}
th[data-sortable].is-sorted-asc,
th[data-sortable].is-sorted-desc { color: var(--ink, #191712); }
th[data-sortable]:focus-visible {
    outline: none;
    box-shadow: inset 0 0 0 2px var(--gold-border, #D9CD8A);
}

/* ========== shared/calendar.css ========== */

/* calendar.css - the Substitute Calendar mirror (.hub-cal).
 *
 * ONE stage, four views, all rendered on the SERVER by
 * src/Sections/SubstituteRequests/CalendarRenderer.php. This file only ever
 * decides which of them is visible and what it looks like; hub-calendar.js
 * swaps innerHTML into .hub-cal__stage and never builds a grid.
 *
 * THE PHONE DEFAULT IS A CSS DECISION, NOT A JS ONE. With no ?view= in the URL
 * the server renders BOTH the month grid and the agenda list and marks the root
 * data-view="auto"; the rules under "AUTO" below show the agenda on a phone and
 * the month on a desktop. No matchMedia, no flash, no redirect, no viewport
 * sniffing - and it is right before a single byte of JavaScript runs.
 *
 * A 7-column month grid at 375px gives 53px columns. That is fine for a day
 * NUMBER and a row of dots, and hopeless for a title, which is exactly why the
 * agenda exists rather than an inner horizontal scroller.
 *
 * Tokens only - no hex literals anywhere (scripts/color-allowlist.py).
 * Generated into the three bundles by build-css-bundles.sh; never hand-edit
 * public/assets/css/*-bundle*.css.
 */

.hub-cal {
    --cal-day-h: 56px;          /* 44px touch floor + room for the dot row */
    --cal-dow-h: 24px;
    --cal-gap: 2px;
    /* MEASURED, not guessed: the month grid's top edge sits 233px below the
       viewport top once the page header, the h1, the sticky bar and the legend
       are drawn, and the page wants a few px of air under the last week. The
       desktop rule hands the grid whatever is left of the window so a month
       FITS instead of running past the fold. Wrong either way it degrades
       gently - too small and the rows floor at --cal-day-h and the page scrolls
       a little, too large and the rows simply grow, still uniform. */
    --cal-chrome: 240px;
    display: block;
}

/* ---------- TOOLBAR ------------------------------------------------------ */
/* z-index 5 inside .admin-main (which is its own stacking context at 1), so
   this competes only with the stage beneath it - see scripts/check-stacking.py
   for why a number here is only ever local. */
.hub-cal__bar {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) 0;
    background: var(--surface, #FFFFFF);
    border-bottom: 1px solid var(--border-primary);
}

.hub-cal__nav {
    display: flex;
    align-items: center;
    gap: var(--space-1);
}

.hub-cal__title {
    margin: 0;
    font-size: var(--text-xl);
    font-weight: 700;
    color: var(--ink);
    flex: 1 1 auto;
    text-align: center;
    min-width: 0;
}

/* Desktop order: arrows, title, pills, refresh, New Entry. The phone reorders
   the same five children rather than reflowing different markup. */
.hub-cal__nav     { order: 1; }
.hub-cal__title   { order: 2; }
.hub-cal__views   { order: 3; }
.hub-cal__refresh { order: 4; }
.hub-cal__new     { order: 5; }

/* The segmented control is the platform's .filter-pill row, not a new
   primitive: it is already the shape this UI wants and already exempt from the
   button gate. It scrolls INSIDE itself if it must; the page never scrolls. */
.hub-cal__views {
    display: flex;
    gap: var(--space-1);
    overflow-x: auto;
    max-width: 100%;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.hub-cal__views::-webkit-scrollbar { display: none; }

.hub-cal__legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    padding: var(--space-2) 0;
    font-size: var(--text-xs);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ink-soft);
}
.hub-cal__legend-item {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}

/* ---------- NOTICES ------------------------------------------------------ */
.hub-cal__notice {
    padding: 10px 14px;
    border-radius: var(--radius-control);
    margin: 0 0 var(--space-3);
    font-size: var(--text-base);
}
.hub-cal__notice--warn {
    background: var(--gold-tint);
    border: 1px solid var(--gold-border);
    color: var(--gold-text);
}

.hub-cal__fail-head {
    margin: var(--space-5) 0 var(--space-2);
    font-size: var(--text-lg);
    color: var(--ink);
}
.hub-cal__fail-retry { margin: 0 0 var(--space-4); }

.hub-cal__empty {
    margin: var(--space-4) 0;
    color: var(--ink-soft);
    font-size: var(--text-base);
}

/* ---------- MONTH -------------------------------------------------------- */
.hub-cal__month {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--cal-gap);
    /* EVERY WEEK THE SAME HEIGHT - what Google does, and what this grid did
       not. Row 1 is the day-of-week strip; every week after it is an implicit
       row. Where the grid's own height is auto (tablet) `1fr` rows all resolve
       to the tallest week; where it is definite (desktop, below) they split it
       evenly. Measured before this: 24px 142px 142px 138px 112px 112px 112px,
       a month fat at the top and thin at the bottom. */
    grid-template-rows: var(--cal-dow-h);
    grid-auto-rows: minmax(var(--cal-day-h), 1fr);
}

.hub-cal__dow,
.hub-cal__week {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: var(--cal-gap);
}

.hub-cal__dow {
    height: var(--cal-dow-h);
    align-items: center;
}
.hub-cal__dow > span {
    text-align: center;
    font-size: var(--text-xs);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

/* THE CELL IS A FLEX BOX SO THE DAY BOX FILLS IT. The grid already stretched
   this cell to the week's height, but .hub-cal__day is a block child carrying
   only a min-height, so it stopped at its own content: measured 112, 112, 112,
   116, 142, 112, 112 inside a 142px row. That is the stepped bottom border
   running along every week. Two lines give the row one baseline. */
.hub-cal__cell { min-width: 0; display: flex; }
.hub-cal__cell > .hub-cal__day { flex: 1 1 auto; }

.hub-cal__day {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
    min-height: var(--cal-day-h);
    padding: var(--space-1);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-control);
    background: var(--surface, #FFFFFF);
    color: var(--ink);
    text-decoration: none;
    overflow: hidden;
}
.hub-cal__day:hover { border-color: var(--gold-border); }
.hub-cal__day:focus-visible {
    outline: 2px solid var(--gold-text);
    outline-offset: 2px;
}

.hub-cal__daynum {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    height: 24px;
    border-radius: var(--radius-full);
    font-size: var(--text-base);
    font-weight: 600;
    line-height: 1;
}

/* DESIGN BRIEF R1: text on gold is BLACK. */
.hub-cal__day--today .hub-cal__daynum {
    background: var(--gold);
    color: var(--nd-black, #000000);
    font-weight: 700;
}

.hub-cal__day--outside {
    background: var(--bg-secondary);
}
/* A day holding a request that is NOT on the calendar. Dashed, because the
   cell is describing something provisional rather than something present -
   and it has to carry at phone widths, where the chips are hidden and the
   dots are all a reader has. */
.hub-cal__day--orphan,
.hub-cal__wcol--orphan {
    border-style: dashed;
    border-color: var(--danger);
}
.hub-cal__day--outside .hub-cal__daynum { color: var(--ink-mute); }

.hub-cal__dots {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 3px;
    min-height: 8px;
}

.hub-cal__dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: var(--radius-full);
    flex: 0 0 auto;
}
/* --gold-text (#75692E), NOT --gold (#C4B454): a 6px gold dot on white
   computes ~2.1:1 and fails WCAG 1.4.11's 3:1 for non-text UI. This is the
   same correction the design brief records for icons-on-gold. */
.hub-cal__dot--pending { background: var(--gold-text); }
.hub-cal__dot--covered { background: var(--success); }
.hub-cal__dot--office  { background: var(--ink-mute); }
/* A GHOST: filed in The Hub, not on the calendar. Hollow and dashed, so it
   does not read as one more entry - it reads as one that is missing. The same
   ring the agenda row uses, at dot size. */
.hub-cal__dot--orphan {
    background: transparent;
    border: 1px dashed var(--danger);
}

.hub-cal__more {
    font-size: var(--text-xs);
    font-weight: 700;
    color: var(--ink-mute);
    line-height: 1;
}

/* Chips only where a title can actually be read. Below 1025px the dots and the
   cell's accessible name carry the same information. */
.hub-cal__chip { display: none; }

.hub-cal__drift {
    display: inline-block;
    width: 5px;
    height: 5px;
    margin-right: 4px;
    border-radius: var(--radius-full);
    background: var(--danger);
    vertical-align: middle;
}

/* ---------- AGENDA ------------------------------------------------------- */
.hub-cal__agenda {
    list-style: none;
    margin: 0;
    padding: 0;
}

.hub-cal__daygroup {
    list-style: none;
    margin: 0 0 var(--space-2);
    padding: 0;
}

.hub-cal__dayhead {
    position: sticky;
    top: 0;
    /* Above the event cards that follow it in the same group - their hit areas
       would otherwise cover the "+"'s reach box. Below .hub-cal__bar's 5, so a
       stuck day header slides under the toolbar rather than through it. */
    z-index: 2;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    height: 32px;
    padding: 0 var(--space-2);
    background: var(--bg-secondary);
    border-radius: var(--radius-control);
    font-size: var(--text-sm);
}
.hub-cal__dayhead-d { font-weight: 700; color: var(--ink); }
.hub-cal__dayhead-m { color: var(--ink-soft); }
.hub-cal__dayhead-n { margin-left: auto; color: var(--ink-mute); font-size: var(--text-xs); }

/* The one-handed add. Painted 28px, takeable at 44px through .hub-reach's
   transparent ::after - the same trick the register cards use for their
   under-height controls. Chosen over a floating action button: there is no FAB
   primitive on this platform, and a FAB carries no date. */
.hub-cal__day-add {
    /* 10px, not the 8px that makes exactly 44: sub-pixel row positions put the
       edge of a 44px box on a knife edge, and elementFromPoint at the corners
       measured a miss. 28 + 20 = 48px takeable for a 28px paint. */
    --reach-up: 10px;
    --reach-down: 10px;
    --reach-left: 10px;
    --reach-right: 10px;
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    margin-left: var(--space-2);
    padding: 0;
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-control);
    background: var(--surface, #FFFFFF);
    color: var(--ink-soft);
    font-size: var(--text-lg);
    line-height: 1;
    cursor: pointer;
}
.hub-cal__day-add:focus-visible {
    outline: 2px solid var(--gold-text);
    outline-offset: 2px;
}

/* THE EVENT CARD. Two lines, 60px closed - the measured register-card budget,
   reused deliberately rather than the register-card LAYER itself: that layer is
   a contract over table.table-cards--roles whose cells are DB columns, and the
   agenda is not a table. See scripts/check-register-contract.py, which only
   fires on the --roles modifier, so there is no conflict either way. */
.hub-cal__event {
    display: grid;
    grid-template-columns: minmax(0, 1fr) max-content;
    grid-template-areas:
        "title chip"
        "meta  chip";
    align-items: center;
    gap: 2px var(--space-2);
    width: 100%;
    min-height: 60px;
    padding: var(--space-2) var(--space-3);
    margin: 0 0 var(--space-1);
    border: 1px solid var(--border-primary);
    border-left: 3px solid var(--border-secondary);
    border-radius: var(--radius-control);
    background: var(--surface, #FFFFFF);
    color: var(--ink);
    text-align: left;
    text-decoration: none;
    font-family: inherit;
    cursor: pointer;
}
.hub-cal__event:focus-visible {
    outline: 2px solid var(--gold-text);
    outline-offset: 2px;
}
.hub-cal__event-title {
    grid-area: title;
    font-size: var(--text-base);
    font-weight: 600;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.hub-cal__event-meta {
    grid-area: meta;
    font-size: var(--text-xs);
    color: var(--ink-soft);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.hub-cal__event-time {
    grid-area: title;
    font-size: var(--text-xs);
    font-weight: 700;
    color: var(--gold-text);
}
.hub-cal__event .status-chip { grid-area: chip; }

/* A ghost: filed in The Hub, absent from the calendar. Dashed RING, never a
   fill - a solid danger block would out-shout every real absence on the page. */
.hub-cal__event--orphan {
    border: 1px dashed var(--danger);
    border-left: 1px dashed var(--danger);
    background: transparent;
}

.hub-cal__dayview-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin: 0 0 var(--space-3);
}
.hub-cal__dayview-title { margin: 0; font-size: var(--text-lg); color: var(--ink); }
.hub-cal__dayview-n { color: var(--ink-mute); font-size: var(--text-sm); }
.hub-cal__dayview-add { margin-left: auto; }

/* ---------- WEEK --------------------------------------------------------- */
.hub-cal__weekgrid { display: none; }
.hub-cal__weekagenda { display: block; }

.hub-cal__wcol {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
    min-height: 220px;
    padding: var(--space-1);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-control);
    background: var(--surface, #FFFFFF);
}
.hub-cal__wcol--today { border-color: var(--gold-border); }
.hub-cal__wcol-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: var(--space-1) 0;
    color: var(--ink);
    text-decoration: none;
    border-bottom: 1px solid var(--border-primary);
}
.hub-cal__wcol-head:focus-visible {
    outline: 2px solid var(--gold-text);
    outline-offset: 2px;
}
.hub-cal__wcol-dow {
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    color: var(--ink-soft);
}
.hub-cal__wcol-allday,
.hub-cal__wcol-timed {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
/* The timed strip only exists when the week HAS a timed event. A 24-hour ruler
   would be ~95% empty on this calendar: an absence is recorded by class period,
   not by clock time. */
.hub-cal__wcol-timed {
    border-top: 1px dashed var(--border-primary);
    padding-top: var(--space-1);
}

.hub-cal__event--pill {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    min-height: 44px;
    padding: var(--space-1) var(--space-2);
    border-left-width: 3px;
}
.hub-cal__event--pill .hub-cal__event-title { white-space: normal; font-size: var(--text-sm); }
.hub-cal__event--hub { border-left-color: var(--gold-border); background: var(--gold-tint); }
.hub-cal__event--hub .hub-cal__event-title { color: var(--gold-text); }
.hub-cal__event--needs { border-left-color: var(--gold); }

/* ---------- AUTO: the phone default, decided in CSS ---------------------- */
.hub-cal[data-view="auto"] .hub-cal__month  { display: none; }
.hub-cal[data-view="auto"] .hub-cal__agenda { display: block; }

@media (min-width: 641px) {
    .hub-cal[data-view="auto"] .hub-cal__month  { display: grid; }
    .hub-cal[data-view="auto"] .hub-cal__agenda { display: none; }
    /* The empty-agenda wrapper carries the same switch, or a month with nothing
       in it would show the agenda's empty sentence beside the grid. */
    .hub-cal[data-view="auto"] .hub-cal__agenda--empty { display: none; }
}

@media (min-width: 768px) {
    .hub-cal__weekgrid { display: block; }
    .hub-cal__weekagenda { display: none; }
}

/* ---------- TOUCH (<=1024px or coarse) ----------------------------------- */
@media (max-width: 1024px), (pointer: coarse) {
    /* 44px is the floor for anything a finger has to hit. The day cell is
       already 56px; the pills and the step arrows come from .hub-btn / the chip
       primitive, which handle their own touch sizing. */
    .hub-cal__event { min-height: 60px; }
    .hub-cal__wcol-head { min-height: 44px; justify-content: center; }
}

@media (max-width: 640px) {
    /* THREE ROWS AND NO MORE. Title, then ONE control row (prev / Today / next
       / Refresh / New Entry), then the pills. Measured 138px against a 160px
       budget - the earlier shape gave New Entry and Refresh a line each and
       measured 259px, which ate a third of a 812px phone screen before a single
       date was drawn. New Entry stays a 44px-tall gold button, just not a
       full-width one. */
    .hub-cal__bar { padding: var(--space-2) 0 var(--space-1); row-gap: var(--space-1); }
    .hub-cal__title   { order: 1; flex: 1 0 100%; text-align: left; font-size: var(--text-lg); }
    .hub-cal__nav     { order: 2; }
    .hub-cal__refresh { order: 3; }
    .hub-cal__new     { order: 4; margin-left: auto; }
    .hub-cal__views   { order: 5; flex: 1 0 100%; }
    .hub-cal__legend { padding: var(--space-1) 0; gap: var(--space-2) var(--space-3); }
    .hub-cal { --cal-gap: 2px; }
    .hub-cal__day { padding: 2px; }
}

/* THE 44px FLOOR AT 320px, the narrowest phone still in service.
   repeat(7, 1fr) over the console's content width leaves 41px columns there -
   under the touch minimum on the one view where every cell IS the control.
   The grid takes back the page gutter it sits in and drops to a hairline gap,
   which buys the three pixels. Nothing else on the page moves, and the page
   still does not scroll sideways: the pull is into padding that already
   exists. Above 359px the grid needs neither. */
@media (max-width: 359px) {
    .hub-cal { --cal-gap: 1px; }
    .hub-cal__month {
        margin-left: calc(var(--space-3) * -1);
        margin-right: calc(var(--space-3) * -1);
    }
    .hub-cal__day { padding: 1px; }
}

@media (min-width: 1025px) {
    /* THE FLOOR FOLLOWS THE CAP, because the row height follows the row
       COUNT. A cell is 30px of chrome - 2px+2px padding, 1px+1px border, a
       24px number row - plus a 22px chip row per entry and one more for the
       "+N". So a cap of 2 needs 96px, 3 needs 118px, 4 needs 140px, and these
       are the same three numbers as CalendarRenderer::CHIP_CAP_BY_ROWS.
       CHANGE THE TWO TOGETHER. A row may grow past its floor, never below it,
       so the "+N" is never the thing that gets clipped. */
    .hub-cal { --cal-day-h: 96px; }
    .hub-cal__month[data-rows="6"] { --cal-day-h: 96px; }
    .hub-cal__month[data-rows="5"] { --cal-day-h: 118px; }
    .hub-cal__month[data-rows="4"] { --cal-day-h: 140px; }

    /* The grid takes what is left of the window. min-height, not height: when
       the window is tall enough the `1fr` rows split it exactly and the month
       lands inside the fold; when it is not, the rows floor at --cal-day-h and
       the page scrolls rather than the grid clipping an entry. Measured
       before: the grid ended 1027px down an 855px window. */
    .hub-cal__month { min-height: calc(100dvh - var(--cal-chrome)); }

    /* THE NUMBER AND THE DOTS SHARE ONE ROW. Stacked, they cost every
       populated cell 10px - an 8px dot row plus its gap - to say a second time
       what the chips underneath already say. Side by side the dots keep their
       at-a-glance status read and cost nothing. */
    .hub-cal__day {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        align-content: start;
        align-items: center;
        gap: 2px 6px;
        padding: 2px 5px;
    }
    .hub-cal__daynum { grid-column: 1; grid-row: 1; }
    .hub-cal__dots {
        grid-column: 2;
        grid-row: 1;
        justify-content: flex-start;
    }
    .hub-cal__chip { grid-column: 1 / -1; }

    .hub-cal__chip {
        display: block;
        max-width: 100%;
        padding: 1px 5px;
        border-radius: var(--radius-base);
        font-size: var(--text-xs);
        /* 16px, not 1.5 (=18px): a 20px chip against Google's ~20px, and the
           2px it saves per chip is what lets three chip rows plus the number
           sit inside a 96px cell. */
        line-height: 16px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    /* The platform's pending recipe: gold as a TINT with gold TEXT on it,
       never gold as the text colour on white. */
    .hub-cal__chip--hub {
        background: var(--gold-tint);
        border: 1px solid var(--gold-border);
        color: var(--gold-text);
    }
    .hub-cal__chip--office {
        background: var(--bg-secondary);
        border: 1px solid var(--border-primary);
        color: var(--ink-soft);
    }
    /* Gold as a FILL is permitted (design brief R3); gold as TEXT is not. */
    .hub-cal__chip--needs { border-left: 3px solid var(--gold); }
    .hub-cal__chip--orphan {
        background: transparent;
        border: 1px dashed var(--danger);
        color: var(--danger);
    }
    .hub-cal__chip--more {
        background: transparent;
        border: 0;
        color: var(--ink-mute);
        font-weight: 700;
    }
    /* A multi-day absence repeats per covered day; the touching corners flatten
       so a run reads as one band. Deliberately NOT an absolutely positioned
       spanning bar: spanning needs per-week row packing and measured pixel
       offsets, both of which break under text zoom, and these absences are
       overwhelmingly one to three days. */
    .hub-cal__chip--continues-left  { border-top-left-radius: 0; border-bottom-left-radius: 0; border-left-width: 0; }
    .hub-cal__chip--continues-right { border-top-right-radius: 0; border-bottom-right-radius: 0; border-right-width: 0; }

    .hub-cal__dot { width: 7px; height: 7px; }
}

/* ---------- EDITOR (inside the .hub-drawer primitive) -------------------- */
.hub-cal-edit { display: block; }
.hub-cal-edit__banner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding: 10px 12px;
    margin: 0 0 var(--space-4);
    border-radius: var(--radius-control);
    background: var(--bg-secondary);
    border: 1px solid var(--border-primary);
    font-size: var(--text-sm);
    color: var(--ink-soft);
}
.hub-cal-edit__banner--hub {
    background: var(--gold-tint);
    border-color: var(--gold-border);
    color: var(--gold-text);
}
.hub-cal-edit__field { margin: 0 0 var(--space-4); }
.hub-cal-edit__field label {
    display: block;
    margin: 0 0 var(--space-1);
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--ink);
}
.hub-cal-edit__field input[type="text"],
.hub-cal-edit__field input[type="date"],
.hub-cal-edit__field input[type="time"],
.hub-cal-edit__field textarea {
    width: 100%;
    min-height: 44px;
    padding: 10px 12px;
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-control);
    background: var(--surface, #FFFFFF);
    color: var(--ink);
    font-family: inherit;
    font-size: var(--text-base);
}
.hub-cal-edit__field textarea { min-height: 88px; resize: vertical; }
.hub-cal-edit__dates {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: var(--space-3);
}
.hub-cal-edit__note {
    margin: var(--space-1) 0 0;
    font-size: var(--text-xs);
    color: var(--ink-soft);
}

/* AN UNCONFIRMED GUESS HAS TO LOOK LIKE ONE. The teacher on a Google-typed
   entry is read out of its title, and a bare-initials hit ("CC") is a guess,
   not a fact - those letters are equally a room or a class. Muted and
   italic, inline after the name, so the name is never read as confirmed. */
.hub-cal-edit__guess {
    font-size: var(--text-xs);
    font-style: italic;
    color: var(--ink-mute);
}

/* WHO WAS OUT. A full-width select, because a name list is long and a narrow
   one truncates exactly the surnames that tell two people apart. The button
   sits under it rather than beside it: at 375px a row of select-plus-button
   gives the select about 180px, and this control exists to be read. */
.hub-cal-edit__teacher select {
    width: 100%;
    min-height: 44px;
    padding: 10px 12px;
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-control);
    background: var(--surface, #FFFFFF);
    color: var(--ink);
    font-family: inherit;
    font-size: var(--text-base);
}
.hub-cal-edit__teacher-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-2);
}
.hub-cal-edit__teacher-row .hub-cal-edit__note { margin: 0; flex: 1 1 12ch; }
/* An inline link inside a sentence still has to be tappable: 15px of text is
   not a target. The row grows to 44 rather than the type. */
.hub-cal-edit__note a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    color: var(--gold-text);
}

/* A CHECKBOX WHOSE LABEL IS THE TARGET. The box paints at 22px - nobody ships a
   44px checkbox - and the whole 44px row toggles it, which is what a finger
   actually gets. Same paint-versus-hit split as .hub-cal__day-add. */
/* THE :not() CHAIN IS NOT DECORATION. enterprise-components.css carries
   `.admin-root label:not(.form-check-label):not(.pkg-filter-label):not(.hub-toggle)`
   at (0,4,1) - each :not() argument counts - and it sets display:inline-block
   plus margin-bottom:4px on every label in the console. A bare
   `.hub-cal-edit__check label` scores (0,1,1) and loses: measured, the row came
   back display:inline-block with the 12px gap inert and the box on the text
   baseline. Matching the chain and adding the ancestor reaches (0,5,1) and wins
   on real specificity, not !important - the same fix, for the same rule, that
   .sec-settings__opt records at enterprise-components.css:1895. */
.admin-root .hub-cal-edit__check label:not(.form-check-label):not(.pkg-filter-label):not(.hub-toggle),
.hub-cal-edit__check label {
    display: flex;
    width: 100%;
    align-items: center;
    gap: var(--space-3);
    min-height: 44px;
    margin: 0;
    cursor: pointer;
    font-weight: 600;
}
.hub-cal-edit__check input[type="checkbox"] {
    flex: 0 0 auto;
    width: 22px;
    height: 22px;
    margin: 0;
    accent-color: var(--gold-text);
}
.hub-cal-edit__check label:focus-within {
    outline: 2px solid var(--gold-text);
    outline-offset: 2px;
    border-radius: var(--radius-control);
}
.hub-cal-edit__ro {
    padding: 10px 12px;
    border-radius: var(--radius-control);
    background: var(--bg-secondary);
    font-size: var(--text-base);
    color: var(--ink);
}
.hub-cal-edit__foot {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border-primary);
}
.hub-cal-edit__foot .hub-btn { width: 100%; }
.hub-cal-edit__yours {
    margin: var(--space-3) 0 0;
    padding: var(--space-3);
    border: 1px dashed var(--border-secondary);
    border-radius: var(--radius-control);
    font-size: var(--text-sm);
    color: var(--ink-soft);
}
.hub-cal-edit__yours h3 {
    margin: 0 0 var(--space-1);
    font-size: var(--text-sm);
    color: var(--ink);
}

@media (min-width: 769px) {
    .hub-cal-edit__foot { flex-direction: row-reverse; justify-content: flex-start; }
    .hub-cal-edit__foot .hub-btn { width: auto; }
}

/* ---------- LOADING (the stage never goes blank mid-fetch) --------------- */
/* THE OLD GRID DIMS; IT IS NOT REPLACED. There was a .hub-cal__skeleton here
   and a skeleton() in hub-calendar.js that built one - six grey rows, always
   six - but nothing ever inserted it, so the teardown at the end of go() only
   ever saw a null parentNode. Both are deleted rather than wired up, for two
   reasons: fading the real grid keeps the month you were reading on screen
   while the next one loads, and grey bars would have thrown it away; and the
   hardcoded 6 is now simply wrong, because the grid draws 4, 5 or 6 rows
   depending on the month. Do not reintroduce a fixed-row placeholder. */
.hub-cal__stage[data-loading="1"] { opacity: 0.55; }

/* ========== shared/register-cards.css ========== */

/* ==========================================================================
   REGISTER CARDS - the shared phone card for every register
   (DESIGN-BRIEF REGISTER CARDS, platform rule 2026-09-08).

   WHAT IT IS. Below 641px a table.table-cards.table-cards--roles renders each
   row as the two-line CARD fuel.php measured with the owner (2026-09-07/08):

       title ........................................ meta  v      line 1
       EARNED +26   RECEIVED -14                  [control]      line 2
       (details track: a <dl> that opens on tap)                 line 3

   THE CONTRACT the markup stamps (Hub\Components\Register emits it; a Blade
   loop or a JS template string may stamp it by hand - the gate
   scripts/check-register-contract.py keeps those honest):
       td[data-role="title"]    exactly one; .hub-regcard__title, then
                                .hub-regcard__meta, then the .hub-regcard__toggle
       td[data-role="meta"]     the desktop column whose value the title repeats;
                                display:none on the card
       td[data-role="figure"]   ledger number: .hub-regcard__fig (+ data-sign
                                painted as ::before), .hub-regcard__unit desktop-only
       td[data-role="status"]   a chip on the ledger line (figure + status <= 2)
       td[data-role="control"]  ONE interactive element pinned bottom-right,
                                painted small, its 44px hit a transparent ::after
       td[data-role="select"]   the checkbox top-left in a 44px .hub-cb-hit label
       td[data-role="value"]    the recipe's label/value row (.hub-stack stacks lines)
       td[data-role="details"]  LAST td: .hub-regcard__more > dl.hub-regcard__list
       td[data-phone="more"|"hide"]  hidden on the card; td.is-empty dropped
       tr[data-expand]          tap-to-expand (hub-cards.js); STATE IS tr.is-open,
                                NEVER the hidden attribute (Bootstrap's
                                [hidden]{display:none!important} on the Blade
                                shells would kill the 0fr track)

   SPECIFICITY RULE (file-wide; pinned by RegisterCardsCssTest). This file is
   bundled BEFORE shared/_responsive.css, whose .table-cards recipe is
   (0,2,1)/(0,2,2) with td:last-child and td.is-empty at (0,3,1). So every
   card rule here is written
       :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="x"]
   = (0,3,3), and tr rules `... table.table-cards--roles tbody tr` = (0,2,3),
   which beat the recipe on specificity, not order. The rules the role rules
   would out-rank (is-empty, the data-phone hides) are RESTATED LAST in the
   640 block at (0,3,3). No !important; no :not() in the 640 block except the
   a:not(.hub-btn) control child. A page override needs (0,4,x). Tokens only,
   with the allow-listed fallbacks.

   LINEAGE + BUDGET. Lifted verbatim from public/sections/reimbursements/fuel.php
   (its 640 block, 2026-09-07/08) with the ft- names replaced by roles. Measured
   at 375: closed = 1 + 6 + 20 + 4 + 21 + 6 + 1 = 59; a 28px --sm button on the
   ledger line -> 66; a chip title (24.8 line) with a chip-link -> 68; a chip-link
   on a plain title -> ~63; a wrapped title +20; open = +19 + 21 per pair + 3 per
   gap + 20 per extra wrapped note line. Every control's takeable hit >= 44px by
   elementFromPoint (the reach, never the painted box).

   SHIP via ./build-css-bundles.sh --min - never hand-edit the bundles. Listed
   in all three bundle lists immediately before shared/_responsive.css.
   Page-local phone cards are no longer written (DESIGN-BRIEF REGISTER CARDS).
   ========================================================================== */

/* ---- A. BASE (all widths) ------------------------------------------------ */

/* Alignment classes the emitter puts on th and td - no inline text-align. */
:is(.admin-root, .hub-page) table.table-cards--roles .hub-num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}
:is(.admin-root, .hub-page) table.table-cards--roles .hub-right { text-align: right; }
:is(.admin-root, .hub-page) table.table-cards--roles .hub-center { text-align: center; }

/* The checkbox label: shell-agnostic, the 44px size arrives in block B. */
.hub-cb-hit {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin: 0;
    cursor: pointer;
}
.hub-cb-hit input {
    width: 20px;
    height: 20px;
    margin: 0;
}

/* The select cell's label, at every width: `.admin-root label:not(...)` (0,4,1)
   blockifies every label with a 4px bottom margin, which pushed the 44px label
   2px out of its cell on the admin shell (measured 43px takeable). (0,4,3). */
:is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="select"] > .hub-cb-hit {
    display: inline-flex;
    margin: 0;
}

/* Phone-only spans inside a control (the chip beside a desktop "View"). */
:is(.admin-root, .hub-page) table.table-cards--roles .hub-phone { display: none; }

/* ---- B. REACH UTILITY (touch) ------------------------------------------- */
/* HIT AREA WITHOUT HEIGHT. A control paints at its compact size and grows its
   hit through a transparent ::after that extends by --reach-up/-down/-left/
   -right. Margin never does this; an overlay does. Any under-height control
   on the platform may wear .hub-reach and set the four properties inline or
   in a page rule. (.hub-chip__x and .sd-cb-hit stay where they are this step.) */
@media (max-width: 1024px), (pointer: coarse) {
    .hub-reach { position: relative; }
    .hub-reach::after {
        content: "";
        position: absolute;
        left: calc(-1 * var(--reach-left, 0px));
        right: calc(-1 * var(--reach-right, 0px));
        top: calc(-1 * var(--reach-up, 0px));
        bottom: calc(-1 * var(--reach-down, 0px));
    }
    .hub-cb-hit {
        width: var(--tap-target, 2.75rem);
        height: var(--tap-target, 2.75rem);
    }
}

/* ---- C. THE CARD (<= 640px) --------------------------------------------- */
@media (max-width: 640px) {
    /* TWO LINES, EVERY CARD. Column 1 is sized by the first ledger cell, 2 is
       the flexible middle, 3 by the control, which collapses to nothing on a
       card with no control. The title spans all three so the meta reaches the
       card's edge on every card alike. row-gap stays 0: the 4px rhythm is
       margin-top on the ledger cells, because a gap would be paid above the
       0fr details track too (59 -> 63). position:relative comes from the
       recipe's tr rule and is kept (the select cell pins to it). */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody tr {
        display: grid;
        grid-template-columns: max-content minmax(0, 1fr) max-content;
        grid-template-areas:
            "title title title"
            "fig1  fig2  control"
            "more  more  more";
        column-gap: 0;
        row-gap: 0;
        align-items: start;
        padding: 6px 12px;
        margin: 0 0 6px;
        font-size: 14px;            /* body step; cards inherited 16px */
        cursor: default;
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody tr[data-expand] { cursor: pointer; }
    /* Belt for the filter bar's tr[hidden]: the grid above can never out-rank
       a hidden row even if enterprise-components' shared selector is reshaped. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody tr[hidden] { display: none; }

    :is(.admin-root, .hub-page) table.table-cards--roles tbody td {
        padding: 0;                 /* the 44px belongs to the control's reach */
        min-height: 0;
        line-height: 20px;
        max-width: none;            /* the .cell-clamp / max-width:260px desktop leak */
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td.cell-clamp {
        overflow: visible;
        text-overflow: clip;
    }

    /* TITLE - line 1: the title left, the meta at the far right, the toggle
       in the corner. overflow:visible so the toggle's offset focus ring is
       never clipped; overflow-wrap:anywhere so an unbroken name wraps inside
       the band instead of running under the control. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="title"] {
        grid-area: title;
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: 0 10px;
        overflow: visible;
        font-size: 15px;
        font-weight: 700;
        line-height: 20px;
        color: var(--ink, #191712);
        text-align: left;
    }
    /* No caps label on these; the details cell's empty ::before would be a grid item. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="title"]::before,
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="meta"]::before,
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="status"]::before,
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"]::before,
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="select"]::before,
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="details"]::before { content: none; }
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__title {
        min-width: 0;
        overflow-wrap: anywhere;
    }
    /* 13px so --ink-mute is legal (the muted tone's floor). */
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__meta {
        margin-left: auto;
        font-size: 13px;
        font-weight: 400;
        color: var(--ink-mute, #6F6A5C);
        white-space: nowrap;
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="meta"] { display: none; }

    /* LEDGER - line 2: "EARNED +26   RECEIVED -14". The caps label is the
       recipe's data-label ::before. flex-wrap + flex-start replace the
       recipe's space-between (the second cell spans the wide column and
       space-between would throw its figure to the far edge). DOM order is
       slot order: the first figure/status lands in fig1, the next in fig2. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="figure"],
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="status"] {
        display: flex;
        flex-wrap: wrap;
        justify-content: flex-start;
        align-items: baseline;
        gap: 0 6px;
        margin-top: 4px;            /* the 4px rhythm, as an item margin (see the tr rule) */
        text-align: left;
        color: var(--ink, #191712);
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="figure"] {
        font-weight: 700;
        font-variant-numeric: tabular-nums;
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="figure"],
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="status"] { grid-area: fig1; }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="figure"] ~ td[data-role="figure"],
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="figure"] ~ td[data-role="status"],
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="status"] ~ td[data-role="figure"],
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="status"] ~ td[data-role="status"] {
        grid-area: fig2;
        padding-left: 16px;
    }
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__fig { white-space: nowrap; }
    /* No unit on the card ("+28 / -21", as the balance block reads): the 56px
       it cost is what lets the control share the ledger line at 375. */
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__unit { display: none; }
    /* The sign is generated content: textContent stays "26" for hub-table.js. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-sign="+"] .hub-regcard__fig::before { content: "+"; }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-sign="-"] .hub-regcard__fig::before { content: "\2212"; }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="status"] .status-chip {
        display: inline-flex;
        align-items: center;
        gap: 5px;
    }

    /* CONTROL - bottom-right of the ledger line. The cell's max-content width
       IS the grid column, so the control sizes the column and the title gets
       the rest. min-height:0 - the 44px belongs to the control's overlay, not
       the cell. overflow:visible keeps the gold focus ring unclipped. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] {
        grid-area: control;
        display: flex;
        align-items: flex-end;      /* the control sits on the card's bottom line */
        justify-content: flex-end;
        align-self: end;
        min-height: 0;
        margin-top: 4px;
        margin-left: 8px;           /* the ledger's clearance from the control */
        padding: 0;
        width: auto;
        overflow: visible;
    }
    /* WHERE THE REACH MAY GO (fuel.php, 2026-09-08): UP the 4px rhythm gap
       above the ledger line, and for the chip-link ONLY 3px more into the
       title line's bottom leading (the 20px line holds 15px type; the 13px
       meta's glyphs end above that strip; the toggle keeps its 20x20 corner
       through z-index:1 below). NEVER further - at 9-10px the bottom of the
       meta navigated. DOWN through the card's 6px padding + 1px border + the
       6px between cards, which no card owns. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > a,
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > button,
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > .hub-menu > .hub-menu-trigger { position: relative; }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > a::after,
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > button::after,
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > .hub-menu > .hub-menu-trigger::after {
        content: "";
        position: absolute;
        left: calc(-1 * var(--reach-left, 0px));
        right: calc(-1 * var(--reach-right, 0px));
        top: calc(-1 * var(--reach-up, 0px));
        bottom: calc(-1 * var(--reach-down, 0px));
    }
    /* A --sm button paints 28px (4 + 28 + 13 = 45 takeable); the primitive's
       44px touch lift is turned back off here - the hit lives in the overlay. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > .hub-btn {
        --reach-up: 4px;
        --reach-down: 13px;
        height: var(--btn-h-sm, 28px);
        min-height: var(--btn-h-sm, 28px);
        padding: 0 10px;
        font-size: 12px;
    }
    /* Icon buttons and the kebab trigger: 28x28 painted, 8px reach each side. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > .hub-btn--icon,
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > .hub-menu > .hub-menu-trigger {
        width: var(--btn-h-sm, 28px);
        height: var(--btn-h-sm, 28px);
        --reach-up: 4px;
        --reach-down: 13px;
        --reach-left: 8px;
        --reach-right: 8px;
    }
    /* The chip-link: a bare <a> wearing a chip (24.8 painted); 7 + 24.8 + 13 =
       44.8 takeable, 4px side padding makes the hit 93 wide (padding, never
       margin). 7px is the ceiling (fuel's measurement) - further and the meta
       navigates. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > a:not(.hub-btn) {
        --reach-up: 7px;
        --reach-down: 13px;
        display: inline-flex;
        align-items: flex-end;
        min-height: 0;
        padding: 0 4px;
        text-decoration: none;
        overflow: visible;          /* .admin-root .module-action-link is overflow:hidden - it clipped the reach to 26px (measured) */
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] .status-chip {
        display: inline-flex;
        align-items: center;
        gap: 5px;
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] .status-chip .svg-icon { font-size: 14px; }
    /* Phone/desktop label swaps inside a control. */
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-phone { display: inline-flex; }
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-desk { display: none; }

    /* SELECT - the checkbox pinned top-left (the recipe's tr is
       position:relative); absolute, so the closed height stays 59. The row
       pads left so every line clears the 44px label. :has() is already in the
       recipe baseline (.table-scroll--panel:has(> .table-cards)). */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="select"] {
        position: absolute;
        top: 6px;
        left: 4px;
        height: var(--tap-target, 2.75rem);
        display: flex;
        align-items: center;
        padding: 0;
        width: auto;
        min-height: 0;
    }
    /* Belt: the card's label is 44px whatever block B decides in future. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="select"] > .hub-cb-hit {
        width: var(--tap-target, 2.75rem);
        height: var(--tap-target, 2.75rem);
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody tr:has(> td[data-role="select"]) {
        padding-left: calc(12px + 2.3rem);
    }

    /* VALUE - the recipe's label/value row ACROSS THE FULL CARD: a value cell
       has no named area, and an auto-placed grid item would land in the next
       free column of an implicit row (measured: three value cells side by side
       in the 3-column grid). Spanning every column keeps each one its own line.
       .hub-stack is the stacked-lines block the recipe's display:flex td
       cannot lay out (one child per line). */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="value"] { grid-column: 1 / -1; }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="value"] > .hub-stack {
        display: block;
        min-width: 0;
        text-align: right;
    }
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-stack > * {
        display: block;
        white-space: normal;
    }
    /* (0,4,3) beats the recipe's td:last-child flex-end for a value row that
       happens to be last (a table without a details column). */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="value"]:last-child { justify-content: space-between; }

    /* TOGGLE - a .hub-btn icon button painted 20px on the title line. THE
       DELIBERATE EXCEPTION to the 44px icon-button rule: the CARD is the tap
       target (hub-cards.js), the button is the keyboard and screen-reader
       control, so a finger never needs the 20px box itself; the primitive's
       44px touch lift is turned off here (a 44px box would be a second line
       on a 59px card). z-index:1 is load-bearing: the chip-link's reach climbs
       3px into the title line at the card's right edge, exactly where this
       box sits, and the higher layer keeps the corner the toggle's. */
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__toggle {
        flex: 0 0 auto;
        align-self: center;         /* not baseline: an svg's baseline is its bottom edge */
        position: relative;
        z-index: 1;
        width: 20px;
        height: 20px;
        min-height: 0;
        padding: 0;
        border: 0;
        border-radius: 4px;
        background: transparent;
        box-shadow: none;
        color: var(--ink-mute, #6F6A5C);   /* 5.4:1 on white; clears the 3:1 glyph rule */
        font-size: 16px;                   /* .svg-icon svg is 1em */
    }
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__toggle:hover { background: transparent; }
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__toggle:focus-visible {
        outline: 2px solid var(--gold-text, #75692E);
        outline-offset: 2px;
        box-shadow: none;                  /* the primitive's soft ring is replaced, not stacked */
    }
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__toggle .svg-icon {
        display: inline-flex;
        transition: transform 200ms ease;
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody tr.is-open .hub-regcard__toggle .svg-icon { transform: rotate(180deg); }

    /* DETAILS - line 3. A 0fr -> 1fr track animates height without measuring;
       the inner box is the sized item and clips. justify-content:stretch pins
       the implicit column (the recipe's td:last-child flex-end would stop it
       stretching and a long note would leave the card). visibility:hidden
       while closed keeps the list out of the accessibility tree and the tab
       order; the delay keeps it readable while the row is still collapsing.
       STATE IS tr.is-open - the hidden attribute is never put on this cell. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="details"] {
        grid-area: more;
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: 0fr;
        justify-content: stretch;
        transition: grid-template-rows 200ms ease;
        padding: 0;
        margin: 0;
        min-height: 0;
        width: auto;
        overflow: hidden;
        text-align: left;
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody tr.is-open > td[data-role="details"] { grid-template-rows: 1fr; }
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__more {
        min-height: 0;
        overflow: hidden;                          /* the sized item; NO padding here or the closed height is the padding */
        visibility: hidden;
        transition: visibility 0s linear 200ms;    /* hide AFTER the collapse finishes */
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody tr.is-open .hub-regcard__more {
        visibility: visible;
        transition-delay: 0s;
    }
    /* Label/value pairs: caps label per the small-caps floor (12/700/.08em
       --ink-soft), 14px --ink values, 20px lines, 3px between pairs, a
       hairline above. Spacing lives INSIDE the clipped box so a closed cell
       is 0px. */
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__list {
        display: grid;
        grid-template-columns: max-content minmax(0, 1fr);
        column-gap: 12px;
        row-gap: 3px;
        align-items: baseline;
        margin: 8px 0 2px;
        padding-top: 8px;
        border-top: 1px solid var(--border-primary, #E5E7EB);
        font-size: 14px;
        line-height: 20px;
        font-weight: 400;
    }
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__list dt {
        margin: 0;
        font-size: 12px;
        font-weight: 700;
        letter-spacing: .08em;
        text-transform: uppercase;
        color: var(--ink-soft, #4A4638);
    }
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__list dd {
        margin: 0;
        color: var(--ink, #191712);
        font-variant-numeric: tabular-nums;
        overflow-wrap: anywhere;
        white-space: pre-line;    /* typed line breaks in a note survive */
    }

    /* RESTATEMENTS - last in the block, (0,3,3), after every role rule: the
       role rules' display values would otherwise out-rank the recipe's
       (0,3,1) is-empty hide and there is no recipe rule for data-phone. */
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-phone="more"],
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-phone="hide"] { display: none; }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td.is-empty { display: none; }
}

/* ---- D. TABLET FLOOR (641-1024px) --------------------------------------- */
/* A touch band where the desktop table still renders and a bare chip-link
   control has no rule on the staff shell (.module-action-link is
   .admin-root-scoped) - it measured 37x20 on fuel.php. The band excludes 640
   so it never fights block C by order. */
@media (min-width: 641px) and (max-width: 1024px) {
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > a:not(.hub-btn) {
        display: inline-flex;
        align-items: center;
        min-height: var(--btn-h-touch, 44px);
        padding: 0 var(--space-2, 8px);
        border-radius: 8px;
        text-decoration: none;
    }
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="control"] > a:not(.hub-btn):focus-visible {
        outline: 2px solid var(--gold-text, #75692E);
        outline-offset: 2px;
    }
}

/* ---- E. REDUCED MOTION (DESIGN-BRIEF MOTION) ---------------------------- */
@media (prefers-reduced-motion: reduce) {
    :is(.admin-root, .hub-page) table.table-cards--roles tbody td[data-role="details"],
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__more,
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__toggle .svg-icon { transition: none; }
}

/* ---- F. GUARD (>= 641px) ------------------------------------------------ */
/* The phone-only pieces: [data-role="details"] hides the th AND every td (a
   display:none cell generates no table box, so the desktop table gains no
   column); the toggle has zero client rects, which is hub-cards.js's desktop
   test; the meta span and the phone chip vanish behind their desktop columns. */
@media (min-width: 641px) {
    :is(.admin-root, .hub-page) table.table-cards--roles [data-role="details"],
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__meta,
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-regcard__toggle,
    :is(.admin-root, .hub-page) table.table-cards--roles .hub-phone { display: none; }
}

/* ========== shared/_responsive.css ========== */

/* ============================================================================
   RESPONSIVE TAIL - MUST BE THE LAST FILE IN EVERY BUNDLE
   ============================================================================
   The mobile/touch overrides for the admin shell, admin header, and the
   users-layout live here and ONLY here. Reason: media queries add no
   specificity, so a base rule for these components in any LATER file silently
   defeats the override (this bug shipped four times: sidebar position, header
   grid-column, users-layout columns). build-css-bundles.sh asserts this file
   concatenates last - do not add rules for these components below/after it.
   ============================================================================ */



/* Below 1025px (tablet breakpoint per conventions) the sidebar becomes an
   off-canvas drawer toggled by the header hamburger (.admin-nav-toggle in
   EnterpriseHeader), sliding over a backdrop. The shell must NOT reserve
   its column - that was the "dead gray strip" bug. */
@media (max-width: 1024px) {
    .admin-shell,
    .admin-shell.has-collapsed-sidebar {
        grid-template-columns: 1fr;
        grid-template-rows: auto minmax(0, 1fr) auto;
    }

    .admin-header,
    .admin-main,
    .admin-footer {
        grid-column: 1;
    }

    .admin-sidebar {
        position: fixed;
        top: 0;
        left: -300px;
        width: 280px;
        height: 100vh;
        height: 100dvh;
        z-index: 10010; /* above the sticky header (20) and user dropdown (10002) */
        overflow-y: auto;
        transition: left 0.3s ease;
        box-shadow: 2px 0 10px rgba(0, 0, 0, 0.3);
    }

    .admin-sidebar.open {
        left: 0;
    }

    .admin-nav-toggle {
        display: flex;
    }

    /* Backdrop under the open drawer (rendered by EnterpriseHeader). */
    .admin-drawer-backdrop.show {
        display: block;
    }

    /* Icon-only context-switch button (Management/Admin) so the header can
       shrink to phone width; the label returns on desktop. */
    .admin-header .context-switch-btn .btn-label {
        display: none;
    }

    .admin-header .context-switch-btn {
        width: var(--tap-target, 2.75rem);
        min-width: var(--tap-target, 2.75rem);
        height: var(--tap-target, 2.75rem);
        padding: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    .admin-header .admin-header-actions {
        flex-shrink: 0;
    }
}

@media (max-width: 640px) {
    /* The full-bleed title-bar negative margins over-bleed the phone viewport
       (document.scrollWidth 397 vs clientWidth 375) - the horizontal bleed is
       desktop-only. Top margin intentionally kept. */
    .admin-main-content .content-header,
    .admin-tab .tab-header {
        margin-left: 0;
        margin-right: 0;
    }
}

/* ==========================================================================
   /admin/users - touch layout (<=1024px per breakpoint conventions)
   ========================================================================== */
@media (max-width: 1024px) {
    /* Both content-headers become wrappable: the Send Invitation button and
       the stacked toolbar drop under their titles instead of fighting them
       on one no-wrap row. */
    .admin-main-content .content-header {
        flex-wrap: wrap;
        gap: 0.5rem;
    }

    /* (Role-filter rail rules removed: the filter is a .hub-drawer at every
       width now - same primitive on desktop and phone.) */
}

/* --------------------------------------------------------------------------
   REGISTER TOOLBAR - the search box had NO TYPEABLE WIDTH on a phone.
   --------------------------------------------------------------------------
   Measured on /admin/users at a true 375px viewport (2026-09-02 sizing audit):
   input#userSearch came out 82px wide carrying 80px of its own horizontal
   padding - a TWO-PIXEL content box - and it measured the same at 320, 360,
   390 and 414. .table-controls (admin/admin-dashboard.css:513) is a flex row
   with no flex-wrap at any width, so search + status + role ask for 380px
   inside a 351px container and the search box, as the only flex:1 item, is
   the one that collapses. Nothing you typed was visible. The page never
   overflowed, which is why the no-horizontal-scroll gate stayed green.

   The row wraps on phones instead, and the search takes its own line - which
   is also what makes the 2.5rem icon padding affordable again. The two
   controls are floored at the touch target while we are here: they measured
   38px against a 44px platform law. */
@media (max-width: 640px) {
    .admin-root .table-controls {
        flex-wrap: wrap;
    }

    .admin-root .table-controls .search-box {
        flex-basis: 100%;
        min-width: 0;
    }

    .admin-root .search-input,
    .admin-root .filter-select {
        min-height: var(--tap-target, 2.75rem);
    }
}

/* Card-transform recipe (dormant until a table opts in).
   Convention: to make a table stack into cards on phones, add class
   .table-cards to the <table> and data-label="<Column Name>" to every <td>.

   EVERY selector is prefixed :is(.admin-root, .hub-page) ON PURPOSE. The base
   component rules (.admin-root .data-table thead/td, specificity 0-2-1,
   enterprise-components) out-rank an unprefixed .table-cards rule (0-1-x) and
   re-impose sticky thead + table-cell layout, pushing card values off-screen.
   Prefixing here lifts the recipe to 0-2-x so it wins by order over the 0-2-1
   base and by specificity over the bare 0-1-1 .data-table copies in
   admin-dashboard.css. :is() keeps that specificity while extending the
   recipe to the staff shell (.hub-page main content), whose register tables
   have no adversarial base rules but ride the same bundle tail. Detaching the
   table from table layout (display:block on table/tbody) stops the columns
   from being sized by the invisible header row. */
@media (max-width: 640px) {
    /* Detach from table layout: without this the table still computes
       display:table and the (clipped) header's columns drive the width. */
    :is(.admin-root, .hub-page) table.table-cards {
        display: block;
        width: 100%;
        min-width: 0; /* .data-table min-width:600px would push values off-screen */
    }

    :is(.admin-root, .hub-page) table.table-cards tbody {
        display: block;
    }

    /* Visually hide the header row (beats the base sticky thead at 0-2-1). */
    :is(.admin-root, .hub-page) table.table-cards thead,
    :is(.admin-root, .hub-page) .table-cards thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }

    :is(.admin-root, .hub-page) .table-cards tr {
        display: block;
        position: relative; /* containing block for pinned cells (status chip) */
        margin: 0 0 0.6rem;
        min-width: 0;
        border: 1px solid var(--border-primary);
        border-radius: 8px;
        padding: 0.5rem 0.75rem;
        background: var(--hub-tile-bg, #fff);
    }

    :is(.admin-root, .hub-page) .table-cards td {
        display: flex;
        justify-content: space-between;
        gap: 1rem;
        align-items: baseline;
        min-width: 0;
        min-height: 0;   /* beats base td height: var(--table-row-height) */
        height: auto;
        line-height: 1.4;
        border: 0;
        padding: 0.3rem 0; /* compact: ~5 fields must fit ~120px */
        text-align: right;
        width: auto;
        overflow: hidden;
        /* The desktop no-wrap density law sets white-space:nowrap on register/
           data cells; in card mode the value must reflow inside its card, not
           get clipped by the overflow:hidden above. */
        white-space: normal;
    }

    :is(.admin-root, .hub-page) .table-cards td > * {
        min-width: 0;
    }

    /* Small-caps floor (brief, 2026-07-19): caps-micro labels - data-labels
       included by name - are MINIMUM 12px and --ink-soft, never the muted
       tone. */
    :is(.admin-root, .hub-page) .table-cards td::before {
        content: attr(data-label);
        font-weight: 700;
        font-size: 12px;
        text-transform: uppercase;
        letter-spacing: 0.08em;
        color: var(--ink-soft, #4A4638);
        text-align: left;
        flex-shrink: 0;
    }

    :is(.admin-root, .hub-page) .table-cards td:last-child {
        justify-content: flex-end;
    }

    /* A cell whose value is legitimately absent on desktop (Paid before
       issue, requester on a batch-printed blank) marks itself .is-empty in
       the blade: the desktop column keeps its slot, the card drops the row
       instead of showing a labeled blank. */
    :is(.admin-root, .hub-page) .table-cards td.is-empty {
        display: none;
    }

    /* Cards carry their own borders; the register panel chrome around them
       would read as a box around boxes. */
    :is(.admin-root, .hub-page) .table-scroll--panel:has(> .table-cards) {
        border: 0;
        border-radius: 0;
        background: transparent;
    }

    /* Value rows must be uniform height (~26-30px). Badges/chips inside a card
       cell shed their table-row margins/min-height AND their vertical padding
       so the Role row stops standing taller than its siblings. */
    :is(.admin-root, .hub-page) .table-cards td .badge,
    :is(.admin-root, .hub-page) .table-cards td .role-badge,
    :is(.admin-root, .hub-page) .table-cards td .status-badge,
    :is(.admin-root, .hub-page) .table-cards td [class*="role-badge-"] {
        margin: 0;
        min-height: 0;
        padding-top: 1px;
        padding-bottom: 1px;
        line-height: 1.3;
    }

    /* The Role value cell itself must be as tight as its text siblings: the
       badge is a small inline chip, not a 42px table row. */
    :is(.admin-root, .hub-page) .table-cards td.col-role {
        padding-top: 0.15rem;
        padding-bottom: 0.15rem;
        align-items: center;
        min-height: 0;
    }

    /* Cap the avatar so the Name (card title) row is ~32px, not 42px. */
    :is(.admin-root, .hub-page) .table-cards td.col-user .user-avatar-img,
    :is(.admin-root, .hub-page) .table-cards td.col-user .user-avatar {
        width: 24px;
        height: 24px;
        font-size: 0.7rem;
    }
    :is(.admin-root, .hub-page) .table-cards td.col-user .user-cell {
        gap: 0.5rem;
        align-items: center;
    }
}

/* ---- /admin/users toolbar + table, touch/phone ---- */
@media (max-width: 1279px) and (min-width: 641px) {
    /* Last-login earns its keep only on wide desktops; cards re-show it. */
    .users-data-table .col-login { display: none; }
}

@media (max-width: 1024px) {
    /* The rule that lived here was dead: `.users-primary-actions .bulk-btn`
       occurred exactly once in the whole codebase - here - and matched no
       markup, so the toolbar has never been adjusted for mobile at all. The
       three buttons are #exportUsersBtn, #importGoogleBtn and #sendInvitation.
       Its stated intent was to HIDE Export CSV and Import from Google on
       phones; that removes working features rather than laying them out, so
       the replacement below wraps the row instead. */
}

@media (max-width: 640px) {
    /* Dense table -> stacked cards (uses the shared .table-cards recipe).
       Card header row: bold name left, status chip pinned top-right.
       .admin-root prefix keeps these above the base .data-table rules. */
    .admin-root .users-data-table.table-cards td.col-checkbox { display: none; }
    /* Groups is DESKTOP-ONLY. It exists to fill width a phone does not have,
       and as chips it nearly doubled the card: 113px -> 195px measured. The
       Role badges next to it already answer the question people ask on a
       phone; the group list is a desk job. */
    .admin-root .users-data-table.table-cards td.col-groups,
    .admin-root .users-data-table.table-cards th.col-groups { display: none; }
    /* The spacer is a desktop layout device; a card has no columns to
       absorb slack between, so it would render as an empty card row. */
    .admin-root .users-data-table.table-cards td.col-spacer,
    .admin-root .users-data-table.table-cards th.col-spacer { display: none; }
    /* Cards have the vertical room, so show every role and drop the
       counter - "+1" is only worth reading when the badge it stands for is
       hidden. */
    .admin-root .users-data-table .role-cell-btn .role-more { display: none; }
    .admin-root .users-data-table.table-cards td.col-user {
        display: block;             /* name + email are the card header, not a label row */
        padding-right: 6.5rem;      /* room for the pinned status */
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .admin-root .users-data-table.table-cards td.col-user .user-name { font-weight: 700; }
    .admin-root .users-data-table.table-cards td.col-user::before { content: none; }
    .admin-root .users-data-table.table-cards td.col-status {
        position: absolute;
        top: 0.5rem;
        right: 0.75rem;
        border: 0;
        padding: 0;
        display: block;
    }
    .admin-root .users-data-table.table-cards td.col-status::before { content: none; }
    /* PIN THE KEBAB like the status chip above, bottom-right. As an ordinary
       card row the kebab took a whole row to itself - measured 54px of a
       167px card, because a touch target is legitimately 44px and it got its
       own line plus padding. Pinned, the card drops back to ~113px, which is
       what it measured before the actions column existed. The 44px target is
       untouched; only its position changes.
       The row is already position:relative - that is what pins the status. */
    .admin-root .users-data-table.table-cards td.col-actions {
        position: absolute;
        bottom: 0.35rem;
        right: 0.6rem;
        border: 0;
        padding: 0;
        display: block;
        width: auto;
    }
    .admin-root .users-data-table.table-cards td.col-actions::before { content: none; }
    /* The date shares that bottom line, so keep it clear of the button. */
    .admin-root .users-data-table.table-cards td.col-login { padding-right: 3rem; }
    .admin-root .users-data-table.table-cards td.col-login { display: flex; }
    .admin-root .users-data-table.table-cards .user-email { max-width: 60vw; }
}


/* Loading spinners on inactive/hidden subtabs must not animate forever -
   the infinite repaint churn breaks capture tooling and wastes cycles. */
.user-subtab:not(.active) .fa-spin,
.user-subtab:not(.active) .svg-icon.spin {
    animation-play-state: paused;
}


/* ---- /admin/users desktop table discipline: content min-widths forced
   635px into a 585px pane; fixed layout makes the email column actually
   truncate. Lives in the tail so no base rule can out-cascade it. ---- */
@media (min-width: 1024px) {
    .admin-root .users-data-table {
        table-layout: fixed;
        width: 100%;
        min-width: 0;
        box-sizing: border-box; /* width:100% includes any border - kills the 3px overflow */
        border-collapse: collapse;
    }

    /* Column arithmetic (border-box: px widths INCLUDE the side pads).
       User is HARD-CAPPED at clamp(240px, 55%, 420px) and never absorbs
       slack (the previous all-columns-specified layout relaxed User
       proportionally too: 542px measured at 1321). Slack goes to the
       REMAINING columns via designated AUTO tracks per band:

         checkbox   26px  (16px box + 2x4 pad)
         User      clamp(240px, 55%, 420px)  - the one capped column
         role      CENTERED; 140px when login shows, auto (slack) 1024-1279
         status    CENTERED; 110px when login shows, auto (slack) 1024-1279
         login     right-aligned AUTO >=1280 (absorbs all slack); hidden +
                   zero-collapsed 641-1279

       1024-1279 (login zero): Role + Status split the leftover equally as
       the two auto tracks - their chips stay visually centered at any
       width. >=1280: Role 140 / Status 110 fixed, Login (auto, right-
       aligned) absorbs the leftover against the table's right edge. In
       both bands the specified widths sum well under the narrowest pane,
       so no header can truncate. */
    /* Fixed layout guarantees columns fit; clip the 1px border-collapse
       rounding remainder so the wrapper never grows a scrollbar. */
    .admin-root .users-table-wrap {
        overflow-x: clip;
    }
    .admin-root .users-data-table .col-checkbox {
        width: 26px;
        min-width: 0;
        /* !important: the generic .checkbox-col carries padding !important;
           26px track minus 2x4px leaves exactly the 18px box. */
        padding: 6px 4px !important;
    }
    /* The Role chip is inline-block on the baseline: descender space made
       the row 47px. line-height:1 + middle-aligned chip = chip + padding. */
    .admin-root .users-data-table td.col-role {
        line-height: 1;
    }
    .admin-root .users-data-table td.col-role .role-badge {
        vertical-align: middle;
    }
    .admin-root .users-data-table .col-user {
        /* HARD 420px pin. NOT clamp()/min() with a percentage: fixed-layout
           track widths cannot resolve %-inside-math and Chrome degrades the
           whole declaration to AUTO (measured: User+Login split the slack
           362.5/362.5 at 1321). A plain px width + a designated per-band
           AUTO absorber column is the only mechanism that actually caps. */
        width: 420px;
        min-width: 0;
        overflow: hidden; /* clip, never spill into Role */
    }
    /* Ellipsis chain: .user-cell is flex, so .user-info must be allowed to
       shrink (min-width:0) or the name pushes the cell wide instead of
       truncating. */
    .admin-root .users-data-table .col-user .user-cell,
    .admin-root .users-data-table .col-user .user-info {
        min-width: 0;
    }
    .admin-root .users-data-table .col-user .user-info { flex: 1; }
    /* Role + Status hold short fixed-width tokens: CENTER header and cell
       (a left-aligned 94px chip stranded in a 155px track). In the
       login-hidden band these are the AUTO tracks that split the slack;
       widths are set in the band blocks below. */
    /* 200px is the measured worst case the role vocabulary can produce:
       "Maintenance Director" 144.5px + 2px gap + a "+13" pill 36.8px +
       16px cell padding = 199.3px. Do not shave it - at the old 190px the
       content box is 174px and a two-role cell wrapped, which is what made
       rows different heights. */
    .admin-root .users-data-table .col-role,
    .admin-root .users-data-table th.col-role {
        width: 200px;
        min-width: 0;
        padding-left: 8px;
        padding-right: 8px;
        overflow: hidden;
        text-align: center;
    }
    /* Status is pinned from 1024, NOT 1280. In the 1024-1279 band Login is
       hidden, so leaving Status auto makes it a SECOND absorber alongside
       the spacer and the two split the slack - measured 217px each at
       1096. Exactly one auto column, at every width. */
    .admin-root .users-data-table .col-status,
    .admin-root .users-data-table th.col-status {
        width: 110px;
        min-width: 0;
        overflow: hidden;
        text-align: center;
    }
    /* GROUPS IS THE ABSORBER. This table is table-layout:fixed, where a
       declared width only holds if some OTHER column is left auto to soak up
       the slack - pin every column and Chrome relaxes them ALL proportionally
       (measured: all five scaled by 1.77x). That job used to belong to an
       empty spacer column, which at 1920 meant 640px of nothing sitting
       between Last Login and the row kebab. Richard, 2026-08-19: "Table is
       kinda fugly", then "my table isnt going all the way to the right".
       Same mechanism, but the 640px now carries information: which Google
       groups the person is in, and which of them grant their role.
       No width here on purpose - auto IS the point. */
    .admin-root .users-data-table .col-groups,
    .admin-root .users-data-table th.col-groups {
        min-width: 0;
        overflow: hidden;
        white-space: nowrap;   /* clip a long list; never grow the row */
        text-align: left;
    }
    /* Row kebab column. Pinned, NOT auto - a second auto column would
       split the slack with the spacer. 60px here because at exactly 1024px
       this is still the TOUCH band, where the trigger is a full 44px
       target; it narrows to 42px from 1025px up (block below). */
    .admin-root .users-data-table .col-actions,
    .admin-root .users-data-table th.col-actions {
        width: 60px;
        padding-left: 8px;
        padding-right: 8px;
        text-align: right;
        overflow: visible;
    }
    /* Name and email share ONE line above the card band: .user-info is
       flattened so its two children become flex items of .user-cell. The
       wrapper itself MUST stay in the markup - deleting it regresses the
       mobile cards from 113.6px to 127.5px because name and email become
       separate flex lines there. */
    .admin-root .users-data-table .col-user .user-info {
        display: contents;
    }
    .admin-root .users-data-table .col-user .user-name {
        flex: 0 1 auto;
        min-width: 0;
    }
    .admin-root .users-data-table .col-user .user-email {
        flex: 1 1 auto;
        min-width: 0;
    }
    /* One line, one height: the extra badges collapse into the +N pill. */
    .admin-root .users-data-table .role-cell-btn {
        flex-wrap: nowrap;
    }
    .admin-root .users-data-table .role-cell-btn .role-extra {
        display: none;
    }
    /* Last Login stays right-aligned; >=1280 it is the AUTO slack column
       (width unset), pinning its content to the table's right edge. */
    .admin-root .users-data-table .col-login,
    .admin-root .users-data-table th.col-login {
        min-width: 0;
        text-align: right;
    }
    /* Status chip on one line-height so the row stays <=46px. */
    .admin-root .users-data-table td.col-status {
        line-height: 1;
    }
    .admin-root .users-data-table td.col-status .status-chip {
        vertical-align: middle;
    }

    /* Headers may never collide regardless of column math: clip them too. */
    .admin-root .users-data-table th {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .admin-root .users-data-table .user-name {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
}

/* While Last Login is display:none'd (hide rule near the top of this file:
   641-1279px), the column must also collapse to a ZERO track under the
   desktop fixed layout - a display:none'd th can still leave a reserved
   track (or be re-shown by a later toggle), which is how Role ended up
   overlapping the hidden Login column. Same media condition as the hide
   rule, intersected with the >=1024 fixed-layout regime; must stay AFTER
   the width:110px rule above (equal 0-3-0 specificity, source order wins). */
@media (min-width: 1024px) and (max-width: 1279px) {
    .admin-root .users-data-table .col-login {
        width: 0;
        min-width: 0;
        max-width: 0;
        padding: 0;
        border: 0;
    }
    /* Login gone: Role + Status are the AUTO tracks (no width) and split
       the pane's slack equally - centered chips stay centered. Nothing to
       declare; their base rules above set no width in this band. */
}

/* Login visible: Role/Status take their compact fixed widths and the
   right-aligned Login column is the AUTO track that absorbs all slack. */
/* Desktop pointer band. 1025px deliberately, NOT 1024: 44px is a TOUCH
   requirement and check-density enforces it at <=1024px - it failed this
   exact rule when it was written one pixel wider ("1024px: hub-menu-trigger
   26px < 44 touch target"). Anything that shrinks a control below the touch
   minimum belongs here, never in the 1024 block. */
@media (min-width: 1025px) {
    /* 26px, not the primitive's 28px, and ONLY in this table: the row pitch
       is set by the 26px avatar (26 + 6 + 6 padding + 1 border = 39px), so
       a 28px control silently became the tallest thing in the row and
       pushed every row to 42.4px - measured. Two pixels nobody can see,
       against the uniform row height that is the whole point of collapsing
       roles to +N. The .hub-btn--sm primitive is untouched everywhere else,
       and 26px is inside check-density's <=30px in-table ceiling. */
    .admin-root .users-data-table .col-actions .hub-menu-trigger {
        width: 26px;
        height: 26px;
        min-height: 0;
        /* THE 8px THAT ATE THE WIDTH. `.data-table td .hub-btn { margin-right:
           0.5rem }` (admin/admin-dashboard.css) DOES reach this trigger: its
           :last-child reset misses, because the last child of .hub-menu is the
           hidden .hub-menu-panel, not the button (display:none does not exempt
           an element from :last-child). With .hub-menu flexed just below, 26px
           + 8px margin overflows the cell's 26px content box and the flex item
           shrank to its automatic minimum: 18px svg + 2x1px used border = 20px.
           Measured 20x26. Both declarations are load-bearing. margin-right:0
           removes the overflow; flex:none stops any future squeeze in a 26px
           box that now has zero slack. Raising width to 28px does NOT fix this
           - measured 20x28, the shrink floor is unchanged. Fixed here, not on
           the shared `.data-table td .hub-btn` rule, which styles every button
           in every admin and mgmt data table. */
        margin-right: 0;
        flex: none;
    }
    /* .hub-menu is inline-block, so a 26px trigger still measured 28px:
       the extra 2px is descender space under the baseline of an inline-block
       wrapper, which then set the row height. Exactly the bug the Role chip
       had (see the col-role line-height:1 note above). Flex has no baseline
       to leave room under. */
    .admin-root .users-data-table .col-actions .hub-menu {
        display: flex;
        justify-content: flex-end;
        line-height: 0;
    }
    .admin-root .users-data-table .col-actions,
    .admin-root .users-data-table th.col-actions {
        width: 42px;
    }
}

/* SIDEBAR STAYS PUT (Richard, 2026-08-18: "if I scroll down, I lose the top
   of it... plus it keeps our footer (copywrite) statically visible either
   way").

   The design was already right and one property short of working. The aside
   is grid-row 1/span 2; the .admin-sidebar__panel inside it is
   position:absolute inset:0 with its own overflow-y:auto; and .sidebar-footer
   carries margin-top:auto to pin the copyright to the bottom. But a grid item
   STRETCHES to its row height, and row 2 grows with the page - measured
   1142px on /admin/sections/substitute_requests - so inset:0 made the panel
   document-tall instead of viewport-tall. Nothing could ever scroll inside
   it, because it was never shorter than its own contents. Scrolling the page
   then carried the whole rail away: nav item 1 went from y=84 to y=-158.

   align-self:start stops the stretch, the height caps it to the viewport, and
   sticky holds it there. The panel then does exactly what it was written to
   do - scroll its own overflow, copyright pinned at the bottom.

   position:sticky still establishes a containing block, so the panel keeps
   positioning against the aside, and overflow stays visible so Lever 1c's
   hover-widen overlay can still escape without reflowing the grid.

   min-width:1025px is the exact complement of the max-width:1024px block
   above, where the sidebar is a position:fixed off-canvas drawer and must NOT
   receive any of this. */
@media (min-width: 1025px) {
    .admin-sidebar {
        position: sticky;
        top: 0;
        align-self: start;
        height: 100vh;
        height: 100dvh; /* same dvh reasoning as .admin-shell min-height */
    }

    /* THE RAIL RISES WITH ITS FLYOUT.

       position:sticky ALWAYS creates a stacking context, z-index or not.
       That is the one side effect the sticky rule above brings with it, and
       it broke the hover flyout: .admin-sidebar__panel takes z-index:40 when
       the rail is hovered (both rail modes, further down this file), and
       that 40 used to be compared against .admin-main's z-index:1 at the
       root. Once the aside became a stacking context, 40 could no longer
       leave it - it only orders children WITHIN the rail - so the whole rail
       (z-index auto, i.e. 0) painted behind main and the slide-out went
       under the content.

       Raising the aside itself restores the old paint order. 40 matches the
       panel's own value, which also keeps the previous behaviour of the
       flyout covering the sticky header (z-index 20) rather than sliding
       under it.

       Deliberately unprefixed, unlike the two flyout rules: it applies to
       the auto-rail and the manual [data-sidebar-pref="collapsed"] rail
       alike, and is harmless at widths where no flyout happens because the
       rail has its own grid column and overlaps nothing. */
    .admin-sidebar:hover,
    .admin-sidebar:focus-within {
        z-index: 40;
    }
}

@media (min-width: 1280px) {
    /* Last Login holds a short date, so pin it too. Every visible column is
       now pinned and .col-spacer is the sole auto absorber - which is what
       lets User keep its measured 420px instead of ballooning to 1112px to
       display a 380px name-and-email.

       Role (200px) and Status (110px) are pinned from 1024 in the block
       above, not here: in the 1024-1279 band Login is hidden, and leaving
       Status auto there made it a second absorber that split the slack with
       the spacer (measured 217px each at vw 1096). */
    .admin-root .users-data-table .col-login,
    .admin-root .users-data-table th.col-login {
        width: 130px;
    }
}


/* ==========================================================================
   BOTTOM CHROME SYNC (mobile) - the footer is locked to --footer-locked-h
   (+notch) below 768px, defined in shared/footer.css, so every offset that
   assumed the 40px desktop var re-anchors here.

   These used to be four literal 24px values kept in step with the footer by
   hand. They now read the property, so the height is changed in one place.
   ========================================================================== */
@media (max-width: 768px) {
    .flow-actions {
        bottom: calc(var(--footer-locked-h, 30px) + env(safe-area-inset-bottom, 0px));
    }

    .form-action-bar {
        bottom: calc(var(--footer-locked-h, 30px) + env(safe-area-inset-bottom, 0px));
    }

    .main-content {
        padding-bottom: calc(var(--footer-locked-h, 30px) + env(safe-area-inset-bottom, 0px) + 12px);
    }

    /* Re-anchor the bar clearance to the locked mobile footer too -
       the base rule assumes the 40px desktop footer. !important matches the
       base .has-action-bar rule (see section-shell.css: safety invariant). */
    .has-action-bar {
        padding-bottom: calc(var(--footer-locked-h, 30px) + var(--action-bar-height, 73px) + env(safe-area-inset-bottom, 0px) + 12px) !important;
    }
}

/* ==========================================================================
   FORM-PAGE field grid (see .form-grid in enterprise-components.css) collapses
   to a single column below the desktop breakpoint. Lives in the responsive
   tail so this override out-ranks the base 2-up rule regardless of source
   order. 1023px: >= 1024px stays 2-up per the form-page standard.
   ========================================================================== */
@media (max-width: 1023px) {
    .form-grid {
        grid-template-columns: 1fr;
    }
}

/* ==========================================================================
   EDGE-TO-EDGE ADMIN TABLES — Levers 2/3/4 (all @media live here, the tail).
   Levers compose through ONE gutter token (--admin-content-gutter) and ONE
   sidebar token (--admin-sidebar-w). See enterprise-components.css for base.
   ========================================================================== */

/* LEVER 2 — symmetric gutter step-downs (:root only; 600 AFTER 1280). */
@media (max-width: 1280px) { :root { --admin-content-gutter: 16px; } }
@media (max-width: 600px)  { :root { --admin-content-gutter: 12px; } }

/* LEVER 4 — density: horizontal padding NOTCH only (vertical padding + line-
   height untouched, so tbody row height is preserved by construction). Font
   notch is OFF (locked). Not applied to card-flip ::before labels/badges. */
@media (min-width: 641px) and (max-width: 1439px) {
    .register-table thead th { padding-left: 8px; padding-right: 12px; }
    .register-table tbody td { padding-left: 8px; padding-right: 8px; }
}

/* LEVER 3 — REVERSED 2026-08-14 (NAV-07, Richard's decision, recorded on the
   board as a deliberate reversal — not drift).

   This block used to full-bleed the WHOLE register (tabs + toolbar + table
   panel + pagination) out to main.admin-main's inner edges via a negative
   margin, for maximum table width. Richard looked at the live page and chose
   breathing room instead: the register now sits inside .admin-main-content's
   padding like the page title does, so it no longer runs flush against the
   sidebar on one side and the window edge on the other.

   BOTH halves had to go, not just the margin. The padding-inline on the
   tabs/toolbar/pager rows existed ONLY to cancel the bleed for non-table rows.
   Left behind with the margin zeroed it would double-inset them one gutter past
   the table, replacing an edge complaint with a fresh internal stagger.

   The inner paddings are deliberately untouched: .hub-filterbar's 12px and the
   7-8px cell padding are correct, and changing them measured WORSE (spread
   3.5px -> 7px). */
@media (min-width: 1025px) {
    div.admin-main-content > .admin-register {
        margin-inline: 0;
    }
}

/* ==========================================================================
   LEVER 1 — SIDEBAR RAIL (token-driven; grid shell). The rail flips
   --admin-sidebar-w -> --admin-rail-w (72px) via [data-sidebar-pref] (manual
   3-state) or the auto-rail media query (<=1366 with NO stored pref). Labels
   CLIP (screen-reader names survive — never display:none); hover/focus reveals
   the panel as a non-reflowing overlay. Real classes: item=.admin-nav-link,
   label=.admin-nav-link>span, section=.nav-group-heading, brand=.sidebar-title,
   active=.admin-nav-link.active. Desktop only (>=1025 = DESKTOP_MIN).
   ========================================================================== */
@media (min-width: 1025px) {
    /* 1a — width, manual override */
    html[data-sidebar-pref="expanded"]  .admin-shell { --admin-sidebar-w: 280px; }
    html[data-sidebar-pref="collapsed"] .admin-shell { --admin-sidebar-w: var(--admin-rail-w); }

    /* 1b/1d — rail skin for the COLLAPSED preference */
    /* NAV-05: neutralise the EXPANDED-mode edge-bleed pairing as a unit. In the
       expanded panel `.admin-nav-link` bleeds to the sidebar edge via
       margin-left:-10px paired with width:calc(100% + 6px). Left in place in the
       rail, that pairing puts the box at x 2..56, so `justify-content:center`
       centres the glyph on 29 — while `.nav-expandable-trigger` (rule 5b, which
       already zeroes both) centres on 36. Hence the 7px split.
       Both properties must go together: zeroing only the margin gives 39, only
       the width gives 26. Neither alone reaches the rail centre of 36. */
    html[data-sidebar-pref="collapsed"] .admin-sidebar .admin-nav-link {
        justify-content: center; padding-left: 0; padding-right: 0; min-height: 44px;
        margin-left: 0; margin-right: 0; width: 100%;
    }
    html[data-sidebar-pref="collapsed"] .admin-sidebar .admin-nav-link > span:not(.svg-icon):not(.sidebar-badge),
    html[data-sidebar-pref="collapsed"] .admin-sidebar .nav-group-heading > span,
    html[data-sidebar-pref="collapsed"] .admin-sidebar .sidebar-title,
    html[data-sidebar-pref="collapsed"] .admin-sidebar .sidebar-footer-content {
        position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
        overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
    }
    html[data-sidebar-pref="collapsed"] .admin-sidebar .nav-exp-caret,
    html[data-sidebar-pref="collapsed"] .admin-sidebar .nav-group-chevron,
    html[data-sidebar-pref="collapsed"] .admin-sidebar .sidebar-badge { display: none; }
    html[data-sidebar-pref="collapsed"] .admin-sidebar .admin-nav-link.active {
        box-shadow: inset 3px 0 0 var(--gold);
    }

    /* auto rail — identical skin, laptop band, ONLY when no stored preference
       (so no-JS still rails per A19). */
    @media (max-width: 1366px) {
        html:not([data-sidebar-pref]) .admin-shell { --admin-sidebar-w: var(--admin-rail-w); }
        /* NAV-05 — same unit-neutralisation as the collapsed-pref rail above. */
        html:not([data-sidebar-pref]) .admin-sidebar .admin-nav-link {
            justify-content: center; padding-left: 0; padding-right: 0; min-height: 44px;
            margin-left: 0; margin-right: 0; width: 100%;
        }
        html:not([data-sidebar-pref]) .admin-sidebar .admin-nav-link > span:not(.svg-icon):not(.sidebar-badge),
        html:not([data-sidebar-pref]) .admin-sidebar .nav-group-heading > span,
        html:not([data-sidebar-pref]) .admin-sidebar .sidebar-title,
        html:not([data-sidebar-pref]) .admin-sidebar .sidebar-footer-content {
            position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
            overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
        }
        html:not([data-sidebar-pref]) .admin-sidebar .nav-exp-caret,
        html:not([data-sidebar-pref]) .admin-sidebar .nav-group-chevron,
        html:not([data-sidebar-pref]) .admin-sidebar .sidebar-badge { display: none; }
        html:not([data-sidebar-pref]) .admin-sidebar .admin-nav-link.active {
            box-shadow: inset 3px 0 0 var(--gold);
        }

        /* 1c(auto) — hover / keyboard reveal, AUTO-RAIL ONLY. Bounded to
           <=1366 so a full-width auto sidebar (>1366) never reveals (Bug 1:
           no shadow / z-index / width / caret change at full width). Same
           auto-rail prefix as the clip rule => equal specificity, later in
           source => the un-clip wins over the clip (Bug 3). Icons (.svg-icon)
           are never clipped, so they stay out of the un-clip (Bug 2). */
        html:not([data-sidebar-pref]) .admin-sidebar:hover .admin-sidebar__panel,
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .admin-sidebar__panel {
            width: 280px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18); z-index: 40;
        }
        html:not([data-sidebar-pref]) .admin-sidebar:hover .admin-nav-link > span:not(.svg-icon),
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .admin-nav-link > span:not(.svg-icon),
        html:not([data-sidebar-pref]) .admin-sidebar:hover .nav-group-heading > span,
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .nav-group-heading > span,
        html:not([data-sidebar-pref]) .admin-sidebar:hover .sidebar-title,
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .sidebar-title,
        html:not([data-sidebar-pref]) .admin-sidebar:hover .sidebar-footer-content,
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .sidebar-footer-content {
            position: static; width: auto; height: auto; margin: 0;
            clip: auto; clip-path: none; overflow: visible; white-space: nowrap; border: 0;
        }
        html:not([data-sidebar-pref]) .admin-sidebar:hover .admin-nav-link,
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .admin-nav-link { justify-content: flex-start; }
        html:not([data-sidebar-pref]) .admin-sidebar:hover .nav-exp-caret,
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .nav-exp-caret,
        html:not([data-sidebar-pref]) .admin-sidebar:hover .nav-group-chevron,
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .nav-group-chevron,
        html:not([data-sidebar-pref]) .admin-sidebar:hover .sidebar-badge,
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .sidebar-badge { display: inline-flex; }
    }

    /* 1c — hover / keyboard reveal, COLLAPSED PREFERENCE ONLY. A full-width
       sidebar (expanded pref, or auto >1366) must NEVER reveal — no shadow,
       z-index, width, caret or label change (Bug 1). Widen the absolute panel
       over main (no reflow — grid column is unchanged) and un-clip labels.
       Same [pref] prefix as the clip rule => equal specificity, later in
       source => the un-clip wins over the clip (Bug 3). Icons (.svg-icon) are
       never clipped, so they stay out of the un-clip (Bug 2). focus-within
       adds the keyboard path. */
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .admin-sidebar__panel,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .admin-sidebar__panel {
        width: 280px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18); z-index: 40;
    }
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .admin-nav-link > span:not(.svg-icon),
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .admin-nav-link > span:not(.svg-icon),
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .nav-group-heading > span,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .nav-group-heading > span,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .sidebar-title,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .sidebar-title,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .sidebar-footer-content,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .sidebar-footer-content {
        position: static; width: auto; height: auto; margin: 0;
        clip: auto; clip-path: none; overflow: visible; white-space: nowrap; border: 0;
    }
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .admin-nav-link,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .admin-nav-link { justify-content: flex-start; }
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .nav-exp-caret,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .nav-exp-caret,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .nav-group-chevron,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .nav-group-chevron,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .sidebar-badge,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .sidebar-badge { display: inline-flex; }
}

/* ============================================================
   RAIL FIX SET (2026-08) — consolidated, supersedes prior blocks.
   Applies to BOTH railed states: [data-sidebar-pref="collapsed"]
   (any desktop width) AND the auto rail (no pref, <=1366). Tightens
   the 72px rail: collapse section headings (5a), center the section-
   trigger icons on x=36 (5b), hide icon-less sub-leaf rows — flyout
   only (5c), warm the non-section glyphs (6), and make the active
   item legible: black glyph on the gold pill with NO redundant gold
   inset bar (4). The panel-widen hover is already scoped to these
   states, so nothing reveals at full width (1). */
@media (min-width: 1025px) {
    /* ---------- COLLAPSED PREFERENCE (any desktop width) ---------- */
    /* 5a — section headings collapse to nothing; items stay reachable w/o the toggle */
    html[data-sidebar-pref="collapsed"] .admin-sidebar .nav-group-heading { display: none; }
    html[data-sidebar-pref="collapsed"] .admin-sidebar .nav-group.closed .nav-group-items { display: block; }
    /* 5c — icon-less sub-leaves belong only in the flyout */
    html[data-sidebar-pref="collapsed"] .admin-sidebar .nav-expandable > .nav-submenu { display: none; }
    /* 5b — the section trigger extends past the edge (margin-left:-10px + wider);
       zero that in the rail so its icon centers on x=36 like the top-level items */
        /* NAV-08 (Richard, 2026-08-14): in the rail the group HEADINGS are display:none,
       but the spacing that framed them is not - .admin-nav is a flex column with
       gap:4px and each .nav-group adds margin-top:8px, so a 12px void survives
       exactly where an invisible "People" / "Configuration" / "System" label sits.
       Read as ragged spacing between icons with nothing to explain it. Both must be
       zeroed together: dropping only the margin leaves 4px, only the gap leaves 8px.
       Expanded mode is untouched - the gaps are meaningful there because the headings
       render. Measured: gaps 12/0 mixed -> 0 uniform, rail height 388 -> 352. */
html[data-sidebar-pref="collapsed"] .admin-sidebar .admin-nav { row-gap: 0; }
html[data-sidebar-pref="collapsed"] .admin-sidebar .nav-group { margin-top: 0; }
html[data-sidebar-pref="collapsed"] .admin-sidebar .nav-expandable-trigger {
        width: 100%; margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0;
    }
    /* 6 — non-active, non-section glyphs warm (was cool slate #6B7280) */
    html[data-sidebar-pref="collapsed"] .admin-sidebar .admin-nav-link:not(.active):not([data-palette]):not([data-section]) .svg-icon {
        color: var(--ink-mute, #6F6A5C);
    }
    /* 4 — active = black glyph on the gold pill; the pill IS the indicator (drop the gold-on-gold bar) */
    html[data-sidebar-pref="collapsed"] .admin-sidebar .admin-nav-link.active { box-shadow: none; }
    html[data-sidebar-pref="collapsed"] .admin-sidebar .admin-nav-link.active .svg-icon { color: var(--btn-primary-text, #000000); }
    /* flyout — reveal the expanded group's sub-leaves + the section headings */
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .nav-expandable.expanded > .nav-submenu,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .nav-expandable.expanded > .nav-submenu { display: block; }
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .nav-group-heading,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .nav-group-heading { display: flex; }
    /* flyout — restore the trigger's normal insets (edge-bleed pill + caret gutter) */
    html[data-sidebar-pref="collapsed"] .admin-sidebar:hover .nav-expandable-trigger,
    html[data-sidebar-pref="collapsed"] .admin-sidebar:focus-within .nav-expandable-trigger {
        width: calc(100% + 10px); margin-left: -10px; margin-right: 4px; padding-left: 24px;
    }

    /* ---------- AUTO RAIL (no stored pref, laptop band) ---------- */
    @media (max-width: 1366px) {
        html:not([data-sidebar-pref]) .admin-sidebar .nav-group-heading { display: none; }
        html:not([data-sidebar-pref]) .admin-sidebar .nav-group.closed .nav-group-items { display: block; }
        html:not([data-sidebar-pref]) .admin-sidebar .nav-expandable > .nav-submenu { display: none; }
            html:not([data-sidebar-pref]) .admin-sidebar .admin-nav { row-gap: 0; }
    html:not([data-sidebar-pref]) .admin-sidebar .nav-group { margin-top: 0; }
        html:not([data-sidebar-pref]) .admin-sidebar .nav-expandable-trigger {
            width: 100%; margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0;
        }
        html:not([data-sidebar-pref]) .admin-sidebar .admin-nav-link:not(.active):not([data-palette]):not([data-section]) .svg-icon {
            color: var(--ink-mute, #6F6A5C);
        }
        html:not([data-sidebar-pref]) .admin-sidebar .admin-nav-link.active { box-shadow: none; }
        html:not([data-sidebar-pref]) .admin-sidebar .admin-nav-link.active .svg-icon { color: var(--btn-primary-text, #000000); }
        html:not([data-sidebar-pref]) .admin-sidebar:hover .nav-expandable.expanded > .nav-submenu,
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .nav-expandable.expanded > .nav-submenu { display: block; }
        html:not([data-sidebar-pref]) .admin-sidebar:hover .nav-group-heading,
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .nav-group-heading { display: flex; }
        html:not([data-sidebar-pref]) .admin-sidebar:hover .nav-expandable-trigger,
        html:not([data-sidebar-pref]) .admin-sidebar:focus-within .nav-expandable-trigger {
            width: calc(100% + 10px); margin-left: -10px; margin-right: 4px; padding-left: 24px;
        }
    }
}

/* 1e — reduced motion */
@media (prefers-reduced-motion: reduce) {
    .admin-shell, aside.admin-sidebar, .admin-sidebar__panel { transition: none; }
}

/* ============================================================
   Frozen-header pass — mobile reset (Step 8) + motion (Step 9).
   The >=641px frozen-header/bounded-scroll rules live in
   enterprise-components.css; everything here undoes them below
   641px so the .table-cards phone flip is pixel-for-pixel
   unchanged. Defensive AND load-bearing: it also preserves
   horizontal scroll for WIDE non-card tables (never overflow:
   visible on x, which would drop that). Lives in the responsive
   tail so it always out-cascades a stray base rule. */
@media (max-width: 640px) {
    .table-scroll--panel {
        max-height: none;
        min-height: 0;
        overflow-x: auto;      /* keep wide-table horizontal scroll when .table-cards is absent */
        overflow-y: visible;   /* let cards scroll with the page */
        scroll-padding-top: 0;
        margin-bottom: 0;
    }
    .table-scroll--panel table.register-table {
        border-collapse: collapse;
        box-shadow: none;
    }
    .table-scroll--panel table.register-table > thead > tr > th {
        position: static;
        box-shadow: none;
    }
    .table-scroll--panel table.register-table > tbody > tr > td {
        box-shadow: none;
    }
}

/* Smooth internal scroll unless the user opts out. */
@media (prefers-reduced-motion: no-preference) {
    .table-scroll--panel { scroll-behavior: smooth; }
}
@media (prefers-reduced-motion: reduce) {
    .table-scroll--panel { scroll-behavior: auto; }
}

/* ==========================================================================
   WORK ORDER DETAIL — responsive layout (base is mobile-first in
   enterprise-components.css). DESKTOP >=1024 = two columns + sticky right rail;
   <1024 = single column (reordered) + pinned bottom action bar.
   ========================================================================== */

/* DESKTOP: two columns, sticky right rail (~340px). */
@media (min-width: 1024px) {
    .wo-grid {
        display: grid;
        grid-template-columns: minmax(0, 1fr) 340px;
        gap: var(--space-4) var(--space-6);
        align-items: start;
    }
    /* grid-row on BOTH, deliberately. .wo-col-side comes first in the DOM but
       belongs in column 2, and grid's default sparse auto-placement never walks
       the cursor backwards - so the side rail claimed row 1/column 2 and the
       main column, pinned to column 1, was pushed down to row 2. That left a
       799px hole above the ticket title on every work order (measured: .wo-grid
       at y=88, .wo-col-main at y=887).

       Naming the row fixes it and is also DOM-order-proof: whoever reorders the
       two columns next cannot bring the hole back. */
    .wo-col-main { grid-column: 1; grid-row: 1; min-width: 0; }
    .wo-col-side {
        grid-column: 2;
        grid-row: 1;
        align-self: start;
        position: sticky;
        top: var(--space-4);
    }
    .wo-rail { display: flex; flex-direction: column; gap: var(--space-4); }
    /* the mobile bar never floats on desktop */
    .wo-actionbar-wrap { display: none !important; }
}

/* TABLET / MOBILE: single column + pinned bottom action bar. display:contents
   promotes the column wrappers' children so they interleave in one order:
   title -> details -> description -> activity; the rail forms relocate to the
   bar. */
@media (max-width: 1023.98px) {
    .wo-grid { display: flex; flex-direction: column; gap: var(--space-4); }
    .wo-col-main,
    .wo-col-side { display: contents; }

    .wo-titlebar      { order: 1; }
    .wo-flash         { order: 2; }
    .wo-card--details { order: 3; }
    .wo-description   { order: 4; }
    .wo-activity      { order: 5; }
    .wo-rail          { display: none; }

    /* content clears the fixed bar (bottom-chrome model) */
    .wo-detail .wo-grid { padding-bottom: 84px; }

    .wo-actionbar-wrap {
        display: block;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 30;
        background: white;
        border-top: 1px solid var(--gray-300);
        box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.08);
    }
    .wo-actionbar { padding: var(--space-2) var(--admin-content-gutter); }
    .wo-actionbar .wo-bar-btn { height: 46px; }
    .wo-sheet {
        padding: var(--space-3) var(--admin-content-gutter);
        border-bottom: 1px solid var(--gray-200);
        background: var(--gray-50);
        max-height: 60vh;
        overflow-y: auto;
    }
    .wo-sheet .wo-btnstack { margin-top: var(--space-2); }
}

/* PHONE breadcrumb: the 4-part trail crushes to "H. M. W. D" at phone widths.
   Show ONLY the server-computed .breadcrumb-back chip (the current page's
   real PARENT - EnterpriseHeader picks the deepest linked crumb that is NOT
   the page itself, so it can never self-link; the old a:last-of-type
   heuristic rendered "< Budgets" ON /management/budgets and went nowhere).
   Full trail returns >=641px. Shared: every admin/management page. */
@media (max-width: 640px) {
    .admin-header .breadcrumb > * { display: none; }
    .admin-header .breadcrumb > .breadcrumb-back {
        display: inline-flex;
        align-items: center;
        max-width: 70vw;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .admin-header .breadcrumb > .breadcrumb-back::before {
        content: "‹\00a0";
        font-size: 1.15em;
        line-height: 1;
    }
}

/* ==========================================================================
   TOUCH-TARGET FLOOR FOR THE CONSOLE CHROME (2026-09-02 sizing audit)
   ==========================================================================
   The brief's rule is "44px IS A TOUCH REQUIREMENT, NOT A DESKTOP ONE", and
   the platform was not keeping it: measuring all 67 phone-reachable pages at a
   true 375px viewport found 1,923 interactive elements under 44px. THREE
   declarations account for 791 of them (41.1%) - the drawer nav rows, their
   group headings, and the brand link - and one more, the back chip, is the
   ONLY in-page back affordance on every console page at this width.

   NOTHING HERE CHANGES WHAT IS PAINTED ON THE PAGE YOU ARE LOOKING AT. The
   nav rows live inside an off-canvas drawer that already scrolls, so 14 links
   growing 4px each costs 56px of drawer scroll and zero page height. The back
   chip keeps its 13px type and its exact painted size - only its hit
   rectangle grows, via the floor plus centring, which is the standard
   hit-area expansion.

   Deliberately NOT here: .role-cell-btn (18px), the Student Directory
   checkboxes and copy buttons, and the seven inline form-input copies. Those
   need markup or an owner decision, not a floor - see the audit.
   ========================================================================== */
@media (max-width: 1024px), (pointer: coarse) {
    .admin-sidebar .admin-nav-link,
    .admin-sidebar .nav-group-heading,
    .admin-sidebar .sidebar-brand-link {
        min-height: var(--tap-target, 2.75rem);
    }

    /* The chip stays 13px and exactly as wide as its text; only the
       rectangle a thumb can land on gets bigger. */
    .admin-header .breadcrumb > .breadcrumb-back {
        min-height: var(--tap-target, 2.75rem);
    }
}


/* Row-click registers (hub-rowlink.js): a row carrying data-href navigates,
   so it says so on hover. The row is a shortcut - the title cell keeps a real
   anchor for keyboard and screen readers - so no focus styling belongs here:
   focus lands on the anchor, which already has it. */
.register-table tbody tr[data-href],
.data-table tbody tr[data-href],
.bg-mgmt-table tbody tr[data-href] {
    cursor: pointer;
}

/* ==========================================================================
   TOUCH TARGET CORRECTIONS (mobile audit, 2026-09-02)
   Every rule here is touch-only: a mouse desktop is byte-identical to before.
   ========================================================================== */
@media (max-width: 1024px), (pointer: coarse) {
    /* The users toolbar floors these at 44px only below 640px (_responsive.css),
       so phones were right and iPads were not - while the Role button beside
       them lifts at 1024. Three controls, one row, two different heights. */
    .admin-root .search-input,
    .admin-root .filter-select {
        min-height: var(--tap-target, 2.75rem);
    }

    /* Grow the chip's hit area without growing the chip. -8px keeps the target
       inside the chip's own row; -13px would reach 44px but overhang the chip
       and steal taps from the line below. 34px is short of 44 but is what the
       layout can honestly give, and the same filter can always be un-ticked in
       its dropdown, which IS 44px. */
    .hub-chip__x::after {
        content: '';
        position: absolute;
        inset: -8px;
    }
}

/* Phone chips (2026-09-07): with the filter controls collapsed behind the
   Filters button, the chip's x is the ONLY on-screen removal control, so it
   gets the lawful 44px. 36px chips on an 8px gap = a 44px pitch, so the -13px
   hit box (18 + 26 = 44) of one row meets the next row's edge without
   overlapping it. Above 640px the -8px / 34px compromise above still applies. */
@media (max-width: 640px) {
    .hub-filterbar__chips { gap: 8px; }
    .hub-chip { height: 36px; font-size: 13px; }
    .hub-chip__x::after { inset: -13px; }
}

@media (max-width: 1024px), (pointer: coarse) {
    /* The register row "Open" link. On a phone the register becomes cards and
       this link is the ONLY route into a record - there is no row-tap
       fallback, so a miss does nothing at all. Measured 26px tall against a
       44px requirement.

       Scoped to table cells deliberately: .module-action-link is also used for
       prose back-links inside <p> ("<- All sections", "<- Back to Management"),
       and flooring the class outright would put a 44px line box in the middle
       of a running sentence. */
    :is(.admin-root, .hub-page) td .module-action-link,
    :is(.admin-root, .hub-page) .register-table .module-action-link {
        min-height: var(--tap-target, 2.75rem);
        padding-left: var(--space-2, 8px);
        padding-right: var(--space-2, 8px);
        justify-content: flex-end;
    }

    /* Two hand-styled selects that never got a height. The fuel one submits its
       form on change, so a mis-tap reloads the register scoped to the wrong
       person rather than merely missing. Deliberately NOT .hub-field-row, which
       would set 36px on desktop beside 28px buttons and create a new mismatch
       there. */
    .hub-touch-select {
        min-height: var(--tap-target, 2.75rem);
    }
}

@media (max-width: 640px) {
    /* .users-primary-actions is `display:flex` with `flex-shrink: 0` and no
       wrap (admin-dashboard.css), so its three buttons need ~420px and get a
       ~351px column on an iPhone - the gold "Add User", the page's primary
       action, ran off the right edge and could only be reached by dragging the
       pane sideways. Wrapping keeps all three reachable and hides nothing.
       This never showed up in the "no sideways scroll" gate because the
       overflow happens inside a scrolling content pane, not on the page. */
    .admin-root .users-primary-actions {
        flex-wrap: wrap;
        flex-shrink: 1;
        justify-content: flex-end;
    }
}

/* Student tick-boxes (.sd-cb-hit): the label around a selection checkbox.
   Base: a tight centring wrapper - desktop mouse looks exactly as before.
   Touch: a 44px target, ANY layout - the old 44px rule lived only in the
   phone .table-cards branch, so an iPad showing the table got the bare 16px
   box. The painted box stays 16px; only the invisible target grows. Missing
   these boxes is not harmless: on both student pages, nothing ticked means
   "print the whole filtered list". */
.sd-cb-hit {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin: 0;
    cursor: pointer;
}
@media (max-width: 1024px), (pointer: coarse) {
    .sd-cb-hit {
        width: var(--tap-target, 2.75rem);
        height: var(--tap-target, 2.75rem);
    }
}

/* Phone sort (hub-filterbar__sortsel): the filter bar's stand-in for the
   header row, which card mode hides below 641px. Hidden wherever headers are
   visible - two sort controls for one table is one too many. 16px font on
   purpose: smaller text makes iPhone Safari zoom the page on focus and not
   zoom back. Height matches the bar's other lifted controls. */
.hub-filterbar__sortsel { display: none; }
@media (max-width: 640px) {
    .hub-filterbar__sortsel {
        display: block;
        width: 100%;
        height: var(--btn-h-touch, 44px);
        padding: 0 10px;
        font-size: 16px;
        border: 1px solid var(--gray-400, #c8c6c4);
        border-radius: 8px;
        background: var(--surface, #fff);
        color: var(--ink, #201f1e);
    }
}

/* The header bell's count (wired 2026-09-03; it was dead chrome before). A
   small red count pinned to the icon button's corner - the .hub-menu trigger
   underneath supplies all behaviour. aria-hidden: the trigger's aria-label
   speaks the number. */
.hub-alerts { position: relative; }
.hub-alerts__badge {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 9px;
    background: var(--danger, #A4262C);
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    pointer-events: none;
}

/* ==========================================================================
   iOS FOCUS ZOOM, EVERY MANAGEMENT FORM
   ==========================================================================
   .admin-root input[type=...] sets font-size: var(--text-base) = 14px, and
   iPhone Safari zooms into any field under 16px on focus and does not zoom
   back. One tap on any management form and the rest of the visit is spent at
   ~130% with the page scrolled sideways.

   The base rule scores (0,2,1), which beats .form-input at (0,1,0) - so the
   shared field class cannot fix this and neither can adding it to a field.
   Only a rule at this specificity can, which is what this is.

   PHONE WIDTHS ONLY. Desktop management screens keep 14px and their density:
   the media query makes it impossible for this to move a desktop pixel.

   The Hub already pays this exact 16px tax by hand in .hub-filterbar__search,
   .hub-filterbar__sortsel and .hub-daterange input[type="date"]. This is the
   same decision made once for every management form, rather than one at a
   time as somebody trips over each.

   select is included: iOS zooms an undersized <select> on focus too, and the
   base rule above does not cover it.
   ========================================================================== */
@media (max-width: 767px) {
    .admin-root .form-control,
    .admin-root input[type="text"],
    .admin-root input[type="search"],
    .admin-root input[type="email"],
    .admin-root input[type="number"],
    .admin-root input[type="date"],
    .admin-root input[type="datetime-local"],
    .admin-root input[type="time"],
    .admin-root input[type="password"],
    .admin-root input[type="tel"],
    .admin-root input[type="url"],
    .admin-root select,
    .admin-root textarea {
        font-size: 16px;
    }
}

/* ==========================================================================
   iOS FOCUS ZOOM - THE FLOOR, EVERY CONTROL, EVERY SURFACE
   ==========================================================================
   The block above fixed this for management forms at phone widths. Richard
   asked for no iPhone left with the problem, and it leaves three holes:

   1. SCOPE. It is .admin-root-scoped, so the vanilla Hub section pages -
      work orders, fleet, reimbursements, budgets, substitute requests, trip
      inspections, checks, receipts - never match it at all.
   2. WIDTH. It stops at 767px. An iPhone held in LANDSCAPE is up to 932 CSS
      px wide, so the fix switches itself off at exactly the moment the
      on-screen keyboard is covering most of the page.
   3. INLINE STYLES. Thirteen live controls carry style="font-size:14px"
      (or 13px, or 12px) inline - the reimbursement queue's payment method
      and reference fields on every row, the purchase-order register search,
      the student-import textarea, and src/Components/YearSelect.php, which
      renders into five pages at once. An inline style beats ANY selector, so
      no ordinary rule can reach them.

   Hole 3 is why this is !important. It is a floor; a floor that anything can
   step through is not a floor. Fixing the thirteen by hand is what has been
   happening - ten other files already set 16px inline, one at a time, as
   somebody tripped over each - and it has not converged.

   THE CONDITION is this codebase's own touch condition, already used for tap
   targets in enterprise-components.css. Phones and tablets match. A desktop
   with a mouse does not, so desktop density does not move - and a laptop
   with a touchscreen reports pointer: fine, because the PRIMARY pointer is
   its trackpad, so it is untouched too. The max-width arm is what catches an
   iPad driven by a trackpad, where the pointer is fine but Safari still
   zooms.

   EXCLUSIONS. Buttons, checkboxes, radios, ranges, colour and file pickers:
   iOS only zooms a control you can type into, and resizing that furniture
   would be change for its own sake. .flow-input-number is excluded because
   it is deliberately BIGGER (17px tabular digits) and a floor must not
   pull it down to meet itself.

   ONE KNOWN TRIM, accepted on purpose: the section-icon <select> in
   public/admin/partials/modals.php is 1.2rem inline so its EMOJI read
   clearly, and on touch this floor takes it to 16px. It is a super-admin
   dropdown used a handful of times a year; an ID-keyed exception in
   shared CSS would cost more than it buys. Any OTHER control that must
   stay above 16px should be excluded the way .flow-input-number is.
   ========================================================================== */
@media (max-width: 1024px), (pointer: coarse) {
    input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]):not([type="image"]):not(.flow-input-number),
    select,
    textarea {
        font-size: 16px !important;
    }
}
