/* ==========================================================================
   Trust Blue + Action Orange — Design System
   Martial Arts School Management Application
   ========================================================================== */

/* Note: Plus Jakarta Sans font is loaded via <link> in base templates for better performance */

/* ==========================================================================
   1. Design Tokens (CSS Custom Properties)
   ========================================================================== */

:root {
    color-scheme: dark;

    /* Background Colors */
    --bg-body: #0f172a;
    --bg-sidebar: #111827;
    --bg-card: #1e293b;
    --bg-card-hover: #334155;
    --bg-input: #0f172a;
    --bg-hover: rgba(255, 255, 255, 0.05);
    --bg-elevated: #243044;

    /* Border Colors */
    --border-color: rgba(148, 163, 184, 0.12);
    --border-color-light: rgba(148, 163, 184, 0.2);

    /* Text Colors */
    --text-primary: #f1f5f9;
    --text-secondary: #94a3b8;
    /* Muted is still text, and has to be read: 5.01 on a card, 4.61 on a table
       header, 4.55 on an elevated panel. It was #64748b, which measured 3.07,
       2.83 and 2.79 — and .text-muted is the most-used class on the site. */
    --text-muted: #8b98ad;
    --text-link: #60a5fa;
    /* A danger FOREGROUND, the twin of --text-link. --accent-danger is a
       background and measures 3.89 as a label on a panel here. */
    --text-danger: #f87171;
    /* The rest of the accents work unchanged as labels on a dark ground;
       named anyway so .btn-outline-* names a foreground everywhere and the
       next palette change is measured in one place. */
    --text-success: #22c55e;
    --text-warning: #eab308;
    --text-info: #38bdf8;
    --text-purple: #a78bfa;

    /* Accent Colors */
    --accent-primary: #2563eb;
    --accent-primary-hover: #3b82f6;
    /* A filled primary button under the pointer. Darker, not lighter: white
       on --accent-primary-hover is 3.68, on this 6.70. */
    --accent-primary-pressed: #1d4ed8;
    --accent-primary-rgb: 37, 99, 235;
    /* Action Orange: the fill of .btn-action, the one warm thing on a staff
       screen. The stylesheet was titled for it and no template used it. On a
       dark panel the bright orange carries a near-black label (6.4:1) and
       lightens under the pointer; white on it is 2.8. The light theme swaps
       the pair below. */
    --accent-cta: #f97316;
    --accent-cta-hover: #fb923c;
    --on-accent-cta: #0f172a;
    --accent-cta-rgb: 249, 115, 22;
    --accent-success: #22c55e;
    --accent-success-rgb: 34, 197, 94;
    --accent-warning: #eab308;
    --accent-warning-rgb: 234, 179, 8;
    --accent-danger: #ef4444;
    --accent-danger-rgb: 239, 68, 68;
    --accent-info: #38bdf8;
    --accent-info-rgb: 56, 189, 248;
    --accent-purple: #a78bfa;
    --accent-purple-rgb: 167, 139, 250;

    /* No glow tokens. There were five, plus an ambient gradient behind the
       page, blur behind the top bar, menus and dialogs, a gradient on the
       primary button and a lift on every card. On a screen somebody operates
       all day that is decoration competing with the data, and it dated the
       product: separation is a hairline border and a real shadow now. */

    /* Type scale. The stylesheet declared thirty font sizes, a page rendered
       eleven or twelve of them, and the smallest were 8.8px and 9.6px. Eight
       steps for text with a 12px floor, four for display figures and the big
       icons of an empty state, and one for a caret or chevron, which is a
       glyph and not something anybody reads, and one for a count on an icon.
       TypeComesFromTheScaleTest holds the file to them. */
    --fs-xs: 0.75rem;       /* 12 — the floor: captions, counts, table headers */
    --fs-sm: 0.8125rem;     /* 13 */
    --fs-base: 0.875rem;    /* 14 — controls and dense text */
    --fs-md: 1rem;          /* 16 — body */
    --fs-lg: 1.125rem;      /* 18 */
    --fs-xl: 1.25rem;       /* 20 */
    --fs-2xl: 1.5rem;       /* 24 — page titles */
    --fs-3xl: 1.75rem;      /* 28 */
    --fs-display-sm: 2rem;
    --fs-display-md: 2.5rem;
    --fs-display-lg: 3rem;
    --fs-display-xl: 4rem;
    --fs-glyph: 0.6rem;
    /* A one- or two-digit count sitting on an icon. 11px, the one step under
       the floor: at 12 it no longer fits over the icon and sits beside it,
       which TheTabLabelsCanBeReadTest has always refused. */
    --fs-count: 0.6875rem;

    /* Layout */
    --topbar-height: 56px;
    --sidebar-width: 250px;
    --sidebar-collapsed-width: 60px;
    --sidebar-transition: width 0.25s ease, min-width 0.25s ease, max-width 0.25s ease, flex 0.25s ease;
    --flyout-width: 220px;
    --border-radius: 0.5rem;
    --border-radius-lg: 0.75rem;
    --border-radius-xl: 1rem;
    --transition-speed: 0.2s;
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

    /* One focus ring, for everything (see "Focus States"). The link colour is
       a foreground in both themes — 5.75 on a dark card, 5.17 on white — so a
       ring drawn in it clears the 3:1 a focus indicator needs wherever it
       lands. The field version keeps a gap of the ground between the control's
       own border and the ring, so the two do not read as one thick edge. */
    --focus-colour: var(--text-link);
    --focus-ring: 0 0 0 2px var(--bg-card), 0 0 0 4px var(--focus-colour);

    /* Shadows */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
    --shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
}

/* ==========================================================================
   1b. Light Theme Overrides
   ========================================================================== */

:root[data-bs-theme="light"],
[data-bs-theme="light"] {
    color-scheme: light;

    /* Action Orange on white wants a deeper shade to carry a white label:
       white on #c2410c is 5.8:1, and it darkens under the pointer. */
    --accent-cta: #c2410c;
    --accent-cta-hover: #9a3412;
    --on-accent-cta: #ffffff;
    --accent-cta-rgb: 194, 65, 12;

    /* Background Colors */
    --bg-body: #f1f5f9;
    --bg-sidebar: #ffffff;
    --bg-card: #ffffff;
    --bg-card-hover: #f1f5f9;
    --bg-input: #ffffff;
    --bg-hover: rgba(0, 0, 0, 0.04);

    /* Border Colors */
    --border-color: #e2e8f0;
    --border-color-light: #cbd5e1;

    /* Text Colors */
    --text-primary: #1e293b;
    --text-secondary: #475569;
    /* 5.43 on white and 4.95 on the page. #94a3b8 measured 2.56 and 2.34, and
       #64748b, the obvious step down, still fails on the page at 4.34. */
    --text-muted: #5b6b82;
    --text-link: #2563eb;
    /* Light mode needs this too: --accent-danger is 3.76 on white. #dc2626
       cleared white (4.83) and missed the page ground (4.41); this is 5.41
       and 4.94. */
    --text-danger: #cf2020;
    /* And on white these four have to be the deep shades. The accents are
       bright by design — they were picked to carry black or white text on a
       dark screen — so as labels on paper-white they measured 2.28, 1.92,
       2.14 and 2.72 against AA's 4.50. The View button on every member row
       was the 2.14. */
    --text-success: #15803d;
    /* #a16207 was 4.49 on the page ground — one hundredth short. */
    --text-warning: #975b06;
    --text-info: #0369a1;
    --text-purple: #7c3aed;

    /* Shadows (lighter for light mode) */
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
    --shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.06);
}

/* Light mode specific overrides */
[data-bs-theme="light"] .btn-close {
    filter: none;
}

[data-bs-theme="light"] .accordion-button::after {
    filter: none;
}

[data-bs-theme="light"] .card-header {
    background-color: rgba(0, 0, 0, 0.02);
}

[data-bs-theme="light"] .card-footer {
    background-color: rgba(0, 0, 0, 0.02);
}

[data-bs-theme="light"] .table thead th {
    background-color: rgba(0, 0, 0, 0.02);
}

[data-bs-theme="light"] table.dataTable thead th,
[data-bs-theme="light"] table.dataTable thead td {
    background-color: rgba(0, 0, 0, 0.02) !important;
}

[data-bs-theme="light"] .badge.bg-secondary {
    background-color: rgba(0, 0, 0, 0.08) !important;
}

[data-bs-theme="light"] .bg-light {
    background-color: #f8fafc !important;
}

[data-bs-theme="light"] .rank-badge {
    border-color: rgba(0, 0, 0, 0.15);
}

[data-bs-theme="light"] .integration-card {
    background: rgba(0, 0, 0, 0.02);
}

[data-bs-theme="light"] .bottom-tab-bar {
    background: rgba(255, 255, 255, 0.95);
    box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.06);
}

[data-bs-theme="light"] .skeleton {
    background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%);
    background-size: 200% 100%;
}

/* Theme-adaptive surface class (replaces hardcoded bg-dark in templates) */
.bg-surface {
    background-color: var(--bg-input);
    color: var(--text-primary);
}

.bg-surface-card {
    background-color: var(--bg-card);
    color: var(--text-primary);
}

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

.border-adaptive {
    border-color: var(--border-color) !important;
}

/* Theme toggle button */
.theme-toggle {
    background: none;
    border: 1px solid var(--border-color);
    color: var(--text-secondary);
    border-radius: var(--border-radius);
    padding: 0.25rem 0.5rem;
    cursor: pointer;
    transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease, border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
    font-size: var(--fs-base);
    line-height: 1;
}

.theme-toggle:hover {
    color: var(--accent-primary);
    border-color: var(--accent-primary);
    background: rgba(var(--accent-primary-rgb), 0.1);
}

/* ==========================================================================
   2. Base Element Styles
   ========================================================================== */

body {
    font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background-color: var(--bg-body);
    color: var(--text-primary);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    line-height: 1.6;
    transition: background-color 0.3s ease, color 0.3s ease;
    letter-spacing: -0.011em;
}

/* A heading's LEVEL says where it sits in the page and its SIZE is a class:
   a card's title is `<h2 class="h5">`, the second level of the page at the
   fifth size. Every card title was an <h5> straight under the <h1>, three
   levels skipped, because the element was being picked for how big it drew.
   So the look belongs to the size class as much as to the element, and the
   weight follows the size that is drawn, not the level. */
h1, h2, h3, h4, h5, h6,
.h1, .h2, .h3, .h4, .h5, .h6 {
    color: var(--text-primary);
    font-weight: 600;
    letter-spacing: -0.02em;
}

h1, h2, .h1, .h2 {
    font-weight: 700;
}

.h3, .h4, .h5, .h6 {
    font-weight: 600;
}

a {
    color: var(--text-link);
    text-decoration: none;
    transition: color var(--transition-speed) ease;
}

a:hover {
    color: var(--accent-primary-hover);
}

::selection {
    background: rgba(var(--accent-primary-rgb), 0.3);
    color: var(--text-primary);
}

hr {
    border-color: var(--border-color);
    opacity: 0.5;
}

/* Scrollbar */
::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}

::-webkit-scrollbar-track {
    background: var(--bg-body);
}

::-webkit-scrollbar-thumb {
    background: var(--border-color);
    border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
    background: var(--border-color-light);
}

/* ==========================================================================
   3. Bootstrap Overrides — Dark Theme
   ========================================================================== */

/* --- Cards --- */
.card {
    background-color: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--border-radius-xl);
    box-shadow: var(--shadow-sm);
    transition: transform 0.25s var(--ease-out), box-shadow 0.25s var(--ease-out);
    color: var(--text-primary);
    overflow: hidden;
}

.card-header {
    background-color: rgba(255, 255, 255, 0.03);
    border-bottom: 1px solid var(--border-color);
    color: var(--text-primary);
    font-weight: 600;
    padding: 1rem 1.25rem;
}

/* A card header's heading says what the header says.

   `h1..h6 { color: var(--text-primary) }` above gives the heading a colour of
   its own, and a colour of its own is not inheritance — so a header painted
   `bg-warning text-dark` or `bg-success text-white` put that class on the div
   and the h5 inside ignored it. The instructor's "Pending requests" header was
   near-white on solid yellow: 1.49, against the 4.50 AA asks for, on ten
   headers across the app.

   Inheriting is the whole fix: the utility on the header is Bootstrap's own
   pairing for that background, and it is correct. A header with no background
   utility inherits `--text-primary` from the rule above, which is what its
   heading already had. */
.card-header h1,
.card-header h2,
.card-header h3,
.card-header h4,
.card-header h5,
.card-header h6,
.card-header :is(.h1, .h2, .h3, .h4, .h5, .h6) {
    color: inherit;
}

.card-footer {
    background-color: rgba(255, 255, 255, 0.03);
    border-top: 1px solid var(--border-color);
}

/* --- Tables --- */
.table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--text-secondary);
    --bs-table-border-color: var(--border-color);
    --bs-table-hover-bg: rgba(var(--accent-primary-rgb), 0.05);
    --bs-table-hover-color: var(--text-primary);
    --bs-table-striped-bg: rgba(255, 255, 255, 0.02);
    color: var(--text-secondary);
}

.table thead th {
    background-color: rgba(255, 255, 255, 0.03);
    /* A column header is what the eye finds a column by. Secondary, not muted:
       5.24 on this header in dark, 7.26 in light. */
    color: var(--text-secondary);
    font-weight: 600;
    font-size: var(--fs-sm);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    border-bottom-color: var(--border-color);
    white-space: nowrap;
}

.table td {
    border-bottom-color: var(--border-color);
    vertical-align: middle;
}

.table tbody tr:hover {
    --bs-table-hover-bg: rgba(var(--accent-primary-rgb), 0.05);
}

/* --text-link, which is a foreground. --accent-primary is a background: as the
   colour of a member's name or an invoice number on a dark card it measured
   2.83, under even the 3:1 allowed for large text, on every list in the app. */
.table a:not(.btn) {
    color: var(--text-link);
}

.table a:not(.btn):hover {
    color: var(--text-primary);
}

/* --- Forms --- */
.form-control,
.form-select {
    background-color: var(--bg-input);
    border: 1px solid var(--border-color);
    color: var(--text-primary);
    border-radius: var(--border-radius);
    transition: border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}

.form-control:focus,
.form-select:focus {
    background-color: var(--bg-input);
    color: var(--text-primary);
    border-color: var(--focus-colour);
    box-shadow: var(--focus-ring);
}

.form-control::placeholder {
    color: var(--text-muted);
}

.form-control:disabled,
.form-control[readonly] {
    background-color: var(--bg-card);
    opacity: 0.7;
}

.form-label {
    color: var(--text-secondary);
    font-weight: 500;
    font-size: var(--fs-base);
}

.form-check-input {
    background-color: var(--bg-input);
    border-color: var(--border-color-light);
}

.form-check-input:checked {
    background-color: var(--accent-primary);
    border-color: var(--accent-primary);
}

.form-check-input:focus {
    box-shadow: var(--focus-ring);
    border-color: var(--focus-colour);
}

.form-check-label {
    color: var(--text-secondary);
}

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

.input-group-text {
    background-color: var(--bg-card);
    border-color: var(--border-color);
    color: var(--text-secondary);
}

/* The eye inside a password box. Inside rather than beside it so nothing
   can wrap the two apart, whatever else the form says around them. */
.password-reveal { position: relative; }
.password-reveal .form-control { padding-right: 2.5rem; }
.password-reveal-btn {
    position: absolute;
    top: 0;
    right: 0;
    height: 100%;
    width: 2.5rem;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
}
.password-reveal-btn:hover, .password-reveal-btn:focus { color: var(--text-secondary); outline: none; }

/* --- Buttons --- */
.btn {
    border-radius: var(--border-radius);
    font-weight: 500;
    transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease, border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
    font-size: var(--fs-base);
}

.btn-primary {
    background: var(--accent-primary);
    border-color: var(--accent-primary);
    color: #fff;
}

.btn-primary:hover,
.btn-primary:focus {
    background: var(--accent-primary-pressed);
    border-color: var(--accent-primary-pressed);
    color: #fff;
}

/*
    At rest this button is blue ON the page, so its colour is a foreground and
    takes --text-link, which each theme already tunes for exactly that job.
    --accent-primary is the wrong token here: it is a background, chosen to
    carry white text, and against the dark theme's panels it measured 2.83:1 —
    under AA's 4.5 for text and under the 3.0 that a control's own border has
    to clear. --text-link is 5.75 on a panel and 7.02 on the page.

    Light mode is unchanged: --text-link and --accent-primary are both #2563eb
    there, which is where the bug hid. The theme had solved this once, for
    links, and the buttons were never moved over.

    Hover and active still fill with --accent-primary under white text, which
    is what that token is for.
*/
.btn-outline-primary {
    --bs-btn-color: var(--text-link);
    --bs-btn-border-color: var(--text-link);
    --bs-btn-hover-bg: var(--accent-primary);
    --bs-btn-hover-border-color: var(--accent-primary);
    --bs-btn-hover-color: #fff;
    --bs-btn-active-bg: var(--accent-primary);
    --bs-btn-active-border-color: var(--accent-primary);
    --bs-btn-active-color: #fff;
}

.btn-outline-secondary {
    color: var(--text-secondary);
    border-color: var(--border-color);
}

.btn-outline-secondary:hover {
    background-color: var(--bg-card-hover);
    border-color: var(--border-color-light);
    color: var(--text-primary);
}

/* A filled button's label sits on its own fill, so the fill has to be dark
   enough for the label to beat it.

   These two set only a background and left Bootstrap's white label in place,
   which measured 2.28 (success) and 3.76 (danger) against the 4.50 AA needs —
   the worst text in the app, on Pay, Approve, Delete and Deactivate. Warning
   and info below never had the problem because they say `color: #000` on their
   brighter accents, and measure 10.95 and 9.80.

   Fixed by darkening the fill rather than blackening the label: white on a
   deep green or red is what a person expects a Pay or a Delete button to look
   like, and both shades are already in the palette — #15803d is the light
   theme's --text-success, #b91c1c the hover fill of .btn-outline-danger.
   --accent-success and --accent-danger keep their brightness for badges,
   glows and outline labels, where they sit on a dark ground and work. */
.btn-success {
    --bs-btn-bg: #15803d;
    --bs-btn-border-color: #15803d;
    --bs-btn-color: #fff;
    --bs-btn-hover-bg: #166534;
    --bs-btn-hover-border-color: #166534;
    --bs-btn-hover-color: #fff;
    --bs-btn-active-bg: #14532d;
    --bs-btn-active-border-color: #14532d;
    --bs-btn-active-color: #fff;
    --bs-btn-disabled-bg: #15803d;
    --bs-btn-disabled-border-color: #15803d;
    --bs-btn-disabled-color: #fff;
}

.btn-danger {
    --bs-btn-bg: #b91c1c;
    --bs-btn-border-color: #b91c1c;
    --bs-btn-color: #fff;
    --bs-btn-hover-bg: #991b1b;
    --bs-btn-hover-border-color: #991b1b;
    --bs-btn-hover-color: #fff;
    --bs-btn-active-bg: #7f1d1d;
    --bs-btn-active-border-color: #7f1d1d;
    --bs-btn-active-color: #fff;
    --bs-btn-disabled-bg: #b91c1c;
    --bs-btn-disabled-border-color: #b91c1c;
    --bs-btn-disabled-color: #fff;
}

/* The one next action where money or a commitment changes hands: Take
   payment, Finalize sale, Charge, Record payment. Staff screens only. It is
   orange so that it is the only warm thing on the screen and leads without
   growing; blue stays the primary for everything else (New member, Save), and
   drops back to links and selection beside one of these.

   Not the member's screens: their Pay is green and their Register blue, to
   match the app, where orange already means undo. Not the school's public
   pages, which take the school's own colours. */
.btn-action {
    --bs-btn-bg: var(--accent-cta);
    --bs-btn-border-color: var(--accent-cta);
    --bs-btn-color: var(--on-accent-cta);
    --bs-btn-hover-bg: var(--accent-cta-hover);
    --bs-btn-hover-border-color: var(--accent-cta-hover);
    --bs-btn-hover-color: var(--on-accent-cta);
    --bs-btn-active-bg: var(--accent-cta-hover);
    --bs-btn-active-border-color: var(--accent-cta-hover);
    --bs-btn-active-color: var(--on-accent-cta);
    --bs-btn-disabled-bg: var(--accent-cta);
    --bs-btn-disabled-border-color: var(--accent-cta);
    --bs-btn-disabled-color: var(--on-accent-cta);
    font-weight: 600;
}

.btn-warning {
    background-color: var(--accent-warning);
    border-color: var(--accent-warning);
    color: #000;
}

.btn-info {
    background-color: var(--accent-info);
    border-color: var(--accent-info);
    color: #000;
}

.btn-outline-success {
    --bs-btn-color: var(--text-success);
    --bs-btn-border-color: var(--text-success);
    --bs-btn-hover-bg: var(--accent-success);
    --bs-btn-hover-border-color: var(--accent-success);
    --bs-btn-hover-color: #000;
    --bs-btn-active-bg: var(--accent-success);
    --bs-btn-active-border-color: var(--accent-success);
    --bs-btn-active-color: #000;
}

/* Same correction as --outline-primary above, and this one failed in BOTH
   themes: --accent-danger is 3.89 as a label on a dark panel and 3.76 on
   white. These are the destructive buttons — Deactivate, Cancel, Deny — so
   they are the labels that most need reading before they are pressed. */
.btn-outline-danger {
    --bs-btn-color: var(--text-danger);
    --bs-btn-border-color: var(--text-danger);
    --bs-btn-hover-bg: #b91c1c;
    --bs-btn-hover-border-color: #b91c1c;
    --bs-btn-hover-color: #fff;
    --bs-btn-active-bg: #b91c1c;
    --bs-btn-active-border-color: #b91c1c;
    --bs-btn-active-color: #fff;
}

.btn-outline-warning {
    --bs-btn-color: var(--text-warning);
    --bs-btn-border-color: var(--text-warning);
    --bs-btn-hover-bg: var(--accent-warning);
    --bs-btn-hover-border-color: var(--accent-warning);
    --bs-btn-hover-color: #000;
    --bs-btn-active-bg: var(--accent-warning);
    --bs-btn-active-border-color: var(--accent-warning);
    --bs-btn-active-color: #000;
}

.btn-outline-info {
    --bs-btn-color: var(--text-info);
    --bs-btn-border-color: var(--text-info);
    --bs-btn-hover-bg: var(--accent-info);
    --bs-btn-hover-border-color: var(--accent-info);
    --bs-btn-hover-color: #000;
    --bs-btn-active-bg: var(--accent-info);
    --bs-btn-active-border-color: var(--accent-info);
    --bs-btn-active-color: #000;
}

.btn-outline-purple {
    --bs-btn-color: var(--text-purple);
    --bs-btn-border-color: var(--text-purple);
    --bs-btn-hover-bg: var(--accent-purple);
    --bs-btn-hover-border-color: var(--accent-purple);
    --bs-btn-hover-color: #000;
    --bs-btn-active-bg: var(--accent-purple);
    --bs-btn-active-border-color: var(--accent-purple);
    --bs-btn-active-color: #000;
}

/* A row that opens its record wherever it is pressed (row_link_controller).
   The pointer says so; the name inside is still the link a keyboard uses. */
.row-link {
    cursor: pointer;
}

.row-actions .dropdown-item-text {
    max-width: 16rem;
    white-space: normal;
}

/* The third level: what a card offers, in its header ("Assign a plan",
   "New invoice"). A card never carries a filled button — seven of them on the
   member page were seven claims to be the thing the page is for. A label on
   the page, so --text-link, which each theme tunes for that; --accent-primary
   is a fill and measured 2.83:1 as text on a dark panel. */
.btn-link {
    --bs-btn-color: var(--text-link);
    --bs-btn-hover-color: var(--text-link);
    --bs-btn-active-color: var(--text-link);
    font-weight: 600;
    text-decoration: none;
}

.btn-link:hover,
.btn-link:focus-visible {
    text-decoration: underline;
}

.card-header .btn-link {
    padding-inline: 0.25rem;
}

.btn-close {
    filter: invert(1) grayscale(100%) brightness(200%);
}

/* --- Badges --- */
/* Each badge is a 20% tint of its colour with the text in a shade of it. The
   shade differs by theme (UX-M1): the accent itself failed contrast on its own
   tint — every colour in the light theme (1.7–3.9:1) and blue, green, red and
   grey in the dark one — and the per-badge text-dark some pages added fixed
   light mode by breaking dark (1.6:1). Each shade below clears 4.5:1 on its
   tint over both the card and the page background of its theme. */
:root {
    --badge-primary-text: #93c5fd;
    --badge-success-text: #4ade80;
    --badge-warning-text: #eab308;
    --badge-danger-text: #fca5a5;
    --badge-info-text: #7dd3fc;
    --badge-secondary-text: #cbd5e1;
}

[data-bs-theme="light"] {
    --badge-primary-text: #1d4ed8;
    --badge-success-text: #166534;
    --badge-warning-text: #854d0e;
    --badge-danger-text: #b91c1c;
    --badge-info-text: #0369a1;
    --badge-secondary-text: #475569;
}

.badge.bg-primary {
    background-color: rgba(var(--accent-primary-rgb), 0.2) !important;
    color: var(--badge-primary-text);
}

.badge.bg-success {
    background-color: rgba(var(--accent-success-rgb), 0.2) !important;
    color: var(--badge-success-text);
}

.badge.bg-warning {
    background-color: rgba(var(--accent-warning-rgb), 0.2) !important;
    color: var(--badge-warning-text);
}

.badge.bg-danger {
    background-color: rgba(var(--accent-danger-rgb), 0.2) !important;
    color: var(--badge-danger-text);
}

.badge.bg-info {
    background-color: rgba(var(--accent-info-rgb), 0.2) !important;
    color: var(--badge-info-text);
}

.badge.bg-secondary {
    background-color: rgba(255, 255, 255, 0.1) !important;
    color: var(--badge-secondary-text);
}

/* --- Alerts --- */
.alert {
    border: 1px solid transparent;
    border-radius: var(--border-radius);
    color: var(--text-primary);
}

.alert-success {
    background-color: rgba(var(--accent-success-rgb), 0.1);
    border-color: rgba(var(--accent-success-rgb), 0.4);
}

.alert-danger {
    background-color: rgba(var(--accent-danger-rgb), 0.1);
    border-color: rgba(var(--accent-danger-rgb), 0.4);
}

.alert-warning {
    background-color: rgba(var(--accent-warning-rgb), 0.1);
    border-color: rgba(var(--accent-warning-rgb), 0.4);
}

.alert-info {
    background-color: rgba(var(--accent-info-rgb), 0.1);
    border-color: rgba(var(--accent-info-rgb), 0.4);
}

.alert-primary {
    background-color: rgba(var(--accent-primary-rgb), 0.1);
    border-color: rgba(var(--accent-primary-rgb), 0.4);
}

/* --- Modals --- */
.modal-content {
    background-color: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--border-radius-xl);
    color: var(--text-primary);
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}

/* A scrollable modal whose body and footer are wrapped in a <form>.
 *
 * Bootstrap's scrollable layout assumes .modal-body and .modal-footer are flex
 * children of .modal-content: the content is capped at the window height, the
 * body is told to scroll, and the footer holds its place at the bottom. Put a
 * <form> around the body and footer — which any modal that submits has to —
 * and none of them are flex children any more. The content is still capped,
 * but nothing inside it is, so the body keeps its full height and pushes the
 * footer straight out of the bottom of the window.
 *
 * Measured on the membership Edit dialog in a 560px window: content correctly
 * clamped to 504px, body running to 647 and the footer to 711 — 151px past the
 * bottom edge, with neither the body nor the modal able to scroll to it. The
 * buttons that save or cancel a membership were simply not reachable.
 *
 * Making the form the flex child it displaced restores all three. `min-height:
 * 0` is the part that matters: without it a flex item refuses to shrink below
 * its content, and the body would go on overflowing.
 */
.modal-dialog-scrollable .modal-content > form {
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
}

.modal-dialog-scrollable .modal-content > form > .modal-body {
    overflow-y: auto;
}

.modal-header {
    border-bottom-color: var(--border-color);
}

.modal-header .modal-title {
    color: var(--text-primary);
}

.modal-footer {
    border-top-color: var(--border-color);
}

.modal-backdrop.show {
    opacity: 0.6;
}

/* --- Nav Tabs --- */
.nav-tabs {
    border-bottom-color: var(--border-color);
}

.nav-tabs .nav-link {
    color: var(--text-muted);
    border-color: transparent;
    transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease, border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}

.nav-tabs .nav-link:hover {
    color: var(--text-primary);
    border-color: transparent;
    background-color: var(--bg-hover);
}

.nav-tabs .nav-link.active {
    background-color: var(--bg-card);
    border-color: var(--border-color) var(--border-color) var(--bg-card);
    color: var(--accent-primary);
    font-weight: 500;
}

/* --- Nav Pills --- */
.nav-pills .nav-link {
    color: var(--text-secondary);
    transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease, border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}

.nav-pills .nav-link:hover {
    background-color: var(--bg-hover);
    color: var(--text-primary);
}

.nav-pills .nav-link.active {
    background-color: var(--accent-primary);
    color: #fff;
}

/* --- List Groups --- */
.list-group-item {
    background-color: var(--bg-card);
    border-color: var(--border-color);
    color: var(--text-secondary);
    transition: background-color var(--transition-speed) ease;
}

.list-group-item:hover {
    background-color: var(--bg-card-hover);
}

.list-group-item-action:hover {
    background-color: var(--bg-card-hover);
    color: var(--text-primary);
}

a.list-group-item:hover,
a.list-group-item-action:hover {
    color: var(--text-primary);
}

/* --- Dropdowns --- */
.dropdown-menu {
    background-color: var(--bg-card);
    border-color: var(--border-color);
    border-radius: var(--border-radius-lg);
    box-shadow: 0 20px 40px -8px rgba(0, 0, 0, 0.5);
    padding: 0.35rem;
}

.dropdown-item {
    color: var(--text-secondary);
    transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease, border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}

.dropdown-item {
    border-radius: var(--border-radius);
}

.dropdown-item:hover,
.dropdown-item:focus {
    background-color: rgba(var(--accent-primary-rgb), 0.1);
    color: var(--text-primary);
}

.dropdown-divider {
    border-top-color: var(--border-color);
}

/* --- Pagination --- */
.page-link {
    background-color: var(--bg-card);
    border-color: var(--border-color);
    color: var(--text-secondary);
}

.page-link:hover {
    background-color: var(--bg-card-hover);
    border-color: var(--border-color);
    color: var(--text-primary);
}

.page-item.active .page-link {
    background-color: var(--accent-primary);
    border-color: var(--accent-primary);
    color: #fff;
}

.page-item.disabled .page-link {
    background-color: var(--bg-card);
    border-color: var(--border-color);
    color: var(--text-muted);
}

/* --- Progress Bar --- */
.progress {
    background-color: var(--bg-body);
    border-radius: var(--border-radius);
}

.progress-bar {
    background-color: var(--accent-primary);
}

/* --- Accordion --- */
.accordion-item {
    background-color: var(--bg-card);
    border-color: var(--border-color);
}

.accordion-button {
    background-color: var(--bg-card);
    color: var(--text-primary);
}

.accordion-button:not(.collapsed) {
    background-color: rgba(var(--accent-primary-rgb), 0.1);
    color: var(--accent-primary);
}

.accordion-button::after {
    filter: invert(1);
}

/* --- Text Utilities --- */
.text-muted {
    color: var(--text-muted) !important;
}

.text-primary {
    color: var(--accent-primary) !important;
}

.text-success {
    color: var(--accent-success) !important;
}

.text-danger {
    color: var(--accent-danger) !important;
}

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

.text-info {
    color: var(--accent-info) !important;
}

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

/* --- Background Utilities --- */
/* This redefines .bg-light as the card background in every theme, so in dark
   mode it is dark. Bootstrap's .text-dark is not overridden for dark mode and
   stays near-black, which made "bg-light text-dark" — a pairing Bootstrap
   intends to be readable — dark text on a dark card. Both uses of that pairing
   have been changed to theme-aware tokens; leaving this rule alone because it
   is load-bearing for panels across the app, but do not pair .bg-light with
   .text-dark. */
.bg-light {
    background-color: var(--bg-card) !important;
}

/* --- Breadcrumb --- */
.breadcrumb {
    background: none;
    padding: 0;
    margin-bottom: 1rem;
    font-size: var(--fs-sm);
}

.breadcrumb-item a {
    color: var(--text-muted);
}

.breadcrumb-item a:hover {
    color: var(--accent-primary);
}

.breadcrumb-item.active {
    color: var(--text-secondary);
}

.breadcrumb-item + .breadcrumb-item::before {
    color: var(--text-muted);
}

/* ==========================================================================
   4. Top Bar
   ========================================================================== */

.topbar {
    position: sticky;
    top: 0;
    z-index: 1040;
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: var(--topbar-height);
    padding: 0 1rem;
    background: var(--bg-sidebar);
    border-bottom: 1px solid var(--border-color);
    transition: background 0.3s ease, border-color 0.3s ease;
}

.topbar-left {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-shrink: 0;
}

.topbar-brand {
    color: var(--text-primary);
    font-weight: 600;
    font-size: var(--fs-md);
    text-decoration: none;
    white-space: nowrap;
}

.topbar-brand:hover {
    color: var(--accent-primary);
}

.topbar-sidebar-toggle {
    background: none;
    border: 1px solid var(--border-color);
    color: var(--text-primary);
    border-radius: var(--border-radius);
    padding: 0.3rem 0.5rem;
    font-size: var(--fs-lg);
    line-height: 1;
    cursor: pointer;
    transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease, border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}

.topbar-sidebar-toggle:hover {
    color: var(--accent-primary);
    border-color: var(--accent-primary);
}

.topbar-search {
    flex: 0 1 320px;
    margin: 0 1rem;
}

.topbar-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-shrink: 0;
}

.topbar-icon-btn {
    color: var(--text-secondary);
    font-size: var(--fs-lg);
    padding: 0.25rem;
    transition: color var(--transition-speed) ease;
    /* This was written for the <a> icons beside it, which have no background or
       border to begin with. On a <button> the browser supplies both, so the
       card reader control arrived as a grey box among three bare glyphs — and
       the mobile search button has looked the same way for as long as it has
       existed, where nobody was looking. */
    background: none;
    border: none;
    line-height: 1;
    cursor: pointer;
}

.topbar-icon-btn:hover {
    color: var(--accent-primary);
}

.topbar-user-btn {
    background: none;
    border: 1px solid var(--border-color);
    color: var(--text-secondary);
    border-radius: var(--border-radius);
    padding: 0.3rem 0.65rem;
    font-size: var(--fs-sm);
    cursor: pointer;
    transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease, border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

.topbar-user-btn:hover {
    color: var(--text-primary);
    border-color: var(--border-color-light);
    background: var(--bg-hover);
}

.topbar-user-btn::after {
    font-size: var(--fs-glyph);
    vertical-align: middle;
}

.topbar-user-badge {
    position: absolute;
    top: -2px;
    left: 12px;
    font-size: var(--fs-count) !important;
    padding: 0.2em 0.45em;
    pointer-events: none;
}

.topbar-offset {
    /* Push content below the sticky topbar */
}

/* ── Mobile Search Slide-Down Panel ── */
.topbar-search-panel {
    display: none;
    background: var(--bg-sidebar);
    border-bottom: 1px solid var(--border-color);
    padding: 0.5rem 1rem;
    z-index: 1039;
}

.topbar-search-panel.is-open {
    display: block;
    animation: slideDown 0.2s ease;
}

@media (min-width: 576px) {
    .topbar-search-panel {
        display: none !important;
    }
}

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

/* ==========================================================================
   4a. Sidebar
   ========================================================================== */

.sidebar {
    min-height: calc(100vh - var(--topbar-height));
    background: var(--bg-sidebar);
    display: flex;
    flex-direction: column;
    border-right: 1px solid var(--border-color);
    transition: width 0.25s ease, min-width 0.25s ease;
    transition: background 0.3s ease, border-color 0.3s ease, var(--sidebar-transition);
}

.sidebar .nav-link {
    color: var(--text-secondary);
    padding: 0.6rem 1rem;
    border-radius: var(--border-radius-lg);
    margin: 0.125rem 0.5rem;
    transition: color 0.2s var(--ease-out), background-color 0.2s var(--ease-out);
    font-size: var(--fs-base);
    display: flex;
    align-items: center;
    position: relative;
}

.sidebar .nav-link:hover {
    color: var(--text-primary);
    background: rgba(var(--accent-primary-rgb), 0.06);
}

.sidebar .nav-link.active {
    /* --text-link, a foreground: --accent-primary measured 3.2 on the dark
       sidebar. The fill and the weight say "you are here"; the stripe and
       the glow that used to say it as well are gone. */
    color: var(--text-link);
    background: rgba(var(--accent-primary-rgb), 0.12);
    font-weight: 600;
}

.sidebar .nav-link i {
    width: 1.5rem;
    text-align: center;
    margin-right: 0.5rem;
    font-size: var(--fs-md);
}

/* Indentation says one thing: I belong to the heading above me.
 *
 * It used to say nothing. Every link sat at the same offset while headings sat
 * further left, so Dashboard, Reports and Setup — which belong to no section —
 * read as children of whichever heading happened to be above them. A heading
 * and a standalone link are peers, so they line up; only a link inside a
 * section is indented, and now that indent means something.
 *
 * Left padding here is 1.5rem against the links' 1rem padding plus 0.5rem
 * margin, which puts heading text and standalone icons on the same 24px edge.
 * Icon-only mode overrides both and is more specific, so it is unaffected. */
.sidebar-heading {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    padding: 0.75rem 1rem 0.25rem 1.5rem;
    margin-top: 0.25rem;
    font-weight: 600;
}

.sidebar-section-links .nav-link {
    padding-left: 1.75rem;
}

.sidebar-nav-scroll {
    flex: 1;
    overflow-y: auto;
}

/* The sidebar stays where it is while the page scrolls.

   It was an ordinary column, as tall as the page: scroll a long one — a
   member's page is four screens — and the navigation left with the top of it,
   with a blank 255px column beside everything below. Getting anywhere else
   meant scrolling back up first.

   From md up only. Below that it is Bootstrap's offcanvas drawer, which is
   fixed already. The list inside scrolls on its own when it is taller than the
   screen (.sidebar-nav-scroll), and keeps overflow visible in icon-only mode so
   the flyouts are not clipped. */
@media (min-width: 768px) {
    .sidebar {
        position: sticky;
        top: var(--topbar-height);
        align-self: flex-start;
        height: calc(100vh - var(--topbar-height));
        min-height: 0;
        /* Sticky makes a stacking context, so the icon-only flyouts' z-index
           of 1060 now counts only inside the sidebar — and the sidebar itself
           was level with the page, under every card that comes after it: the
           flyouts opened behind the content. Above the page, below the top
           bar (1040) and anything modal. */
        z-index: 1030;
    }

    .sidebar .sidebar-nav-scroll {
        flex: 1 1 auto;
        min-height: 0;
    }
}

/* The nav list has to be told to fill the sidebar it is in.
   
   This element is also Bootstrap's .offcanvas-body — the sidebar is a drawer
   below md and an ordinary column above it. Unwrapping it back into a column
   is done by `.offcanvas-md .offcanvas-body { display: flex }`, a *row* flex
   container, which leaves the list inside it a flex item at flex: 0 1 auto:
   sized to its own content, not to its parent.
   
   So the sidebar was always exactly as wide as its longest label. Nobody saw
   it while the only sidebar was the admin one, whose "Agreement Templates"
   already fills the column. On the instructor sidebar the longest is "Private
   Lessons", which left 40px of the column unused and the collapse button
   floating short of the right edge instead of on it.
   
   Desktop only: below md the offcanvas-body is not a flex container and the
   list was already full width, which is why this reads as "the sidebar looks
   wrong on my laptop and right on my phone". */
.sidebar .sidebar-nav-scroll > .nav {
    width: 100%;
}

/* And it is a list, not a grid of columns.
   
   Bootstrap's .nav carries flex-wrap: wrap, which in a column direction means
   "break into a second column when you run out of height". Nothing here ever
   wants that, and leaving it on cost a second bug on top of the width one:
   Blink inflated the switcher's row by 35px of empty space — the gap under it
   — and left every other row alone. Whatever the exact mechanism, an item in a
   wrapping column container is not sized the way the same item is in a
   straight one, and the straight one is what a sidebar is. */
.sidebar .nav {
    flex-wrap: nowrap;
}


/* ==========================================================================
   4b. Sidebar — Collapsible Sections & Icon-Only Mode
   ========================================================================== */

/* ── Section Group Structure ── */

.sidebar-section-group {
    list-style: none;
    position: relative;
}

.sidebar-section-toggle {
    display: flex;
    align-items: center;
    cursor: pointer;
    user-select: none;
    width: 100%;
}

.sidebar-section-toggle:hover {
    color: var(--text-secondary);
}

.sidebar-section-toggle:focus-visible {
    outline: 2px solid var(--focus-colour);
    outline-offset: 2px;
    border-radius: 3px;
}

.sidebar-section-icon {
    display: none;
}

.sidebar-heading-text {
    flex: 1;
}

.sidebar-chevron {
    font-size: var(--fs-glyph);
    transition: transform var(--transition-speed) ease;
    margin-left: auto;
    opacity: 0.5;
}

.sidebar-section-toggle.collapsed .sidebar-chevron {
    transform: rotate(-90deg);
}

/* Section links — animated collapse */
/* A section is shut unless something says otherwise.
   
   It used to be the other way round — open in the markup, closed by JavaScript
   once the controller connected — which meant every navigation drew the whole
   menu expanded and then animated it shut, on every page, for 0.3s. Stating the
   closed case in the markup is what removes that: the sidebar arrives in its
   final state and nothing moves after paint. */
.sidebar-section-links {
    overflow: hidden;
    max-height: 0;
    opacity: 0;
    pointer-events: none;
    transition: max-height 0.3s ease, opacity 0.2s ease;
}

/* The section holding the page you are on, found by the active link inside it.
   
   Done in CSS rather than by the server because the server does not know which
   section is active — each link works that out for itself from the route — and
   done here rather than in JavaScript because anything JavaScript does happens
   after the page is drawn. It also makes "the section you are in is always
   open" true structurally: there is no state to get wrong and no way to close
   it.
   
   :has() is an enhancement, not a dependency. Where it is unsupported the
   controller opens the active section on connect, which is what the whole
   sidebar used to do — a flicker in one section rather than five. */
.sidebar-section-links:has(.nav-link.active),
.sidebar-section-links.sidebar-section-open {
    max-height: 1000px;
    opacity: 1;
    pointer-events: auto;
}

/* The heading of the section you are in. It cannot be closed, so it does not
   pretend to be pressable: no pointer, no chevron, no hover lift. */
.sidebar-section-fixed {
    cursor: default;
}

.sidebar-section-fixed .sidebar-chevron {
    visibility: hidden;
}

/* ── Sidebar Collapse Toggle Button ── */

.sidebar-collapse-btn {
    background: none;
    border: 1px solid var(--border-color);
    color: var(--text-secondary);
    border-radius: var(--border-radius);
    padding: 0.25rem 0.4rem;
    cursor: pointer;
    transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease, border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
    font-size: var(--fs-sm);
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.sidebar-collapse-btn:hover {
    color: var(--accent-primary);
    border-color: var(--accent-primary);
    background: rgba(var(--accent-primary-rgb), 0.1);
}

/* ── Staff / Member Switch ── */

.view-mode-wrap {
    padding: 0 1rem 0.5rem;
}

/* Wraps deliberately. Below about 1200px the sidebar is a percentage of a
   narrower window — 170px at 1024 — and two labelled buttons across that
   truncate the longer label to "Instru…". The basis is set so they sit side by
   side while both fit (the widest label is "Front Desk") and stack full-width
   when they do not, which reads perfectly well and never clips a word. */
.view-mode-switch {
    display: flex;
    flex-wrap: wrap;
    gap: 0.15rem;
    padding: 0.2rem;
    background: rgba(var(--accent-primary-rgb), 0.05);
    border: 1px solid var(--border-color);
    border-radius: var(--border-radius-lg);
}

.view-mode-switch form {
    display: flex;
    flex: 1 1 5.5rem;
    min-width: 0;
}

.view-mode-btn {
    display: flex;
    flex: 1;
    min-width: 0;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    padding: 0.3rem 0.35rem;
    border: 0;
    border-radius: calc(var(--border-radius-lg) - 0.2rem);
    background: transparent;
    color: var(--text-secondary);
    font-size: var(--fs-sm);
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    transition: color 0.2s var(--ease-out), background 0.2s var(--ease-out);
}

.view-mode-btn i {
    font-size: var(--fs-base);
    flex-shrink: 0;
}

.view-mode-btn .sidebar-label {
    overflow: hidden;
    text-overflow: ellipsis;
}

.view-mode-btn:hover {
    color: var(--text-primary);
    background: rgba(var(--accent-primary-rgb), 0.1);
}

/* Tinted, not filled — the same language the active nav link speaks, which
   also keeps the text off a solid accent whose contrast is the school's to
   choose and not ours to guarantee. */
.view-mode-btn.active {
    color: var(--accent-primary);
    background: rgba(var(--accent-primary-rgb), 0.14);
}

.view-mode-btn.active:hover {
    background: rgba(var(--accent-primary-rgb), 0.18);
}

/* ── Icon-Only Collapsed Mode ── */

.sidebar.sidebar-icon-only {
    width: var(--sidebar-collapsed-width) !important;
    min-width: var(--sidebar-collapsed-width) !important;
    max-width: var(--sidebar-collapsed-width) !important;
    flex: 0 0 var(--sidebar-collapsed-width) !important;
    overflow: visible;
}

body.sidebar-is-collapsed .content-area {
    flex: 1 !important;
    max-width: calc(100% - var(--sidebar-collapsed-width)) !important;
}

/* Hide text labels in collapsed mode — from sight only. display:none took
   each link's only text with it, so a collapsed link had no name at all for
   a screen reader (UX-H5). Desktop only: below 768px the collapsed mode is
   switched off and the mobile rule shows the labels again, which it could
   not do against these. */
@media (min-width: 768px) {
    .sidebar.sidebar-icon-only .sidebar-label {
        position: absolute !important;
        width: 1px !important;
        height: 1px !important;
        padding: 0 !important;
        margin: -1px !important;
        overflow: hidden !important;
        clip: rect(0, 0, 0, 0) !important;
        white-space: nowrap !important;
        border: 0 !important;
    }
}

.sidebar.sidebar-icon-only .sidebar-heading-text,
.sidebar.sidebar-icon-only .sidebar-chevron {
    display: none;
}

/* Hide search bar in collapsed mode */
.sidebar.sidebar-icon-only .sidebar-search-wrap {
    display: none;
}

/* Collapsed to icons, only the side you are *not* on is drawn, as a plain row
   like the ones under it.
   
   Two labelled buttons cannot fit across 60px, and the boxed pair they became
   without their labels read as a control floating on a rail that has none.
   One icon is an action rather than a second copy of where you already are —
   which is what the tooltip then says: "Switch to Member view". Which side you
   are on is still legible from the icons below, since the rail's contents are
   the whole difference between the two. */
.sidebar.sidebar-icon-only .view-mode-wrap {
    padding: 0;
}

.sidebar.sidebar-icon-only .view-mode-switch {
    flex-wrap: nowrap;
    gap: 0;
    padding: 0;
    background: none;
    border: 0;
}

.sidebar.sidebar-icon-only .view-mode-form.active {
    display: none;
}

.sidebar.sidebar-icon-only .view-mode-btn {
    justify-content: center;
    padding: 0.5rem;
    margin: 0.125rem 0.5rem;
    border-radius: var(--border-radius-lg);
}

.sidebar.sidebar-icon-only .view-mode-btn i {
    font-size: var(--fs-lg);
}

/* Show section icon in collapsed mode */
.sidebar.sidebar-icon-only .sidebar-section-icon {
    display: inline-block;
    font-size: var(--fs-lg);
    text-align: center;
    width: 100%;
    color: var(--text-muted);
}

/* Center the section heading as an icon trigger */
.sidebar.sidebar-icon-only .sidebar-section-toggle {
    justify-content: center;
    padding: 0.5rem 0;
}

/* Adjust heading style in collapsed mode */
.sidebar.sidebar-icon-only .sidebar-heading {
    padding: 0.4rem 0;
    text-align: center;
}

/* Hide section links in collapsed mode (flyouts replace them) */
.sidebar.sidebar-icon-only .sidebar-section-links {
    display: none;
}

/* Nav links in collapsed mode: show only icon, centered */
.sidebar.sidebar-icon-only .nav-link {
    justify-content: center;
    padding: 0.5rem;
    margin: 0.125rem auto;
}

.sidebar.sidebar-icon-only .nav-link i {
    margin-right: 0;
    font-size: var(--fs-lg);
}

/* Scroll area in collapsed mode: needs overflow visible for flyouts */
.sidebar.sidebar-icon-only .sidebar-nav-scroll {
    overflow: visible;
}

/* ── Flyout Menus ── */

.sidebar-flyout {
    display: none;
    position: absolute;
    left: var(--sidebar-collapsed-width);
    top: 0;
    width: var(--flyout-width);
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
    box-shadow: var(--shadow-lg);
    padding: 0.5rem 0;
    z-index: 1060;
    max-height: calc(100vh - 100px);
    overflow-y: auto;
}

.sidebar.sidebar-icon-only .sidebar-flyout.sidebar-flyout-visible {
    display: block;
    animation: flyoutIn 0.15s ease forwards;
}

@keyframes flyoutIn {
    from { opacity: 0; transform: translateX(-8px); }
    to   { opacity: 1; transform: translateX(0); }
}

.sidebar-flyout-title {
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-muted);
    padding: 0.4rem 1rem 0.3rem;
    font-weight: 600;
}

.sidebar-flyout-link {
    display: flex;
    align-items: center;
    padding: 0.45rem 1rem;
    color: var(--text-secondary);
    font-size: var(--fs-sm);
    text-decoration: none;
    transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease, border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}

.sidebar-flyout-link:hover {
    color: var(--text-primary);
    background: var(--bg-hover);
}

.sidebar-flyout-link.active {
    color: var(--accent-primary);
    background: rgba(var(--accent-primary-rgb), 0.1);
}

.sidebar-flyout-link i {
    width: 1.5rem;
    text-align: center;
    margin-right: 0.5rem;
    font-size: var(--fs-base);
}

/* ==========================================================================
   5. Content Area
   ========================================================================== */

.content-area {
    background: var(--bg-body);
    min-height: calc(100vh - var(--topbar-height));
}

/* ==========================================================================
   6. Component Library
   ========================================================================== */

/* --- Page Header --- */
.page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1.75rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--border-color);
}

.page-header h1,
.page-header h2 {
    font-weight: 700;
    color: var(--text-primary);
    margin: 0;
    font-size: var(--fs-2xl);
    letter-spacing: -0.02em;
}

/* The icon before a title. The partial used to leave a space there, which the
   heading's tightened letter-spacing closed to almost nothing; the 250 headers
   written by hand each gave it me-2 instead. One gap, here. */
.page-header h1 > .bi {
    margin-right: 0.5rem;
}

/* --- Stat Cards --- */
.stat-card {
    border-radius: var(--border-radius-xl);
    position: relative;
    overflow: hidden;
}

.stat-card .stat-value {
    font-size: var(--fs-3xl);
    font-weight: 700;
    color: var(--text-primary);
    letter-spacing: -0.02em;
}

.stat-card .stat-label {
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    font-weight: 500;
}

.stat-card .stat-icon {
    font-size: var(--fs-display-sm);
    opacity: 0.3;
}

/* The tone of a figure is the colour of the figure. It was a 4px stripe down
   the card's left edge and a faint bubble in its corner — two decorations
   standing in for one meaning, neither of them on the number being read. The
   --text-* tokens are foregrounds and clear 4.5:1 in both themes; "primary"
   is simply the ordinary text colour. */
.stat-card.success .card-body > :is(h3, .h3),
.stat-card-success .card-body > :is(h3, .h3),
.stat-card.success .stat-value,
.stat-card-success .stat-value { color: var(--text-success); }

.stat-card.warning .card-body > :is(h3, .h3),
.stat-card-warning .card-body > :is(h3, .h3),
.stat-card.warning .stat-value,
.stat-card-warning .stat-value { color: var(--text-warning); }

.stat-card.danger .card-body > :is(h3, .h3),
.stat-card-danger .card-body > :is(h3, .h3),
.stat-card.danger .stat-value,
.stat-card-danger .stat-value { color: var(--text-danger); }

.stat-card.info .card-body > :is(h3, .h3),
.stat-card-info .card-body > :is(h3, .h3),
.stat-card.info .stat-value,
.stat-card-info .stat-value { color: var(--text-info); }

.stat-card-purple {
    border-radius: var(--border-radius-xl);
    position: relative;
    overflow: hidden;
}

.stat-card-purple .card-body > :is(h3, .h3),
.stat-card-purple .stat-value { color: var(--text-purple); }

/* --- Empty State --- */
.empty-state {
    text-align: center;
    padding: 3rem 1rem;
    color: var(--text-muted);
}

/* The direct child only: this is the big faded picture above the message.
   Unscoped, it also caught the icon inside the action button that
   _partials/empty_state.html.twig renders — blowing a "Add products" button
   up into a square with a 3rem glyph stacked over wrapped text, at 30%
   opacity. Every empty state in the app that offers an action with an icon
   had it. */
.empty-state > i {
    font-size: var(--fs-display-lg);
    margin-bottom: 1rem;
    opacity: 0.3;
    display: block;
}

.empty-state h5,
.empty-state .h5 {
    color: var(--text-secondary);
    font-weight: 500;
    margin-bottom: 0.5rem;
}

.empty-state p {
    max-width: 400px;
    margin: 0 auto;
    font-size: var(--fs-base);
}

/* Inside a card, a panel or a table cell: the message at the scale of the
   thing around it. `compact` was only ever compact in markup — no icon and a
   single muted line — and nothing here shrank the 3rem padding above it, so
   it arrived the same height as the full-page version and made an empty
   table row taller than a full one. */
.empty-state-compact {
    padding: 1rem 0.75rem;
}

.empty-state-compact p {
    max-width: none;
    font-size: var(--fs-base);
}

/* --- Filter Card --- */
.filter-card {
    border: 1px dashed var(--border-color) !important;
    background: transparent !important;
    box-shadow: none !important;
}

/* --- Action Buttons --- */
.btn-icon {
    padding: 0.35rem 0.55rem;
    border-radius: var(--border-radius);
    line-height: 1;
}

/* --- Rank Badge --- */
.rank-badge {
    display: inline-block;
    width: 1rem;
    height: 1rem;
    border-radius: 50%;
    border: 2px solid rgba(255, 255, 255, 0.2);
    vertical-align: middle;
    box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
}

/* --- Status Dot --- */
.status-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 0.5rem;
}

.status-dot-success {
    background-color: var(--accent-success);
}

.status-dot-warning {
    background-color: var(--accent-warning);
}

.status-dot-danger {
    background-color: var(--accent-danger);
}

/* --- Integration Status --- */
.integration-card {
    border: 1px solid var(--border-color);
    border-radius: var(--border-radius-lg);
    padding: 1rem;
    background: rgba(255, 255, 255, 0.02);
    transition: background-color 0.25s var(--ease-out), border-color 0.25s var(--ease-out), box-shadow 0.25s var(--ease-out);
}

.integration-card:hover {
    background: var(--bg-hover);
    border-color: var(--border-color-light);
}

/* ==========================================================================
   7. Login / Auth Pages
   ========================================================================== */

.login-wrapper {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-body);
    padding: 2rem;
}

.login-card {
    max-width: 420px;
    width: 100%;
}

.login-card .card {
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow-lg);
}

.login-brand {
    color: var(--accent-primary);
    font-size: var(--fs-display-sm);
}

.login-brand + h4 {
    color: var(--text-primary);
}

/* ==========================================================================
   8. Toast Notifications
   ========================================================================== */

.toast-container {
    z-index: 1090;
    /* Top centre, under the top bar. Bootstrap's top-0 end-0 put a toast over
       the search box and the account menu; straight below that it covers the
       page's own actions, the primary button among them. The middle of a page
       header is the one place up here with nothing in it. On a phone the
       container is the width of the screen either way. */
    top: var(--topbar-height) !important;
    left: 50% !important;
    right: auto !important;
    transform: translateX(-50%);
    width: min(28rem, 100vw);
}

.toast {
    background-color: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--border-radius);
    box-shadow: var(--shadow-lg);
    animation: slideInRight 0.3s ease;
    overflow: hidden;
}

.toast.text-bg-success {
    /* Opaque: the tint is mixed into the card colour, not laid over the page. */
    background-color: color-mix(in srgb, var(--accent-success) 15%, var(--bg-card)) !important;
    border: 1px solid rgba(var(--accent-success-rgb), 0.45) !important;
}

.toast.text-bg-danger {
    /* Opaque: the tint is mixed into the card colour, not laid over the page. */
    background-color: color-mix(in srgb, var(--accent-danger) 15%, var(--bg-card)) !important;
    border: 1px solid rgba(var(--accent-danger-rgb), 0.45) !important;
}

.toast.text-bg-warning {
    /* Opaque: the tint is mixed into the card colour, not laid over the page. */
    background-color: color-mix(in srgb, var(--accent-warning) 15%, var(--bg-card)) !important;
    border: 1px solid rgba(var(--accent-warning-rgb), 0.45) !important;
}

.toast.text-bg-info {
    /* Opaque: the tint is mixed into the card colour, not laid over the page. */
    background-color: color-mix(in srgb, var(--accent-info) 15%, var(--bg-card)) !important;
    border: 1px solid rgba(var(--accent-info-rgb), 0.45) !important;
}

.toast.text-bg-primary {
    /* Opaque: the tint is mixed into the card colour, not laid over the page. */
    background-color: color-mix(in srgb, var(--accent-primary) 15%, var(--bg-card)) !important;
    border: 1px solid rgba(var(--accent-primary-rgb), 0.45) !important;
}

.toast-body {
    color: var(--text-primary);
    font-size: var(--fs-base);
    padding: 0.75rem 1rem;
}

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

@keyframes fadeOut {
    from {
        opacity: 1;
    }
    to {
        opacity: 0;
        transform: translateX(50px);
    }
}

.toast.hiding {
    animation: fadeOut 0.3s ease forwards;
}

/* ==========================================================================
   9. DataTables Dark Theme
   ========================================================================== */

table.dataTable thead th,
table.dataTable thead td {
    background-color: rgba(255, 255, 255, 0.03) !important;
    color: var(--text-muted) !important;
    border-bottom: 1px solid var(--border-color) !important;
    font-weight: 600;
    font-size: var(--fs-sm);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

table.dataTable tbody tr {
    background-color: transparent !important;
}

table.dataTable tbody tr:hover {
    background-color: rgba(var(--accent-primary-rgb), 0.05) !important;
}

table.dataTable tbody td {
    border-bottom: 1px solid var(--border-color) !important;
    color: var(--text-secondary);
}

/* Search Input (hidden for now — restore f in dom option to re-enable)
div.dt-container div.dt-search input {
    background-color: var(--bg-input) !important;
    color: var(--text-primary) !important;
    border: 1px solid var(--border-color) !important;
    border-radius: var(--border-radius) !important;
    padding: 0.375rem 0.75rem;
}

div.dt-container div.dt-search input:focus {
    border-color: var(--accent-primary) !important;
    box-shadow: 0 0 0 3px rgba(var(--accent-primary-rgb), 0.25) !important;
    outline: none;
}

div.dt-container div.dt-search label {
    color: var(--text-secondary);
    font-size: var(--fs-sm);
}
*/

/* Right-align pagination */
div.dt-container div.dt-paging {
    display: flex;
    justify-content: flex-end;
    margin-top: 0.75rem;
}

/* Pagination Buttons (Bootstrap 5 pagination component) */
div.dt-container div.dt-paging .page-link {
    color: var(--text-secondary) !important;
    background: var(--bg-card) !important;
    border: 1px solid var(--border-color) !important;
}

div.dt-container div.dt-paging .page-link:hover {
    background: var(--bg-card-hover) !important;
    color: var(--text-primary) !important;
    border-color: var(--border-color-light) !important;
}

div.dt-container div.dt-paging .page-item.active .page-link,
div.dt-container div.dt-paging .page-item.active .page-link:hover {
    background: var(--accent-primary) !important;
    color: #fff !important;
    border-color: var(--accent-primary) !important;
}

div.dt-container div.dt-paging .page-item.disabled .page-link,
div.dt-container div.dt-paging .page-item.disabled .page-link:hover {
    color: var(--text-muted) !important;
    background: var(--bg-card) !important;
    opacity: 0.5;
}

table.dataTable.table-hover tbody tr:hover > * {
    --bs-table-bg-state: rgba(var(--accent-primary-rgb), 0.05);
}

/* ==========================================================================
   10. Micro-Interactions & Animations
   ========================================================================== */

/* Focus States

   One ring. There were five: a 20%-alpha blue glow on fields, a 25%-alpha one
   on buttons, Bootstrap's own 25%-alpha #0d6efd on sidebar links (the faintest
   thing in the app, on #111827), the browser's thin default on plain links,
   and nothing at all on the update prompt's buttons. Somebody tabbing through
   a page could not learn what "here" looks like, and at a quarter opacity most
   of them were under the 3:1 a focus indicator needs.

   A field keeps a ring as a box-shadow, because its outline is its border.
   Everything else gets an outline, which follows the control's own radius and
   costs no layout. Bootstrap sets `outline: 0` plus a faint box-shadow on
   several of these at the same specificity; this file loads after it. */
.form-control:focus,
.form-select:focus,
.form-check-input:focus {
    outline: none;
}

a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
[role="button"]:focus-visible,
.btn:focus-visible,
.btn-check:focus-visible + .btn,
.btn-close:focus-visible,
.nav-link:focus-visible,
.dropdown-item:focus-visible,
.page-link:focus-visible,
.accordion-button:focus-visible {
    outline: 2px solid var(--focus-colour);
    outline-offset: 2px;
    box-shadow: none;
}

/* On a fill of the accent itself the link blue disappears into it. */
.btn-primary:focus-visible,
.skip-link:focus-visible {
    outline-color: var(--text-primary);
}

/* A card that is a link says so by its edge, not by moving. */
.card-hover:hover {
    border-color: var(--border-color-light);
    box-shadow: var(--shadow-md);
}

/* Link Transition */
.nav-link {
    transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease, border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}

/* Skeleton Loading */
/* A frame is a block: Turbo says so with a <style> it injects, which the
   nonce CSP has no reason to let through. */
turbo-frame {
    display: block;
}

.skeleton-line {
    height: 0.9rem;
    margin-bottom: 0.75rem;
    width: 100%;
}

.skeleton-line:last-child {
    width: 60%;
    margin-bottom: 0;
}

.skeleton-line-title {
    height: 1.25rem;
    width: 40%;
    margin-bottom: 1.25rem;
}

/* On a card the shimmer has to be told from the card it sits on. */
.card .skeleton {
    background: linear-gradient(90deg, var(--bg-card-hover) 25%, var(--border-color) 50%, var(--bg-card-hover) 75%);
    background-size: 200% 100%;
}

.skeleton {
    background: linear-gradient(90deg, var(--bg-card) 25%, var(--bg-card-hover) 50%, var(--bg-card) 75%);
    background-size: 200% 100%;
    animation: shimmer 1.5s infinite;
    border-radius: var(--border-radius);
}

@keyframes shimmer {
    0% {
        background-position: -200% 0;
    }
    100% {
        background-position: 200% 0;
    }
}

/* Pulse Indicator */
.pulse {
    animation: pulse 2s infinite;
}

@keyframes pulse {
    0% {
        opacity: 1;
    }
    50% {
        opacity: 0.5;
    }
    100% {
        opacity: 1;
    }
}

/* Quick Links Card */
.quick-link-card {
    transition: background-color 0.25s var(--ease-out), border-color 0.25s var(--ease-out), box-shadow 0.25s var(--ease-out);
    cursor: pointer;
    text-decoration: none;
    border-radius: var(--border-radius-xl);
}

.quick-link-card:hover {
    box-shadow: var(--shadow-md);
    border-color: var(--accent-primary);
}

.quick-link-card:hover i {
    color: var(--accent-primary);
}

/* ==========================================================================
   11. Responsive Adjustments
   ========================================================================== */

@media (max-width: 1199.98px) {
    .stat-card .card-body :is(h3, .h3),
    .stat-card-purple .card-body :is(h3, .h3) {
        font-size: var(--fs-xl);
    }

    .stat-card .card-body .fs-1,
    .stat-card-purple .card-body .fs-1 {
        font-size: var(--fs-2xl) !important;
    }

    .stat-card .card-body :is(h6, .h6),
    .stat-card-purple .card-body :is(h6, .h6) {
        font-size: var(--fs-xs);
    }
}

/* ── Top Bar Responsive ── */
@media (max-width: 575.98px) {
    .topbar-search {
        display: none;
    }

    .topbar-user-name {
        display: none;
    }
}

@media (min-width: 576px) and (max-width: 767.98px) {
    .topbar-search {
        flex: 0 1 200px;
    }
}

/* ---------- Tablet Breakpoint ---------- */
@media (max-width: 991.98px) {
    .page-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.75rem;
    }

    .stat-card .card-body :is(h3, .h3),
    .stat-card-purple .card-body :is(h3, .h3) {
        font-size: var(--fs-lg);
    }

    .stat-card .card-body .fs-1,
    .stat-card-purple .card-body .fs-1 {
        font-size: var(--fs-2xl) !important;
    }

    .sidebar .nav-link {
        font-size: var(--fs-sm);
    }
}

@media (max-width: 767.98px) {
    .page-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.75rem;
    }

    /* On a phone the page's actions share their rows, and the one that leads
       has a row to itself. Every button used to be a full-width row of its
       own — and one inside a form was only as wide as its label, so a member's
       page opened on five ragged rows of buttons and put the first card 859px
       down. */
    .page-header .btn {
        width: 100%;
    }

    /* In the header partial's own row they grow to share it instead: a width
       of 100% wraps every button however much room is left. One inside a form
       fills its form, which is the thing that grows. */
    .page-header > .d-flex > * {
        flex: 1 1 auto;
    }

    .page-header > .d-flex > .btn {
        width: auto;
    }

    .page-header > .d-flex > .btn-primary {
        flex-basis: 100%;
    }

    .stat-card .stat-value {
        font-size: var(--fs-xl);
    }

    .stat-card .card-body :is(h3, .h3),
    .stat-card-purple .card-body :is(h3, .h3) {
        font-size: var(--fs-lg);
    }

    .stat-card .card-body .fs-1,
    .stat-card-purple .card-body .fs-1 {
        font-size: var(--fs-xl) !important;
    }

    .stat-card .card-body :is(h6, .h6),
    .stat-card-purple .card-body :is(h6, .h6) {
        font-size: var(--fs-xs);
    }

    /* Offcanvas sidebar on mobile — slides over content */
    .sidebar {
        min-height: auto;
        width: var(--sidebar-width) !important;
        background: var(--bg-sidebar);
    }

    .sidebar .offcanvas-header {
        background: var(--bg-sidebar);
        color: var(--text-primary);
    }

    .sidebar .offcanvas-header .btn-close {
        filter: invert(1) grayscale(100%) brightness(200%);
    }

    /* Disable icon-only mode on mobile */
    .sidebar.sidebar-icon-only {
        width: var(--sidebar-width) !important;
        min-width: var(--sidebar-width) !important;
        max-width: var(--sidebar-width) !important;
    }

    .sidebar.sidebar-icon-only .sidebar-label,
    .sidebar.sidebar-icon-only .sidebar-heading-text,
    .sidebar.sidebar-icon-only .sidebar-chevron {
        display: inline !important;
    }

    .sidebar.sidebar-icon-only .sidebar-section-icon {
        display: none !important;
    }

    .sidebar.sidebar-icon-only .sidebar-search-wrap {
        display: block !important;
    }

    .sidebar.sidebar-icon-only .sidebar-flyout {
        display: none !important;
    }

    .sidebar.sidebar-icon-only .sidebar-section-links {
        display: block !important;
    }

    .sidebar.sidebar-icon-only .nav-link {
        justify-content: flex-start;
        font-size: var(--fs-base);
    }

    .sidebar.sidebar-icon-only .nav-link i {
        margin-right: 0.5rem;
    }

    .sidebar-collapse-btn {
        display: none !important;
    }

    /* Touch-friendly minimum tap targets (48px).
     *
     * Vertical padding only. The shorthand this replaces also reset the left
     * padding, which silently flattened the section indent at exactly the
     * width where it is needed most — a front-desk tablet. min-height does the
     * work of making the target big enough; the horizontal side belongs to the
     * rules that express hierarchy. */
    .sidebar .nav-link {
        min-height: 48px;
        display: flex;
        align-items: center;
        padding-top: 0.625rem;
        padding-bottom: 0.625rem;
    }

    .btn-sm {
        min-height: 40px;
        padding: 0.5rem 0.75rem;
    }

    .breadcrumb-item a {
        display: inline-block;
        min-height: 32px;
        line-height: 32px;
    }

    .content-area {
        padding-bottom: 1.5rem;
    }

    /* Stack table action buttons vertically on mobile */
    td .btn-group {
        display: flex;
        flex-direction: column;
        gap: 0.25rem;
    }

    td .btn-group .btn {
        width: 100%;
    }
}

/* Light mode close button in offcanvas */
[data-bs-theme="light"] .sidebar .offcanvas-header .btn-close {
    filter: none;
}

/* ==========================================================================
   12. Print Styles
   ========================================================================== */

@media print {
    body {
        background: #fff !important;
        color: #000 !important;
    }

    .topbar,
    .sidebar,
    .toast-container,
    .btn,
    .breadcrumb {
        display: none !important;
    }

    .card {
        background: #fff !important;
        border: 1px solid #ddd !important;
        color: #000 !important;
        box-shadow: none !important;
    }

    .table {
        color: #000 !important;
    }

    .content-area {
        background: #fff !important;
    }

    /* A wrapper whose whole job is to scroll cannot scroll on paper — it
       clips, and the columns past the fold are simply gone. Every report
       table is inside one of these, so a wide report printed as-is lost its
       right-hand columns silently (UX-L14). */
    .table-responsive {
        overflow: visible !important;
    }

    /* A table that runs past one page repeats its header on the next, and no
       row is split down the middle. */
    thead {
        display: table-header-group;
    }

    tr {
        break-inside: avoid;
    }

    /* A figure and its chart are single objects: better to move one to the
       next page than to cut it in half. Long table cards are deliberately
       left free to break, or a report of any length would start on page two. */
    .stat-card,
    canvas {
        break-inside: avoid;
    }
}

/* ==========================================================================
   13. Tom Select Dark Theme Overrides
   ========================================================================== */

[data-bs-theme="dark"] .ts-wrapper .ts-control {
    background-color: #212529;
    color: #dee2e6;
    border-color: #495057;
}

[data-bs-theme="dark"] .ts-wrapper .ts-control input {
    color: #dee2e6;
}

[data-bs-theme="dark"] .ts-dropdown {
    background-color: #2b3035;
    color: #dee2e6;
    border-color: #495057;
}

[data-bs-theme="dark"] .ts-dropdown .option:hover,
[data-bs-theme="dark"] .ts-dropdown .active {
    background-color: #0d6efd;
    color: #fff;
}

[data-bs-theme="dark"] .ts-wrapper .ts-control .item {
    background-color: #495057;
    color: #dee2e6;
    border-color: #6c757d;
}

[data-bs-theme="dark"] .ts-wrapper.focus .ts-control {
    border-color: #0d6efd;
    box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}

[data-bs-theme="dark"] .plugin-clear_button .clear-button {
    color: #adb5bd;
}

/* ==========================================================================
   14. Turbo Loading Bar
   ========================================================================== */

.turbo-progress-bar {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 9999;
    height: 3px;
    width: 0;
    background: var(--accent-primary);
    transition: width 0.3s ease;
}

.turbo-progress-bar.loading {
    animation: turbo-bar 2s ease-in-out infinite;
}

@keyframes turbo-bar {
    0% { width: 0; }
    50% { width: 70%; }
    100% { width: 90%; }
}

/* ==========================================================================
   15. Form Validation States
   ========================================================================== */

.form-control.is-invalid,
.form-select.is-invalid {
    border-color: var(--accent-danger) !important;
    box-shadow: 0 0 0 3px rgba(var(--accent-danger-rgb), 0.25);
}

.form-control.is-valid,
.form-select.is-valid {
    border-color: var(--accent-success) !important;
}

.invalid-feedback {
    color: var(--accent-danger);
    font-size: var(--fs-sm);
    margin-top: 0.25rem;
}

/* ==========================================================================
   16. Toast Enhancements
   ========================================================================== */

.toast-container .toast {
    animation: toast-slide-in 0.3s ease forwards;
    margin-bottom: 0.5rem;
    position: relative;
    overflow: hidden;
}

@keyframes toast-slide-in {
    from { transform: translateY(-8px); opacity: 0; }
    to { transform: translateY(0); opacity: 1; }
}

.toast.hiding {
    animation: toast-slide-out 0.3s ease forwards;
}

@keyframes toast-slide-out {
    from { transform: translateY(0); opacity: 1; }
    to { transform: translateY(-8px); opacity: 0; }
}

.toast-progress {
    position: absolute;
    bottom: 0;
    left: 0;
    height: 3px;
    background: rgba(255, 255, 255, 0.5);
    animation: toast-countdown 5s linear forwards;
}

@keyframes toast-countdown {
    from { width: 100%; }
    to { width: 0%; }
}

/* =============================================================================
   Utility Classes — extracted from inline styles to reduce CSP risk
   Note: Dynamic values (progress widths, rank colors) still require inline styles
   because CSP nonces don't apply to style="" attributes.
   ============================================================================= */

/* Icon sizing */
.icon-xs { font-size: var(--fs-xs); }
.icon-sm { font-size: var(--fs-sm); }
.icon-lg { font-size: var(--fs-display-md); }
.icon-xl { font-size: var(--fs-display-lg); }
.icon-2xl { font-size: var(--fs-display-xl); }

/* Progress bar height variants */
.progress-xs { height: 6px; }
.progress-sm { height: 8px; }

/* Circle/avatar containers */
.circle-48 { width: 48px; height: 48px; }
.circle-56 { width: 56px; height: 56px; }
.circle-80 { width: 80px; height: 80px; }
.circle-100 { width: 100px; height: 100px; }

/* Profile photo sizing */
.photo-sm { width: 80px; height: 80px; object-fit: cover; }
.photo-lg { width: 100px; height: 100px; object-fit: cover; }
.photo-upload-wrapper { position: relative; display: inline-block; cursor: pointer; }

/* Badge numbered circle (account/billing pages) */
.badge-numbered { width: 28px; height: 28px; line-height: 20px; }

/* Layout containers */
.container-narrow { max-width: 650px; margin: 0 auto; }

/* Cart/scrollable containers */
.scroll-container-sm { max-height: 300px; overflow-y: auto; }

/* Form input widths */
.input-w-xs { max-width: 100px; }
.input-w-sm { max-width: 200px; }
.input-w-md { max-width: 220px; }

/* Country code styling */
.text-upper { text-transform: uppercase; }

/* Impersonation banner */
/* The prompt the service worker puts up when a new version is waiting
   (UX-L16). At the foot of the page and the size of what it says. It was a
   sticky bar across the top, the twin of the impersonation banner below — but
   impersonating somebody is a state the reader must not forget, and a waiting
   release is not. The bar sat over the fixed top bar and took 91px off a phone.
   Coloured as a card, so its buttons are the ordinary ones and carry the
   ordinary focus ring. */
.app-update-banner {
    position: fixed;
    left: 50%;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    transform: translateX(-50%);
    z-index: 1060;
    width: max-content;
    max-width: calc(100vw - 32px);
    background: var(--bg-card);
    color: var(--text-primary);
    border: 1px solid var(--border-color-light);
    border-radius: var(--border-radius-lg);
    box-shadow: var(--shadow-lg);
    padding: 10px 12px 10px 16px;
    font-size: var(--fs-base);
    font-weight: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px 12px;
}

/* Clear of the member portal's tab bar, which is fixed to the same edge. */
@media (max-width: 767.98px) {
    .has-bottom-tabs .app-update-banner {
        bottom: calc(76px + env(safe-area-inset-bottom, 0px));
    }
}

@media print {
    .app-update-banner { display: none; }
}

.impersonation-banner {
    position: sticky;
    top: 0;
    z-index: 1060;
    background: #ffc107;
    color: #000;
    padding: 8px 16px;
    text-align: center;
    font-size: var(--fs-base);
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
}
.impersonation-code {
    font-size: var(--fs-sm);
    background: rgba(0,0,0,0.1);
    padding: 2px 6px;
    border-radius: 3px;
}

/* Notification dropdown */
.notification-menu {
    z-index: 1050;
    max-height: 400px;
    overflow-y: auto;
    min-width: 100%;
    width: 360px;
    left: 0;
}
.notification-menu-mobile {
    z-index: 1050;
    max-height: 60vh;
    overflow-y: auto;
    width: 100%;
}
.notification-count-icon { font-size: var(--fs-count); }

/* Streak/custom colors */
.text-streak { color: #fd7e14; }

/* Achievement badge item sizing (progress page) */
.badge-achievement { width: 80px; }

/* Product title in POS grid */
.text-product-name { font-size: var(--fs-sm); }

/* Search results dropdown */
.search-results-dropdown {
    z-index: 1050;
    max-height: 400px;
    overflow-y: auto;
    min-width: 100%;
    width: 360px;
    left: 0;
}
.search-results-dropdown-mobile {
    z-index: 1050;
    max-height: 60vh;
    overflow-y: auto;
    width: 100%;
}

/* ==========================================================================
   18. Changing Page
   ========================================================================== */

/* A change of page is one quick cross-fade, and the frame stays still.

   There was an entrance animation here instead: every block of every page
   faded and slid up 12px over 0.35s, the second, third, fourth and fifth each
   starting 0.05s after the last, and filled `backwards` — so until its turn
   came a block was invisible. Half a second before a page had settled, on
   every navigation, in a tool somebody works in all day; the same entrance on
   every section of every screen, which is decoration and not a signal; and
   the top bar and sidebar redrawn with it each time.

   It also had a history. Filled `both`, it left a transform on the row for
   the life of the page, which made the row the containing block for every
   fixed-position modal inside it: dialogs opened off screen behind a backdrop
   that swallowed every click. `backwards` fixed that and a test pinned the
   word. With no entrance animation there is no transform to keep, and the test
   now refuses one outright (tests/Style/PageEntranceAnimationTest.php).

   The view transition is Turbo 8's, switched on by the meta tag in
   base.html.twig, and only where the browser has it; elsewhere a page simply
   appears, which is what it did before anybody animated it. The top bar and
   sidebar are named so they are carried across rather than faded out and in.
   Turbo skips the transition altogether for anybody who has asked for less
   motion; the rule below is the second lock on that door. */
::view-transition-old(root),
::view-transition-new(root) {
    animation-duration: 0.14s;
    animation-timing-function: ease-out;
}

.topbar { view-transition-name: app-topbar; }

@media (min-width: 768px) {
    /* From md only: below that the sidebar is an offcanvas drawer, closed and
       off screen, and naming it would animate it sliding in from there. */
    .sidebar { view-transition-name: app-sidebar; }
}

@media (prefers-reduced-motion: reduce) {
    ::view-transition-group(*),
    ::view-transition-old(*),
    ::view-transition-new(*) {
        animation: none !important;
    }
}

/* ==========================================================================
   19. Tabular Figures for Financial Data
   ========================================================================== */

.display-6,
.table td:nth-child(n),
.stat-value,
.badge,
[data-financial] {
    font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   19. Keyboard Shortcut Hint for Search
   ========================================================================== */

.search-shortcut-hint {
    position: absolute;
    right: 0.5rem;
    top: 50%;
    transform: translateY(-50%);
    pointer-events: none;
    font-size: var(--fs-xs);
    color: var(--text-muted);
    border: 1px solid var(--border-color);
    border-radius: 4px;
    padding: 0.1rem 0.35rem;
    line-height: 1;
    opacity: 0.7;
}

/* ==========================================================================
   20. Reduced Motion Support
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    .skeleton {
        animation: none !important;
        background: var(--bg-card-hover) !important;
    }

    .turbo-progress-bar {
        animation: none !important;
    }
}

/* ==========================================================================
   21. Skip Link (Accessibility)
   ========================================================================== */

.skip-link {
    position: absolute;
    top: -100%;
    left: 0;
    z-index: 10000;
    padding: 0.75rem 1.5rem;
    background: var(--accent-primary);
    color: #fff;
    font-weight: 600;
    text-decoration: none;
    border-radius: 0 0 var(--border-radius) 0;
}

.skip-link:focus {
    top: 0;
    color: #fff;
}

/* ==========================================================================
   22. Responsive Table → Card (Mobile)
   ========================================================================== */

/* Product image containers — fluid height */
.product-img-container {
    height: 200px;
    object-fit: cover;
}

.product-img-placeholder {
    height: 200px;
}

.product-detail-placeholder {
    height: 300px;
}

@media (min-width: 576px) {
    .product-detail-placeholder {
        height: 400px;
    }
}

/* A run of dashboard figures that would leave one alone on its last row
   (see dashboard/admin.html.twig). Five abreast where there is room; below
   that three to a row, the last row growing to fill it. */
.widget-run-even {
    flex-grow: 1;
}

@media (min-width: 1200px) {
    /* A basis just under a fifth, grown to fill. At exactly 20% five of them
       come to a rounding error more than the row and the fifth wraps — alone,
       and now full width, which is the fault this was written to end. */
    .widget-run-even {
        flex: 1 0 18%;
        max-width: none;
    }
}

/* The strip at the head of a member's page: four figures somebody reads at a
   glance, so the numbers line up and the labels stay quiet. */
.member-figures dd .fs-4 {
    font-variant-numeric: tabular-nums;
    line-height: 1.2;
}

/* Six tabs do not fit a phone. They scroll sideways as one row rather than
   wrapping into a second, which would read as two sets of tabs. */
.member-tabs {
    scrollbar-width: none;
}

.member-tabs::-webkit-scrollbar {
    display: none;
}

/* Today on the staff's weekly schedule: marked by its edge, and clear of the
   sticky top bar when the page opens on it. */
.schedule-today {
    scroll-margin-top: calc(var(--topbar-height) + 1rem);
    border-color: var(--text-link);
    box-shadow: 0 0 0 1px var(--text-link);
}

/* The filter bar's fold (see _partials/filter_bar). From 768px up the two
   wrappers vanish from layout, so every field sits in the one row it always
   did and Bootstrap's collapse has nothing to hide. Below that they are an
   ordinary collapsed row. */
@media (min-width: 768px) {
    .filter-more-wrap,
    .filter-more-wrap > .filter-more.collapse {
        display: contents !important;
    }
}

/* What is applied, each with its own way off. */
.filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    min-height: 32px;
    padding: 0.2rem 0.7rem;
    border-radius: 999px;
    background: rgba(var(--accent-primary-rgb), 0.12);
    color: var(--text-link);
    font-size: var(--fs-sm);
    font-weight: 500;
    text-decoration: none;
}

.filter-chip:hover {
    background: rgba(var(--accent-primary-rgb), 0.2);
    color: var(--text-link);
}

.filter-chip-name {
    color: var(--text-secondary);
    font-weight: 400;
}

.filter-chip .bi {
    font-size: var(--fs-xs);
}

@media (pointer: coarse) {
    .filter-chip { min-height: 40px; }
}

/* A list of records as cards (data_table's `mobile: 'cards'`). The older
   .table-card-mobile below takes the row's FIRST cell as the card's title,
   which is right for a cart and wrong for any list with a tick box in front.
   Here the cells say what they are:

     data-card="select"   the tick box, pinned to the corner
     data-card="title"    the name: the card's heading, no label
     data-card="actions"  the row's buttons, along the foot
     data-label="Plan"    every other cell, printed as "Plan ......... value"

   EveryTableCanBeReadOnAPhoneTest holds a table that asks for this to a title
   and to labels. */
@media (max-width: 767.98px) {
    .table-cards thead {
        position: absolute;
        left: -9999px;
    }

    .table-cards,
    .table-cards tbody {
        display: block;
    }

    .table-cards tbody tr {
        display: block;
        position: relative;
        background: var(--bg-card);
        border: 1px solid var(--border-color-light);
        border-radius: var(--border-radius-lg);
        padding: 0.85rem 1rem;
        margin-bottom: 0.75rem;
    }

    .table-cards tbody tr:has(> td[data-card="select"]) {
        padding-left: 2.9rem;
    }

    .table-cards tbody td {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 1rem;
        width: auto !important;
        padding: 0.25rem 0;
        border: 0;
        background: none !important;
        box-shadow: none !important;
        white-space: normal !important;
        text-align: right;
    }

    .table-cards tbody td[data-label]::before {
        content: attr(data-label);
        flex-shrink: 0;
        font-size: var(--fs-sm);
        color: var(--text-secondary);
        text-align: left;
    }

    /* Nothing to say, so no row for it. A table cell wants its dash, or the
       column reads as a rendering fault; a card printing "Phone ... -" is a
       line spent saying nothing. The placeholder says it is one. */
    .table-cards tbody td[data-label]:empty,
    .table-cards tbody td[data-label]:has(> .no-value:only-child) {
        display: none;
    }

    .table-cards tbody td[data-card="select"] {
        position: absolute;
        top: 0.95rem;
        left: 1rem;
        padding: 0;
    }

    .table-cards tbody td[data-card="title"] {
        justify-content: flex-start;
        text-align: left;
        font-size: var(--fs-md);
        font-weight: 600;
        padding-bottom: 0.4rem;
    }

    .table-cards tbody td[data-card="actions"] {
        justify-content: flex-start;
        padding-top: 0.6rem;
        margin-top: 0.35rem;
        border-top: 1px solid var(--border-color);
    }

    .table-cards tbody td[data-card="actions"] .btn {
        min-width: 44px;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* A belt is a colour and a name; on a desk the name is a tooltip, and on a
       touch screen there is no such thing. */
    /* Bootstrap hides .visually-hidden with !important on every property, so
       showing it again has to say so too. */
    .table-cards .rank-name,
    .table-cards .mark-label {
        position: static !important;
        width: auto !important;
        height: auto !important;
        margin: 0 0 0 0.35rem !important;
        padding: 0 !important;
        clip: auto !important;
        overflow: visible !important;
        white-space: normal !important;
        font-size: var(--fs-sm);
        color: var(--text-secondary);
    }

    /* The list usually sits in a card of its own. With every row a card that
       is a card inside a card: two borders, two paddings, and 32px of a 390px
       screen spent on the outer one. */
    .card:has(> .card-body .table-cards),
    .card:has(> .table-responsive .table-cards) {
        background: transparent;
        border: 0;
        box-shadow: none;
    }

    .card:has(> .card-body .table-cards) > .card-body {
        padding: 0;
    }
}

/* Quantity input — fluid width */
.qty-input {
    width: 70px;
    min-width: 60px;
    max-width: 90px;
}

/* Cart table — transforms to stacked card layout on mobile */
@media (max-width: 767.98px) {
    .table-card-mobile thead {
        display: none;
    }

    .table-card-mobile tbody tr {
        display: block;
        background: var(--bg-card);
        border: 1px solid var(--border-color);
        border-radius: var(--border-radius-lg);
        padding: 1rem;
        margin-bottom: 0.75rem;
    }

    .table-card-mobile tbody td {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 0.35rem 0;
        border: none;
        background: none !important;
    }

    .table-card-mobile tbody td::before {
        content: attr(data-label);
        font-weight: 600;
        font-size: var(--fs-sm);
        color: var(--text-secondary);
        margin-right: 1rem;
        flex-shrink: 0;
    }

    .table-card-mobile tbody td:first-child {
        padding-bottom: 0.5rem;
        margin-bottom: 0.5rem;
        border-bottom: 1px solid var(--border-color);
    }

    .table-card-mobile tbody td:first-child::before {
        display: none;
    }

    /* Invoice/receipt table — simpler stacking */
    .table-stack-mobile thead {
        display: none;
    }

    .table-stack-mobile tbody tr {
        display: block;
        padding: 0.75rem 0;
        border-bottom: 1px solid var(--border-color);
    }

    .table-stack-mobile tbody td {
        display: block;
        padding: 0.15rem 0;
        border: none;
        text-align: left !important;
        background: none !important;
    }

    .table-stack-mobile tbody td::before {
        content: attr(data-label);
        font-weight: 600;
        font-size: var(--fs-xs);
        color: var(--text-muted);
        display: block;
    }

    .table-stack-mobile tbody td:first-child::before {
        display: none;
    }

    .table-stack-mobile tbody td:first-child {
        font-weight: 600;
        font-size: var(--fs-base);
        padding-bottom: 0.25rem;
    }

    .table-stack-mobile tfoot td {
        text-align: right !important;
    }

    /* Remove fixed column widths on mobile */
    .table-card-mobile th,
    .table-card-mobile td,
    .table-stack-mobile th,
    .table-stack-mobile td {
        width: auto !important;
    }
}

/* ==========================================================================
   23. Mobile Bottom Tab Bar (Member Portal)
   ========================================================================== */

.bottom-tab-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1040;
    display: flex;
    justify-content: space-around;
    align-items: stretch;
    background: var(--bg-sidebar);
    border-top: 1px solid var(--border-color);
    padding-bottom: env(safe-area-inset-bottom, 0);
    box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
}

.bottom-tab {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    flex: 1;
    padding: 0.5rem 0.25rem;
    min-height: 56px;
    color: var(--text-muted);
    text-decoration: none;
    /* 12px is the floor. These were 10px, which is small enough on a phone
       that the icon ends up doing the work and the word underneath it is
       decoration. All five labels still fit: the longest, "Schedule", needs
       about 50px of the ~56px a fifth of a 320px screen leaves after padding,
       and there is more room on every phone wider than that. */
    font-size: var(--fs-xs);
    font-weight: 500;
    letter-spacing: 0.02em;
    transition: color var(--transition-speed) ease;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

.bottom-tab i {
    font-size: var(--fs-xl);
    margin-bottom: 0.125rem;
    transition: transform var(--transition-speed) ease;
}

/* The unread count, on the icon rather than beside the label: the label has
   0.75rem to work with and a number after it pushes the word off centre. */
.bottom-tab-badge {
    position: absolute;
    top: 0.25rem;
    left: 50%;
    margin-left: 0.15rem;
    /* 11px: legible, and still narrow enough that "99+" sits over the icon
       rather than beside it. */
    font-size: var(--fs-count);
    padding: 0.15em 0.4em;
}

.bottom-tab:hover,
.bottom-tab:focus {
    color: var(--text-secondary);
    text-decoration: none;
}

.bottom-tab.active {
    color: var(--accent-primary);
}

.bottom-tab.active i {
    transform: scale(1.1);
}

/* Add padding to main content so it's not hidden behind the tab bar */
@media (max-width: 767.98px) {
    .has-bottom-tabs .content-area {
        padding-bottom: calc(56px + env(safe-area-inset-bottom, 0) + 1rem) !important;
    }
}

/* ==========================================================================
   24. Bulk Action Bar
   ========================================================================== */
.bulk-action-bar {
    position: fixed;
    bottom: 1.5rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1035;
    animation: bulkBarSlideUp 0.2s ease-out;
}

@keyframes bulkBarSlideUp {
    from { opacity: 0; transform: translateX(-50%) translateY(1rem); }
    to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

.bulk-action-bar-inner {
    display: flex;
    align-items: center;
    gap: 1rem;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--border-radius-lg);
    padding: 0.625rem 1.25rem;
    box-shadow: var(--shadow-lg);
}

.bulk-action-count {
    font-weight: 600;
    font-size: var(--fs-base);
    color: var(--accent-primary);
    white-space: nowrap;
}

.bulk-action-buttons {
    display: flex;
    gap: 0.375rem;
    flex-wrap: nowrap;
}

.bulk-check {
    cursor: pointer;
}

.bulk-select-th {
    width: 2.5rem;
}

@media (prefers-reduced-motion: reduce) {
    .bulk-action-bar { animation: none; }
}

/* ==========================================================================
   25. Keyboard Shortcuts Modal
   ========================================================================== */
.shortcut-list {
    margin: 0;
}

.shortcut-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.25rem 0;
}

.shortcut-row dt {
    flex-shrink: 0;
    font-weight: normal;
}

.shortcut-row dd {
    margin: 0;
    color: var(--text-secondary);
    font-size: var(--fs-sm);
}

kbd {
    display: inline-block;
    padding: 0.15rem 0.4rem;
    font-size: var(--fs-xs);
    font-family: inherit;
    color: var(--text-primary);
    background: var(--bg-body);
    border: 1px solid var(--border-color);
    border-radius: 0.25rem;
    box-shadow: 0 1px 0 var(--border-color);
    min-width: 1.5rem;
    text-align: center;
}


/* ── The MyMemberForge lockup ──────────────────────────────────────────
   Shield as an image, name as text. The name takes its colour from the
   theme, which is the whole reason it is not baked into the artwork: the
   drawn wordmark is navy and the default theme is navy. */
.brand {
    display: inline-flex;
    align-items: center;
    gap: .6em;
    text-decoration: none;
    color: inherit;
}

.brand-shield {
    width: auto;
    /* Height drives everything, so one variable sizes the whole lockup. */
    height: 1em;
    flex: none;
}

.brand-words {
    display: inline-flex;
    flex-direction: column;
    line-height: 1.05;
}

.brand-name {
    font-weight: 700;
    letter-spacing: -.01em;
    /* currentColor, not a hard fallback: the error pages and the 2FA step set
       their own body colour and never define the theme variables, and the name
       has to stay readable there rather than resolving to something invisible. */
    color: var(--text-primary, currentColor);
    white-space: nowrap;
}

.brand-tagline {
    font-size: .3em;
    font-weight: 500;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-secondary, #94a3b8);
    white-space: nowrap;
}

/* One font-size per variant; the shield and the name both follow it. */
.brand-sm { font-size: var(--fs-xl); }
.brand-md { font-size: var(--fs-display-sm); }
.brand-lg { font-size: var(--fs-display-md); }

/* The shield reads better slightly larger than the cap height beside it. */
.brand-sm .brand-shield { height: 1.6em; }
.brand-md .brand-shield { height: 1.7em; }
.brand-lg .brand-shield { height: 1.8em; }

/* Stacked, for the narrow card the sign-in pages use. */
.brand-stack {
    flex-direction: column;
    gap: .35em;
    text-align: center;
}

.brand-stack .brand-words { align-items: center; }
.brand-stack .brand-shield { height: 3.4em; }

/* ── Card reader status ──
   A dot on the topbar icon. Small on purpose: a connected reader is the normal
   state and should not announce itself; the colours are there for the glance
   somebody takes when a payment will not start. */
.reader-status-dot {
    position: absolute;
    right: .35rem;
    bottom: .35rem;
    width: .5rem;
    height: .5rem;
    border-radius: 50%;
    border: 1px solid var(--bg-body, #0f172a);
}

/* Three answers now, not two. There is no connection to be in the middle of —
   the reader is on, off, or not chosen — and "off" is worth telling apart from
   "none", because one is a walk to the counter and the other is a setting. */
.reader-status-online { background: #22c55e; }
.reader-status-offline { background: #ef4444; }
.reader-status-unknown { background: #94a3b8; }

/* Pulsing rather than static, because asking takes a moment and a still amber
   dot reads as a warning rather than as progress. */
.reader-status-looking {
    background: #f59e0b;
    animation: reader-status-pulse 1s ease-in-out infinite;
}

@keyframes reader-status-pulse {
    50% { opacity: .3; }
}

/* ── Dashboard stat tiles that drill down ────────────────────────────────────────────
   The chevron is the affordance that survives having no cursor, so a phone
   can tell a card that goes somewhere from one that does not. A pointer gets
   the movement as well, which is cheaper to read than the glyph.

   Motion is dropped for anybody who has asked for less of it: this is
   decoration, and the chevron carries the meaning on its own. */
.dash-stat-link {
    display: block;
    transition: box-shadow .12s ease;
}

.dash-stat-link:hover {
    box-shadow: var(--shadow-md);
}

.dash-stat-link:hover .bi-chevron-right {
    color: var(--bs-primary, #2563eb) !important;
}

/* The whole card is the target, so the ring has to be on the whole card —
   a focus outline around the chevron alone would point at the wrong thing. */
.dash-stat-link:focus-visible {
    outline: 2px solid var(--bs-primary, #2563eb);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .dash-stat-link { transition: none; }
    .dash-stat-link:hover { transform: none; }
}
