/* ================================================================
   Ghana School Management System - Custom Stylesheet
   ================================================================ */

/* ── TYPOGRAPHY ─────────────────────────────────────────────────── */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Fira+Code:wght@400;500&display=swap');

:root {
  /* --primary et al derive from layout.css's --brand-500/600/700 (the
     single source of truth for the user's chosen accent colour — see
     assets/css/themes.css's [data-accent="..."] blocks) rather than
     carrying their own hardcoded hex, so every .btn-primary/badge/
     link/focus-ring in this file repaints when the accent changes,
     with no per-component theme-awareness needed. The literal hex
     fallbacks only apply if layout.css somehow failed to load. */
  --primary:       var(--brand-500, #2563eb);
  --primary-light: color-mix(in srgb, var(--brand-500, #2563eb) 65%, white);
  --primary-dark:  var(--brand-700, #1d4ed8);
  --accent:        #e67e22;

  /* Re-point Bootstrap 5.3's own primary tokens at the same accent, so
     plain Bootstrap utility classes used throughout the view layer
     (text-primary, bg-primary, bg-primary bg-opacity-10, btn-outline-
     primary, border-primary, nav-link.active, form focus rings, ...)
     follow the chosen theme too — not just this file's own
     hand-written .btn-primary-style rules. */
  --bs-primary:              var(--brand-500, #2563eb);
  --bs-primary-rgb:          var(--brand-500-rgb, 37, 99, 235);
  --bs-link-color:           var(--brand-500, #2563eb);
  --bs-link-color-rgb:       var(--brand-500-rgb, 37, 99, 235);
  --bs-link-hover-color:     var(--brand-600, #1d4ed8);
  --bs-link-hover-color-rgb: var(--brand-500-rgb, 37, 99, 235);

  /* Checkbox/radio checked state — Bootstrap 5.3 does NOT chain these to
     --bs-primary the way simple utilities (.text-primary/.bg-primary/
     .border-primary) do; .form-check-input:checked's colour is its own
     separate pair of
     variables, hardcoded to Bootstrap's blue by default regardless of
     what --bs-primary is set to. Every row-select/select-all checkbox
     in the app was staying blue no matter the chosen theme until these
     are re-pointed too. */
  --bs-form-check-input-checked-bg-color:     var(--brand-500, #2563eb);
  --bs-form-check-input-checked-border-color: var(--brand-500, #2563eb);
  --bs-form-check-input-indeterminate-bg-color:     var(--brand-500, #2563eb);
  --bs-form-check-input-indeterminate-border-color: var(--brand-500, #2563eb);

  --success:       #10b981;
  --danger:        #ef4444;
  --warning:       #f59e0b;
  --info:          #3b82f6;
  /* --sidebar-w / --header-h live in layout.css (loaded after this file
     on every page via header.php) — that's the authoritative source for
     app-shell sizing, since the shell itself lives there now too. */
  --bg:            #f1f5f9;
  --card:          #ffffff;
  --card-2:        #f8fafc;
  --text:          #0f172a;
  --muted:         #94a3b8;
  --border:        #e2e8f0;
  --border-light:  #f1f5f9;
  /* Form controls specifically need more edge than --border gives most
     surfaces (a card outline can afford to be nearly invisible; an empty
     input box relies on its outline to read as "a field" at all) and
     --label-color needs to actually pass as text, not just a caption --
     --muted (#94a3b8, ~2.9:1 on white) is fine for a timestamp, not for
     "what is this field for". */
  --border-strong: #cbd5e1;
  --label-color:   #475569;
  --radius:        14px;
  /* Layered (two-shadow) depth instead of one flat, low-opacity shadow —
     DrillCost's cards/buttons use this same "close ambient shadow + far
     soft shadow" combination, which reads as genuinely elevated rather
     than just a faint edge. Single biggest lever for the "flat" feeling
     since --shadow-sm/-md drive .card/.kpi-card/.table-card/.db-panel
     app-wide. */
  --shadow-sm:     0 1px 3px rgba(15,23,42,.07), 0 1px 2px rgba(15,23,42,.05);
  --shadow-md:     0 10px 28px rgba(15,23,42,.12), 0 3px 8px rgba(15,23,42,.07);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  color-scheme: light;
}

/* ── DARK MODE ──────────────────────────────────────────────────── */
[data-theme="dark"] {
  color-scheme: dark;
  --bg:            #0b1120;
  --card:          #131c2e;
  --card-2:        #1a2540;
  --text:          #e2e8f0;
  --muted:         #64748b;
  --border:        #1e293b;
  --border-light:  #162032;
  --border-strong: #334155;
  --label-color:   #cbd5e1;
  --shadow-sm:     0 1px 3px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.25);
  --shadow-md:     0 10px 28px rgba(0,0,0,.5), 0 3px 8px rgba(0,0,0,.3);

  /* Bootstrap 5.3 builds nearly every component (tables, dropdowns, list
     groups, pagination, form validation text, ...) off these root tokens
     via var(). We never touch data-bs-theme, so without re-pointing these
     at our own palette, Bootstrap's parts silently stay light-mode —
     that's why table text/backgrounds ignored the toggle entirely. */
  --bs-body-color:              var(--text);
  --bs-body-bg:                 var(--bg);
  --bs-emphasis-color:          var(--text);
  --bs-emphasis-color-rgb:      226, 232, 240;
  --bs-secondary-color:         var(--muted);
  --bs-secondary-bg:            var(--card-2);
  --bs-tertiary-bg:             var(--card-2);
  --bs-border-color:            var(--border);
  --bs-border-color-translucent: var(--border);
  --bs-heading-color:           var(--text);
  --bs-link-color:              var(--primary-light);
  --bs-link-hover-color:        var(--primary-light);
}
[data-theme="dark"] .table-card .table tbody tr:hover > * { background-color: rgba(var(--brand-500-rgb, 37,99,235),.10) !important; }
[data-theme="dark"] body { background: var(--bg); color: var(--text); }
[data-theme="dark"] .card,
[data-theme="dark"] .card-header,
[data-theme="dark"] .table-card,
[data-theme="dark"] .modal-content { background: var(--card); color: var(--text); border-color: var(--border) !important; }
[data-theme="dark"] .card-header { background: var(--card) !important; border-bottom-color: var(--border) !important; }
[data-theme="dark"] .table-card .table th { background: var(--card-2) !important; color: var(--muted) !important; border-bottom-color: var(--border) !important; }
[data-theme="dark"] .table-card .table td { color: var(--text) !important; border-color: var(--border-light); }
[data-theme="dark"] .table-card .table tbody tr:hover { background: rgba(var(--brand-500-rgb, 37,99,235),.10); }
[data-theme="dark"] .form-control,
[data-theme="dark"] .form-select { background: var(--card-2) !important; border-color: var(--border-strong) !important; color: var(--text) !important; }
/* Browser autofill (password managers, saved logins) forces its own white/
   pale background on autofilled fields, ignoring the rule above — standard
   workaround: an absurdly long transition delay keeps the browser's fill
   color from visibly taking over. */
[data-theme="dark"] .form-control:-webkit-autofill,
[data-theme="dark"] .form-control:-webkit-autofill:hover,
[data-theme="dark"] .form-control:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--text);
  -webkit-box-shadow: 0 0 0 1000px var(--card-2) inset;
  box-shadow: 0 0 0 1000px var(--card-2) inset;
  transition: background-color 9999s ease-in-out 0s;
  caret-color: var(--text);
}
[data-theme="dark"] .form-label { color: var(--label-color); }
[data-theme="dark"] .dropdown-menu { background: var(--card); border-color: var(--border); }
[data-theme="dark"] .dropdown-item { color: var(--text); }
[data-theme="dark"] .dropdown-item:hover { background: var(--card-2); }
[data-theme="dark"] .nav-tabs { border-bottom-color: var(--border); }
[data-theme="dark"] .dataTables_wrapper .dataTables_filter input,
[data-theme="dark"] .dataTables_wrapper .dataTables_length select { background: var(--card-2); border-color: var(--border); color: var(--text); }
[data-theme="dark"] .btn-close { filter: invert(1); }
[data-theme="dark"] .loading-overlay { background: rgba(11,17,32,.85); }

/* Bootstrap utility classes carry fixed colors and ignore [data-theme] on
   their own — remap the ones used across views so text stays legible and
   light/white surfaces flip along with everything else. */
[data-theme="dark"] .text-dark,
[data-theme="dark"] .text-black      { color: var(--text) !important; }
[data-theme="dark"] .text-secondary,
[data-theme="dark"] .text-body-secondary { color: var(--muted) !important; }
[data-theme="dark"] .bg-light        { background: var(--card-2) !important; color: var(--text); }
[data-theme="dark"] .bg-white        { background: var(--card) !important; color: var(--text); }
[data-theme="dark"] .bg-secondary    { background: var(--card-2) !important; color: var(--text) !important; }
/* Bootstrap's table-variant classes (<thead class="table-light">, etc.) set
   their own --bs-table-bg/--bs-table-color locally on that element, which
   beats our root-level bridge since it's redeclared closer to the cells —
   this is the one Bootstrap pattern the bridge alone can't reach. */
[data-theme="dark"] .table-light {
  --bs-table-bg: var(--card-2);
  --bs-table-color: var(--muted);
  background-color: var(--card-2) !important;
  color: var(--muted) !important;
}
[data-theme="dark"] .table-dark {
  --bs-table-bg: var(--card-2);
  --bs-table-color: var(--text);
  background-color: var(--card-2) !important;
  color: var(--text) !important;
}
[data-theme="dark"] .table-secondary {
  --bs-table-bg: var(--card-2);
  --bs-table-color: var(--muted);
  background-color: var(--card-2) !important;
  color: var(--muted) !important;
}
[data-theme="dark"] .table-danger {
  --bs-table-bg: rgba(239,68,68,.15);
  --bs-table-color: #fca5a5;
  background-color: rgba(239,68,68,.15) !important;
  color: #fca5a5 !important;
}
[data-theme="dark"] .table-warning {
  --bs-table-bg: rgba(245,158,11,.15);
  --bs-table-color: #fcd34d;
  background-color: rgba(245,158,11,.15) !important;
  color: #fcd34d !important;
}
[data-theme="dark"] .table-success {
  --bs-table-bg: rgba(16,185,129,.15);
  --bs-table-color: #6ee7b7;
  background-color: rgba(16,185,129,.15) !important;
  color: #6ee7b7 !important;
}
[data-theme="dark"] .table-info {
  --bs-table-bg: rgba(59,130,246,.15);
  --bs-table-color: #93c5fd;
  background-color: rgba(59,130,246,.15) !important;
  color: #93c5fd !important;
}
[data-theme="dark"] .table-primary {
  --bs-table-bg: rgba(var(--brand-500-rgb, 37,99,235),.15);
  --bs-table-color: #93c5fd;
  background-color: rgba(var(--brand-500-rgb, 37,99,235),.15) !important;
  color: #93c5fd !important;
}

/* SweetAlert2 confirm/alert dialogs (confirmDelete, confirmAction, and every
   Swal.fire(...) call across the app) render their own popup with no
   awareness of [data-theme] at all — plain white regardless of theme. */
[data-theme="dark"] .swal2-popup           { background: var(--card) !important; color: var(--text); }
[data-theme="dark"] .swal2-title,
[data-theme="dark"] .swal2-html-container  { color: var(--text) !important; }
[data-theme="dark"] .swal2-close           { color: var(--muted) !important; }
[data-theme="dark"] .swal2-input,
[data-theme="dark"] .swal2-textarea,
[data-theme="dark"] .swal2-select          { background: var(--card-2) !important; color: var(--text) !important; border-color: var(--border) !important; }

/* Tom Select (searchable dropdowns, auto-applied to every <select> — see
   enhanceSelect() in app.js). Reskinned to match DrillCost's own select
   treatment (its Select2 rules, ported to our tokens) — rounded control,
   inset shadow, themed focus ring, accent-filled highlighted option.
   Search is already on by default for every enhanced select (only
   data-no-search opts a field out); this is the visual half of that. */
/* tom-select.bootstrap5's own CSS ships `.ts-wrapper{flex-grow:1;width:1%}`
   -- copied from Bootstrap's .input-group child pattern, which only makes
   sense when the immediate parent is itself display:flex (an input-group).
   Everywhere else (a plain Bootstrap .col-*, a form-label/control stack,
   any ordinary block container -- i.e. nearly every select in this app),
   that literal `width:1%` is just a CSS width, so the whole control
   collapses to a 1%-wide sliver instead of filling its column. This is
   very likely what was actually behind the "form fields not arranged
   well" reports earlier this session too. */
.ts-wrapper { width: 100%; }
/* ...except inside a real Bootstrap .input-group (e.g. a select sharing
   its row with a prefix/button addon) -- there the vendor's original
   flex-share sizing is exactly what's wanted, so restore it. */
.input-group > .ts-wrapper { width: 1%; flex: 1 1 auto; }
.ts-wrapper .ts-control {
  border-radius: 8px;
  border: 1px solid #ced4da;
  padding: 6px 10px;
  min-height: 38px;
  background-color: #fff;
  box-shadow: 0 1px 3px rgba(0,0,0,.02) inset;
  transition: border-color .2s ease-in-out, box-shadow .2s ease-in-out;
}
.ts-wrapper.focus .ts-control {
  border-color: var(--primary) !important;
  box-shadow: 0 0 0 4px rgba(var(--brand-500-rgb), .15) !important;
}
/* .focus (keyboard focus) and .dropdown-active (popup actually open) are
   different states -- .focus stays on the control after picking a value
   (so you can keep typing to search again) even though the dropdown
   itself closes right after the pick. The corner-flattening below needs
   to track "is there actually a dropdown below me right now", not just
   focus, or picking a value leaves a half-cut box with nothing under it
   -- flush-merges with .ts-dropdown only while it's genuinely open. */
.ts-wrapper.dropdown-active .ts-control {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.ts-dropdown {
  border-radius: 8px;
  border: 1px solid #e5e7eb;
  box-shadow: 0 8px 24px rgba(0,0,0,.12);
  /* No gap + square top corners + no top border: sits flush against
     .ts-control's now-flattened bottom edge, so control+dropdown read as
     one seamless box rather than two separate floating pieces — this is
     only ever visible while open, so no need to gate it on a :focus
     state. dropdown_input (a real search row physically inside the
     dropdown, like DrillCost's own Select2) would be the more literal
     match, but it's a confirmed tom-select bug specifically inside
     Bootstrap modals (closes the modal out from under itself) — this is
     the CSS-only way to get close to the same read without moving the
     input's DOM position, which is what actually breaks in a modal. */
  margin-top: 0;
  border-top: none;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  overflow: hidden;
  /* dropdownParent:'body' (see enhanceSelect() in app.js) reparents this
     to <body> to escape a modal's overflow clipping -- but that also
     lifts it OUT of the modal's own stacking context, so the vendor
     default z-index:10 then renders it BEHIND the modal (Bootstrap's
     .modal/.modal-backdrop sit around 1050-1060). It was technically
     still opening, just invisible under the modal -- looked and felt
     exactly like the control had silently become a plain text input.
     Comfortably above any Bootstrap component (modal/tooltip/popover
     all top out under 1090) fixes it regardless of context. */
  z-index: 3000;
}
.ts-dropdown .option { padding: 8px 12px; font-size: 13px; }
.ts-dropdown .option.active { background: var(--primary) !important; color: #fff !important; }
.ts-control .item { background: rgba(var(--brand-500-rgb), .1); color: var(--primary); border-radius: 5px; }
.ts-control .item.active { background: var(--primary); color: #fff; }
/* The dropdown_input plugin's own search row — pinned to the top of the
   open dropdown, like DrillCost's Select2 .select2-search--dropdown. */
.ts-dropdown .dropdown-input {
  border-radius: 8px 8px 0 0;
  font-size: 13px;
  background: var(--card-2, #f8f9fa);
  border-color: #e5e7eb;
}
.plugin-dropdown_input.focus .ts-dropdown .dropdown-input {
  border-color: var(--primary) !important;
  box-shadow: 0 0 0 3px rgba(var(--brand-500-rgb), .15) !important;
  outline: 0;
}

/* Dark mode — values match DrillCost's darkmode.css select2 block. */
[data-theme="dark"] .ts-control            { background: #253043 !important; color: #e5e7eb !important; border-color: rgba(255,255,255,.15) !important; box-shadow: none !important; }
[data-theme="dark"] .ts-control input      { color: #e5e7eb !important; }
[data-theme="dark"] .ts-control input::placeholder { color: #6b7280 !important; }
[data-theme="dark"] .ts-wrapper.focus .ts-control { border-color: var(--primary) !important; box-shadow: 0 0 0 3px rgba(var(--brand-500-rgb), .25) !important; }
[data-theme="dark"] .ts-wrapper.disabled .ts-control { background: var(--card-2) !important; opacity: .6; }
[data-theme="dark"] .ts-dropdown           { background: #1e2633 !important; color: #d1d5db !important; border-color: rgba(255,255,255,.12) !important; box-shadow: 0 8px 24px rgba(0,0,0,.5) !important; }
[data-theme="dark"] .ts-dropdown .option   { color: #d1d5db !important; }
[data-theme="dark"] .ts-dropdown .option.active { background: var(--primary) !important; color: #fff !important; }
[data-theme="dark"] .ts-dropdown .no-results,
[data-theme="dark"] .ts-dropdown .optgroup-header { color: var(--muted) !important; background: transparent !important; }
[data-theme="dark"] .ts-control .item      { background: rgba(var(--brand-500-rgb), .18) !important; color: #e5e7eb !important; }
[data-theme="dark"] .ts-control .item.active { background: var(--primary) !important; color: #fff !important; }
[data-theme="dark"] .ts-dropdown .dropdown-input { background: #253043 !important; border-color: rgba(255,255,255,.15) !important; color: #e5e7eb !important; }
[data-theme="dark"] .plugin-dropdown_input.focus .ts-dropdown .dropdown-input { border-color: var(--primary) !important; box-shadow: 0 0 0 3px rgba(var(--brand-500-rgb), .25) !important; }

code, pre, .font-mono { font-family: 'Fira Code', monospace; }
/* `clip`, not `hidden` — a deliberate safety net against any absolutely-
   positioned element appended to <body> (a popover, a dropdown, ...)
   that isn't perfectly clamped to the viewport, which would otherwise
   grow <body> itself and put a horizontal scrollbar on the WHOLE page
   instead of just scrolling inside whatever card it belongs to. Wide
   content (tables, etc.) still scrolls fine — that's handled by
   overflow-x:auto on .table-card and similar containers, not here.
   `hidden` looks equivalent but isn't: per spec it makes html/body
   establish their own scroll container, which silently breaks every
   position:sticky descendant on the page (its "stuck" reference frame
   becomes that container instead of the real viewport — the classic,
   widely-documented cause of "sticky just doesn't work"; Settings'
   .settings-nav was exactly this). `clip` clips the same overflow
   without creating a scroll container, so sticky keeps working. */
html { overflow-x: clip; }
body {
  background: var(--bg); color: var(--text);
  transition: background .22s ease, color .22s ease;
  overflow-x: clip;
  /* Crisper text rendering — most noticeable over the new gradient
     sidebar/topbar and darker card shadows, where unhinted anti-
     aliasing reads as slightly soft/blurry against a colour instead of
     a flat neutral background. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* ── SCROLLBARS ─────────────────────────────────────────────────── */
::-webkit-scrollbar        { width: 6px; height: 6px; }
::-webkit-scrollbar-track  { background: transparent; }
::-webkit-scrollbar-thumb  { background: #ccc; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #aaa; }

/* ── SIDEBAR NAV ────────────────────────────────────────────────── */
#sidebar .nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  border-radius: 9px;
  font-size: 10px;
  font-weight: 700;
  padding: 0 5px;
  margin-left: auto;
  background: var(--accent);
  color: #fff;
}

/* ── STAT CARDS ─────────────────────────────────────────────────── */
.stat-card {
  border-radius: var(--radius);
  padding: 20px 24px;
  color: #fff;
  position: relative;
  overflow: hidden;
  box-shadow: var(--shadow-md);
  transition: transform .2s ease, box-shadow .2s ease;
}
.stat-card:hover          { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,.15); }
.stat-card .icon          { font-size: 32px; opacity: .25; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); }
.stat-card h3             { font-size: 28px; font-weight: 800; margin: 6px 0 2px; }
.stat-card p              { margin: 0; opacity: .85; font-size: 13px; font-weight: 500; }
.stat-card .trend         { font-size: 11px; opacity: .7; margin-top: 4px; }
.bg-primary-grad          { background: linear-gradient(135deg, var(--primary-dark), var(--primary)); }
.bg-success-grad          { background: linear-gradient(135deg, #047857, #10b981); }
.bg-warning-grad          { background: linear-gradient(135deg, #d97706, #f59e0b); }
.bg-danger-grad           { background: linear-gradient(135deg, #b91c1c, #ef4444); }
.bg-purple-grad           { background: linear-gradient(135deg, #6d28d9, #8b5cf6); }
.bg-teal-grad             { background: linear-gradient(135deg, #0f766e, #14b8a6); }

/* ── CARDS ──────────────────────────────────────────────────────── */
.card {
  background: var(--card) !important;
  border: 1px solid var(--border) !important;
  border-radius: var(--radius) !important;
  box-shadow: var(--shadow-sm);
}
.card-header {
  background: var(--card) !important;
  border-bottom: 1px solid var(--border) !important;
  border-radius: var(--radius) var(--radius) 0 0 !important;
  padding: 14px 20px !important;
  font-weight: 600;
  font-size: 14px;
}

/* ── TABLES ─────────────────────────────────────────────────────── */
/* Most table views wrap their <table> in a .table-card with no scroll
   container of its own — on a phone this just overflowed the page and
   forced the whole body to scroll sideways. Scoping the scroll to the
   card itself keeps the rest of the page (sidebar, header) in place. */
.table-card { overflow-x: auto; }

/* DataTables' dom string (DT_FOOTER_DOM in app.js) lays out the toolbar/
   search row and the info/length/pagination row with Bootstrap's .row,
   whose negative side margins are gutters meant to be absorbed by a
   padded parent. Most .table-card bodies use card-body p-0 (zero padding,
   so the table itself sits flush against the card edges) — with nothing
   to absorb it, the .row's negative margins bleed a few px past the card
   edge, which silently trips .table-card's own overflow-x:auto above and
   renders as a phantom horizontal scrollbar at the very bottom of the
   card (below the pagination row, since that's the container's own
   scrollbar, not the table's). Padding the wrapper and zeroing the row's
   margins removes the bleed without touching Bootstrap's per-column
   gutter padding or the table's own legitimate overflow-x on mobile. */
.dataTables_wrapper { padding: 0 16px; }
.dataTables_wrapper > .row { margin-left: 0; margin-right: 0; }

/* Server-side DataTables (initServerDataTable in app.js) wrap their
   <table> in this after init — a fixed-height, internally-scrolling body
   with the header pinned via position:sticky, so a full page (25+ rows)
   never pushes the pagination/info footer (which sits right after the
   table, outside this wrapper) below the fold. Horizontal scroll for wide
   tables still works the same as before (overflow-x here too). */
.dt-scroll-wrap { max-height: 65vh; overflow-y: auto; overflow-x: auto; position: relative; }
/* Bootstrap's .table sets border-collapse:collapse, which silently breaks
   position:sticky on <th>/<td> in every browser (a well-known CSS
   interaction, not a bug in the sticky rule below) — without this
   override the whole header just scrolls away with the body instead of
   staying pinned. border-spacing:0 keeps cell borders looking identical
   to collapsed ones once separate is forced back on. */
.dt-scroll-wrap table { border-collapse: separate !important; border-spacing: 0; margin-bottom: 0 !important; }
.dt-scroll-wrap thead { position: sticky; top: 0; z-index: 3; }
.dt-scroll-wrap thead th {
  position: sticky;
  top: 0;
  z-index: 3;
  background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 100%) !important;
}

/* DrillCost-style header: dark accent gradient, uppercase white text —
   stays the same in both light and dark app-theme (DrillCost's own
   .table-zijin thead does the same, never switching to a "dark mode"
   variant since it's already dark) so a table's header never blends
   into the page around it either way. */
.table-card .table th {
  background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 100%);
  font-size: 10.5px;
  font-weight: 700;
  color: #fff;
  text-transform: uppercase;
  letter-spacing: .5px;
  border-bottom: none !important;
  white-space: nowrap;
  padding: 9px 14px;
}
.table-card .table th a,
.table-card .table th i,
.table-card .table th .dt-filter-icon { color: rgba(255,255,255,.85); }
.table-card .table th .dt-filter-icon:hover,
.table-card .table th .dt-filter-icon:hover i { color: #fff; }
/* Active (applied) filter — a fixed red regardless of theme, not the
   accent colour, so it reads as a clear "something is filtered" signal
   against the header instead of blending into it (the header itself
   IS the accent colour, so var(--primary) here was nearly invisible).
   Must also target the inner <i> directly, not just the .dt-filter-icon
   wrapper span — "th i" above is a rule that matches the <i> itself,
   and a direct match on an element always beats an inherited value
   regardless of the ancestor rule's specificity, so red on the span
   alone was silently losing to the plain white "th i" rule. Bumped
   size too — DrillCost's own active-filter mark is a distinctly bigger,
   bolder triangle, not just a recoloured version of the same small
   icon every other column shows. */
.table-card .table th .dt-filter-icon.dt-filter-active,
.table-card .table th .dt-filter-icon.dt-filter-active i {
  color: #ff5252 !important;
  font-size: 13px;
}
.table-card .table th.sorting:hover,
.table-card .table th.sorting_asc,
.table-card .table th.sorting_desc { color: #fff; }
.table-card .table td {
  vertical-align: middle;
  font-size: 12.5px;
  padding: 8px 14px;
  border-color: #f0f4f8;
}
.table-card .table tbody tr:hover { background: rgba(var(--brand-500-rgb, 37,99,235),.06); }

/* Checked row (initSelectableTable in app.js toggles this class on
   .dt-row-check change) — a stronger, sticky theme tint so a selected
   row stays visibly marked even after the mouse moves off it, matching
   DrillCost's .highlight-row. Placed after :hover and :nth-child so it
   wins over both regardless of parity/hover state. */
.table-card .table tbody tr.row-selected,
.table-card .table tbody tr.row-selected:hover,
.table-card .table tbody tr.row-selected:nth-child(even):not(:hover) > * {
  background-color: rgba(var(--brand-500-rgb, 37,99,235),.12) !important;
}

/* Zebra striping — :not(:hover) so a hovered row always shows the hover
   color above regardless of which rule the cascade would otherwise pick
   between two equal-specificity selectors (rather than relying on
   declaration order, which the next person moving code around could
   silently flip). */
.table-card .table tbody tr:nth-child(even):not(:hover) > * { background-color: #fafbfc; }
[data-theme="dark"] .table-card .table tbody tr:nth-child(even):not(:hover) > * { background-color: rgba(255,255,255,.025); }

/* ── BADGES ─────────────────────────────────────────────────────── */
.badge-active   { background: #d1fae5 !important; color: #065f46 !important; }
.badge-inactive { background: #fee2e2 !important; color: #991b1b !important; }
.badge-pending  { background: #fef9c3 !important; color: #713f12 !important; }
[data-theme="dark"] .badge-active   { background: rgba(16,185,129,.15) !important; color: #6ee7b7 !important; }
[data-theme="dark"] .badge-inactive { background: rgba(239,68,68,.15)  !important; color: #fca5a5 !important; }
[data-theme="dark"] .badge-pending  { background: rgba(245,158,11,.15) !important; color: #fcd34d !important; }

/* ── FORMS ──────────────────────────────────────────────────────── */
.form-control, .form-select {
  background: var(--card);
  border-radius: 6px;
  border: 1px solid var(--border-strong);
  color: var(--text);
  font-size: 14px;
  padding: 9px 12px;
  transition: border-color .2s, box-shadow .2s;
}
.form-control:focus, .form-select:focus {
  background: var(--card);
  border-color: var(--primary);
  color: var(--text);
  box-shadow: 0 0 0 3px rgba(var(--brand-500-rgb, 37,99,235),.15);
  outline: none;
}
.form-control::placeholder { color: var(--muted); }
/* Belt-and-suspenders on top of the --bs-form-check-input-checked-*
   variables above — forces it regardless of Bootstrap version/build
   quirks in how those variables get consumed. */
.form-check-input:checked {
  background-color: var(--brand-500, #2563eb) !important;
  border-color: var(--brand-500, #2563eb) !important;
}
.form-check-input:focus {
  border-color: var(--brand-500, #2563eb);
  box-shadow: 0 0 0 3px rgba(var(--brand-500-rgb, 37,99,235),.2);
}
/* Unchecked-state border only ever had Bootstrap's light default grey
   (--bs-border-color), which vanishes on two surfaces this app added:
   dark mode's dark card background, and the header checkbox sitting on
   the now-coloured gradient th (this file's own body background makes
   the header one white-ish already via ".table-card .table th i" etc,
   but the checkbox is a native form control, not text, so that rule
   never reached it). */
[data-theme="dark"] .form-check-input { background-color: var(--card-2); border-color: rgba(255,255,255,.35); }
.table-card .table th .form-check-input {
  background-color: rgba(255,255,255,.15);
  border-color: rgba(255,255,255,.75);
}
.form-label { font-size: 13px; font-weight: 600; color: var(--label-color); margin-bottom: 5px; }
.input-group-text { background: var(--card-2); border: 1px solid var(--border-strong); color: var(--muted); font-size: 14px; }
[data-theme="dark"] .form-control:focus, [data-theme="dark"] .form-select:focus { background: var(--card-2); }
[data-theme="dark"] .input-group-text { background: var(--card); border-color: var(--border-strong); }

/* ── BUTTONS ────────────────────────────────────────────────────── */
.btn          { border-radius: 6px; font-size: 13.7px; font-weight: 500; padding: 8px 16px; transition: all .2s;
                display: inline-flex; align-items: center; gap: 6px; }
/* Resting glow (not just on hover) — DrillCost's own filled buttons
   (.qa-teal/.qa-blue/etc) carry a coloured shadow at rest, which is a
   big part of why they read as "3D"/raised rather than a flat filled
   rectangle; translateY on hover sells the lift further. */
.btn-primary  {
  background: var(--primary); border-color: var(--primary);
  box-shadow: 0 2px 8px rgba(var(--brand-500-rgb, 37,99,235),.28);
  transition: all .2s ease;
}
.btn-primary:hover {
  background: var(--primary-dark); border-color: var(--primary-dark);
  box-shadow: 0 6px 16px rgba(var(--brand-500-rgb, 37,99,235),.4);
  transform: translateY(-1px);
}
.btn-primary:active { transform: translateY(0); }
/* .btn-outline-primary is a Bootstrap COMPONENT (not a simple utility
   like text-primary/bg-primary), and like .btn-primary's disabled
   state, its --bs-btn-color/--bs-btn-border-color/--bs-btn-hover-bg
   are baked to a literal blue at Bootstrap's build time rather than
   referencing --bs-primary dynamically — so outline-primary buttons
   outside the DataTable toolbar (which already had its own scoped fix
   below) stayed Bootstrap-blue regardless of theme. This is the
   general, app-wide fix. */
.btn-outline-primary {
  color: var(--primary);
  border-color: var(--primary);
  background: transparent;
}
.btn-outline-primary:hover, .btn-outline-primary:active, .btn-outline-primary:focus {
  background: var(--primary) !important;
  border-color: var(--primary) !important;
  color: #fff !important;
}
/* Bootstrap's own .btn:disabled carries higher specificity (class +
   pseudo-class) than our plain .btn-primary rule above, so a disabled
   primary button (e.g. the Import CSV modal's button before a file is
   chosen) was silently falling back to Bootstrap's baked-in blue
   regardless of theme — this specifically out-specifies that. */
.btn-primary:disabled, .btn-primary.disabled {
  background: var(--primary); border-color: var(--primary); opacity: .55;
}
.btn-sm       { padding: 5px 12px; font-size: 12.8px; border-radius: 6px; }
.btn-lg       { padding: 11px 22px; font-size: 15.2px; border-radius: 8px; }
[data-theme="dark"] .btn-outline-secondary { color: var(--muted); border-color: var(--border); }
[data-theme="dark"] .btn-outline-secondary:hover { background: var(--card-2); color: var(--text); border-color: var(--border); }

/* ── AVATAR ─────────────────────────────────────────────────────── */
.avatar {
  width: 36px; height: 36px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 13px; flex-shrink: 0;
  background: var(--primary); color: #fff;
}
.avatar-lg { width: 56px; height: 56px; font-size: 20px; }
.avatar-sm { width: 28px; height: 28px; font-size: 11px; }
.avatar-female { background: #ec4899; }
.avatar-male   { background: var(--primary); }

/* ── PROGRESS BARS ──────────────────────────────────────────────── */
.progress          { border-radius: 4px; overflow: hidden; }
.progress-bar      { transition: width .6s ease; }
.progress-thin     { height: 4px; }
.progress-medium   { height: 8px; }

/* ── ALERTS ─────────────────────────────────────────────────────── */
.alert { border: 1px solid; border-radius: 10px; padding: 14px 16px; font-size: 13.5px; }
[data-theme="dark"] .alert-info    { background: rgba(59,130,246,.1);  border-color: rgba(59,130,246,.25);  color: #93c5fd; }
[data-theme="dark"] .alert-success { background: rgba(16,185,129,.1);  border-color: rgba(16,185,129,.25);  color: #6ee7b7; }
[data-theme="dark"] .alert-warning { background: rgba(245,158,11,.1);  border-color: rgba(245,158,11,.25);  color: #fcd34d; }
[data-theme="dark"] .alert-danger  { background: rgba(239,68,68,.1);   border-color: rgba(239,68,68,.25);   color: #fca5a5; }

/* ── PAGE HEADER (content-level heading, matches multi's layout) ── */
.page-header {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 14px; margin-bottom: 22px;
}
/* DrillCost's .page-title-badge — a themed pill instead of plain text,
   with a fixed gold left-edge accent (their own "Zijin gold" brand
   mark, kept literal rather than accent-derived so it reads as a
   consistent signature stripe across every theme, the way it does in
   the reference). Works with the existing <h1 class="page-title"> +
   <p class="page-subtitle"> markup on every page unchanged — no view
   file needed an icon or restructuring for this to apply everywhere. */
.page-title {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 7px 20px;
  border-radius: 50rem;
  background: var(--primary-dark);
  color: #fff;
  box-shadow: 0 2px 8px rgba(var(--brand-500-rgb, 37,99,235),.25);
  border-left: 4px solid #f2a900;
  font-size: 1.05rem; font-weight: 700; letter-spacing: .3px;
  margin: 0;
}
.page-subtitle { font-size: .8rem; color: var(--muted); margin: 6px 0 0 4px; }
.breadcrumb { font-size: .78rem; margin: 0; }
.breadcrumb-item a { color: var(--muted); text-decoration: none; }
.breadcrumb-item a:hover { color: var(--primary); }
.breadcrumb-item.active { color: var(--text); }

/* ── KPI CARD (Platform Admin's own dashboard stat-card pattern, ported
   here — tinted icon badge on the left, value+label+sub stacked to its
   right) — one shared component, so every page using .kpi-card/.kpi-icon
   picks this up automatically, no per-page CSS. Still CSS Grid, not
   flex: repeating "icon" in the icon column across all three template-
   area rows makes it auto-span and vertically center against the whole
   card, without needing a wrapper div around value/label/sub — so this
   is a pure CSS change, no markup touched anywhere that already uses
   .kpi-icon/.kpi-value/.kpi-label/optional .kpi-sub in that order. */
.kpi-card {
  background: var(--card); border-radius: 14px; box-shadow: var(--shadow-sm);
  padding: 18px 20px; border: 1px solid var(--border);
  position: relative; overflow: hidden;
  transition: transform .15s, box-shadow .15s;
  /* Bootstrap's .row/.col-* already stretch every column in a row to
     equal height — this just has the card itself actually fill that
     height, so a row mixing cards with/without a .kpi-sub line (or a
     longer .kpi-label) doesn't produce visibly uneven card sizes. */
  height: 100%;
  display: grid;
  grid-template-columns: 42px 1fr;
  grid-template-areas: "icon value" "icon label" "icon sub";
  column-gap: 14px;
  align-items: center;
}
.kpi-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
/* Selected/active state for a .kpi-card used as a filter toggle (e.g. the
   Users page's per-role tiles) -- a clickable <a> wraps the card, so this
   just needs to read clearly as "on" without touching the icon/value/label
   layout at all. */
.kpi-card-active { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary); }
.kpi-icon {
  grid-area: icon;
  width: 42px; height: 42px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.1rem;
  align-self: center;
}
.kpi-icon.blue,   .kpi-icon.primary { background: #dbeafe; color: var(--primary); }
.kpi-icon.green,  .kpi-icon.success { background: #d1fae5; color: #059669; }
.kpi-icon.amber,  .kpi-icon.warning { background: #fef9c3; color: #d97706; }
.kpi-icon.red,    .kpi-icon.danger  { background: #fee2e2; color: #dc2626; }
.kpi-icon.purple  { background: #ede9fe; color: #7c3aed; }
.kpi-icon.indigo  { background: #e0e7ff; color: #4338ca; }
.kpi-icon.cyan,   .kpi-icon.info    { background: #cffafe; color: #0891b2; }
.kpi-icon.teal    { background: #ccfbf1; color: #0d9488; }
[data-theme="dark"] .kpi-icon.blue,  [data-theme="dark"] .kpi-icon.primary { background: rgba(var(--brand-500-rgb, 37,99,235),.15); }
[data-theme="dark"] .kpi-icon.green, [data-theme="dark"] .kpi-icon.success { background: rgba(16,185,129,.15); }
[data-theme="dark"] .kpi-icon.amber, [data-theme="dark"] .kpi-icon.warning { background: rgba(245,158,11,.15); }
[data-theme="dark"] .kpi-icon.red,   [data-theme="dark"] .kpi-icon.danger  { background: rgba(239,68,68,.15); }
[data-theme="dark"] .kpi-icon.purple { background: rgba(124,58,237,.15); }
[data-theme="dark"] .kpi-icon.indigo { background: rgba(67,56,202,.15); }
[data-theme="dark"] .kpi-icon.cyan,  [data-theme="dark"] .kpi-icon.info   { background: rgba(8,145,178,.15); }
[data-theme="dark"] .kpi-icon.teal   { background: rgba(13,148,136,.15); }

/* word-break, not nowrap+ellipsis: a long value (GH₵125,000.00, a big
   invoice count, ...) wrapping to a second line just makes that card's
   row a little taller (grid rows auto-size to content) -- silently
   hiding part of a number behind an ellipsis would be worse than a
   two-line card. */
.kpi-value { grid-area: value; font-size: 19px; font-weight: 800; color: var(--text); line-height: 1.15; word-break: break-word; }
.kpi-label { grid-area: label; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin-top: 4px; }
.kpi-sub   { grid-area: sub; font-size: .72rem; color: var(--muted); margin-top: 2px; }

/* ── DASHBOARD BANNER (DrillCost's "Operations Dashboard" header,
   reference screenshot) — always the accent gradient regardless of
   light/dark mode, matching the topbar/table-header treatment
   elsewhere; a faint repeating dot pattern for texture, same idea as
   DrillCost's own banner. ── */
.dash-banner {
  position: relative;
  overflow: hidden;
  border-radius: 14px;
  padding: 26px 32px;
  color: #fff;
  background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 100%);
  box-shadow: 0 10px 28px rgba(var(--brand-500-rgb, 37,99,235),.25);
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 20px;
}
.dash-banner::before {
  content: '';
  position: absolute; inset: 0;
  background-image: radial-gradient(rgba(255,255,255,.1) 1.4px, transparent 1.4px);
  background-size: 22px 22px;
  pointer-events: none;
}
.dash-banner > * { position: relative; z-index: 1; }
.dash-banner-icon {
  width: 52px; height: 52px; border-radius: 14px;
  background: rgba(255,255,255,.16);
  display: flex; align-items: center; justify-content: center;
  font-size: 24px; flex-shrink: 0;
}
.dash-banner h1 { font-size: 1.3rem; font-weight: 800; margin: 0; color: #fff; }
.dash-banner p  { font-size: .82rem; margin: 3px 0 0; color: rgba(255,255,255,.8); }
.dash-banner-pill {
  display: inline-flex; align-items: center; gap: 7px;
  background: rgba(255,255,255,.14);
  border: 1px solid rgba(255,255,255,.25);
  border-radius: 50rem;
  padding: 7px 16px;
  font-size: .78rem; font-weight: 600;
  color: #fff; white-space: nowrap;
}

/* ── SEGMENTED BAR (DrillCost's "Hole Status" bar) — one cell per unit
   (e.g. one per student), coloured by group (e.g. class), so the bar's
   total length IS the total count rather than a proportionally-scaled
   stack. Cells shrink evenly to fit the card width via flex:1. ── */
.seg-bar {
  display: flex; gap: 2px;
  border-radius: 8px; overflow: hidden;
  height: 34px;
  background: var(--card-2);
}
.seg-bar-cell { flex: 1 1 0; min-width: 3px; transition: opacity .15s; }
.seg-bar-cell:hover { opacity: .8; }
.seg-legend { display: flex; flex-wrap: wrap; gap: 18px; margin-top: 14px; }
.seg-legend-item { display: flex; align-items: center; gap: 7px; font-size: .82rem; color: var(--text); }
.seg-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.seg-legend-item b { font-weight: 700; }
.seg-legend-item .seg-legend-pct { color: var(--muted); font-size: .78rem; }

/* ── PANEL (DrillCost's "Drill Log Entries" card — light tinted header
   with a pill-badge title, not a solid colour bar) ── */
.db-panel {
  background: var(--card); border-radius: 10px; box-shadow: var(--shadow-sm);
  border: 1px solid var(--border); border-top: 3px solid var(--primary);
  overflow: hidden; height: 100%;
}
.db-panel-head {
  background: var(--card-2); padding: 12px 18px;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  border-bottom: 1px solid var(--border);
}
/* Plain bold uppercase text next to a small tinted icon badge — not a
   pill wrapped around both (DrillCost's own card titles, e.g. "ACTUAL
   VS FORECAST"/"DOWNTIME BY ACTIVITY", are just that: a quiet icon
   chip + clean text sitting on the tinted header strip, nothing
   wrapping the text itself). */
.db-panel-title {
  font-size: .76rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text); display: inline-flex; align-items: center; gap: 9px;
}
.db-panel-title i {
  width: 22px; height: 22px; border-radius: 6px; flex-shrink: 0;
  background: rgba(var(--brand-500-rgb, 37,99,235),.12);
  color: var(--primary);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px;
}
.db-panel-count {
  background: var(--primary); color: #fff; font-size: .68rem; font-weight: 700;
  border-radius: 50rem; padding: 1px 9px; margin-left: 4px;
}
.db-panel-body { padding: 16px 18px; }

/* Chart.js canvas wrapper — Chart.js's default maintainAspectRatio:true
   locks the chart's height to the ASPECT RATIO of its size at creation
   time; a canvas with a bare height="200" attribute and no explicit
   width computes that ratio from whatever the card happened to be at
   load, then keeps shrinking height as the card gets narrower on a
   phone -- a doughnut's legend (Fee Status/Gender Ratio) has no room to
   lay out its rows and gets clipped, and a bar/line chart's y-axis
   labels start overlapping the plot area. Fix (Chart.js's own
   documented pattern): maintainAspectRatio:false in the chart options
   below, paired with an explicit-height wrapper here instead of relying
   on the canvas's own height attribute -- with the ratio lock off, the
   canvas fills 100% of this div's height every time regardless of width. */
.chart-box { position: relative; height: 220px; }
@media (max-width: 576px) { .chart-box { height: 200px; } }

/* ── Same icon-badge + clean-text title treatment for the plain
   Chart.js/rank-list cards (Collections trend, Attendance trend, Fee
   Status, Gender Ratio, Top Fee Heads, Classes with Most Owed, ...) —
   opt-in via .chart-card-title on the existing .card-header, rather
   than a blanket rule on every .card-header in the app (used far too
   widely elsewhere, on far smaller/denser cards, to badge every icon
   inside one automatically). ── */
.chart-card-title {
  display: flex; align-items: center; gap: 9px;
  font-size: .8rem; font-weight: 700;
}
.chart-card-title i {
  width: 22px; height: 22px; border-radius: 6px; flex-shrink: 0;
  background: rgba(var(--brand-500-rgb, 37,99,235),.12);
  color: var(--primary);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; margin: 0 !important;
}
/* Per-card icon color variety — same named palette as .kpi-icon, so a
   dashboard row of chart cards reads as distinct at a glance instead of
   every badge sharing the one accent tint. Opt-in: add e.g.
   <i class="bi bi-x green"> alongside .chart-card-title/.db-panel-title. */
.chart-card-title i.green,   .db-panel-title i.green   { background: #d1fae5; color: #059669; }
.chart-card-title i.amber,   .db-panel-title i.amber   { background: #fef9c3; color: #d97706; }
.chart-card-title i.red,     .db-panel-title i.red     { background: #fee2e2; color: #dc2626; }
.chart-card-title i.purple,  .db-panel-title i.purple  { background: #ede9fe; color: #7c3aed; }
.chart-card-title i.indigo,  .db-panel-title i.indigo  { background: #e0e7ff; color: #4338ca; }
.chart-card-title i.cyan,    .db-panel-title i.cyan    { background: #cffafe; color: #0891b2; }
.chart-card-title i.teal,    .db-panel-title i.teal    { background: #ccfbf1; color: #0d9488; }
[data-theme="dark"] .chart-card-title i.green,  [data-theme="dark"] .db-panel-title i.green  { background: rgba(16,185,129,.15); }
[data-theme="dark"] .chart-card-title i.amber,  [data-theme="dark"] .db-panel-title i.amber  { background: rgba(245,158,11,.15); }
[data-theme="dark"] .chart-card-title i.red,    [data-theme="dark"] .db-panel-title i.red    { background: rgba(239,68,68,.15); }
[data-theme="dark"] .chart-card-title i.purple, [data-theme="dark"] .db-panel-title i.purple { background: rgba(124,58,237,.15); }
[data-theme="dark"] .chart-card-title i.indigo, [data-theme="dark"] .db-panel-title i.indigo { background: rgba(67,56,202,.15); }
[data-theme="dark"] .chart-card-title i.cyan,   [data-theme="dark"] .db-panel-title i.cyan   { background: rgba(8,145,178,.15); }
[data-theme="dark"] .chart-card-title i.teal,   [data-theme="dark"] .db-panel-title i.teal   { background: rgba(13,148,136,.15); }

/* ── HINT BANNER — a tinted, theme-aware alternative to Bootstrap's
   .alert-light/.alert-primary (the latter bakes literal blue at build
   time, same recurring gotcha as everywhere else in this file). Used for
   quiet inline hints like "drag to reorder" on Classes. ── */
.hint-banner {
  background: rgba(var(--brand-500-rgb, 37,99,235), .08);
  border: 1px solid rgba(var(--brand-500-rgb, 37,99,235), .18);
  color: var(--text);
  border-radius: 10px;
  padding: 10px 14px;
  font-size: 12.5px;
}
.hint-banner i { color: var(--primary); }
[data-theme="dark"] .hint-banner { background: rgba(var(--brand-500-rgb, 37,99,235), .12); border-color: rgba(var(--brand-500-rgb, 37,99,235), .28); }

/* ── RANK LIST (DrillCost's "Downtime by Activity" bar-per-row list) ── */
.rank-list-item { padding: 8px 0; }
.rank-list-item:not(:last-child) { border-bottom: 1px solid var(--border-light); }
.rank-list-head { display: flex; justify-content: space-between; align-items: baseline; font-size: .82rem; margin-bottom: 5px; gap: 10px; }
.rank-list-head b { color: var(--text); font-weight: 600; }
.rank-list-head span { color: var(--muted); font-size: .78rem; white-space: nowrap; }
.rank-list-track { height: 6px; border-radius: 3px; background: var(--card-2); overflow: hidden; }
.rank-list-fill { height: 100%; border-radius: 3px; }

/* ── STATUS PILL BADGES (matches multi's .sb-* pattern) ──────────── */
.sb-pill { display: inline-block; font-size: .68rem; font-weight: 700; padding: 2px 8px; border-radius: 20px; }
.sb-paid    { background: #d1fae5; color: #065f46; }
.sb-unpaid  { background: #fee2e2; color: #991b1b; }
.sb-partial { background: #fef9c3; color: #713f12; }
.sb-voided  { background: #f1f5f9; color: #94a3b8; }
.badge { font-weight: 500; font-size: .72rem; padding: 3px 8px; border-radius: 20px; }

/* ── DATE-RANGE "N DAYS" PILL (wireDateRangeDayCounts() in app.js) ──
   Theme-tinted pill, same recipe as .kpi-icon's color variants: a light
   tint of the theme color behind matching-color text/icon, swapped for a
   low-opacity version of the same hue in dark mode. .date-range-days-wrap
   is the pill's own column (display:none until both dates are set — see
   app.js) so it never reserves layout space it isn't using. */
.date-range-days-wrap { display: none; align-items: center; }
.date-range-days-pill {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 700;
  padding: 6px 12px; border-radius: 999px;
  background: #dbeafe; color: var(--primary);
  white-space: nowrap;
}
.date-range-days-pill.date-range-days-pill-error { background: #fee2e2; color: #dc2626; }
[data-theme="dark"] .date-range-days-pill { background: rgba(var(--brand-500-rgb, 37,99,235),.15); }
[data-theme="dark"] .date-range-days-pill.date-range-days-pill-error { background: rgba(239,68,68,.15); color: #fca5a5; }

/* ── MODAL BASE ─────────────────────────────────────────────────── */
/* One consistent, modern treatment for every modal in the app — large,
   top-anchored, borderless-with-shadow, scrollable body with a pinned
   header/footer. !important throughout is deliberate: this overrides
   Bootstrap's own per-size classes (.modal-sm/.modal-lg/.modal-xl) AND
   any inline style="max-width:...px" an individual modal still carries,
   so the whole app gets one uniform modal size without having to hunt
   down and edit every modal's markup one at a time. */
.modal-content {
  border-radius: 0 !important; /* !important: beats individual modals' own
    .rounded-3/.rounded-4 utility classes (still present in most modal
    markup from before this was unified) — those carry !important
    themselves, so a non-important rule here would lose to them. */
  border: 0;
  box-shadow: 0 24px 64px rgba(0,0,0,.22);
  max-height: calc(100vh - 4rem);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
/* Dividers right under the title and right above the footer buttons,
   in the tenant's own theme color (--brand-500, same source .modal-title
   and .page-title already pull from) rather than a neutral gray rule —
   ties the modal chrome to whichever brand color the school has chosen,
   the same way the title pill already does. */
.modal-header  { padding: 20px 28px 12px; border-bottom: 2px solid rgba(var(--brand-500-rgb, 37,99,235), .3); flex-shrink: 0; }
.modal-body    { padding: 16px 28px; overflow-y: auto; min-height: 0; }
.modal-footer  { padding: 14px 28px 20px; border-top: 2px solid rgba(var(--brand-500-rgb, 37,99,235), .3); flex-shrink: 0; }
/* Same pill recipe as .page-title (gold-edged, primary-dark chip) so a
   modal reads as a continuation of the page it opened from instead of a
   plain bold line of text — every modal in the app picks this up from
   its existing <h5/h6 class="modal-title">, no markup changes needed. */
.modal-title {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 7px 18px;
  border-radius: 50rem;
  background: var(--primary-dark);
  color: #fff !important;
  box-shadow: 0 2px 8px rgba(var(--brand-500-rgb, 37,99,235),.25);
  border-left: 4px solid #f2a900;
  font-size: .95rem; font-weight: 700; letter-spacing: .3px;
}

/* Small sub-section label inside a form/modal body -- DrillCost's own
   "CORE DETAILS" / "PLANNED COORDINATES" pattern: a small icon + a
   short uppercase label in the theme color, with a thin rule right
   after it, so a cluster of related fields (identity vs. contact info,
   say) reads as its own group without the weight of a full section
   header -- deliberately light, so several can sit in one form without
   it feeling chopped up. Works above a Bootstrap .row, a bare inline
   grid div, or an <hr> alike, since it doesn't rely on what precedes it
   -- just skips the top margin on the first one in its container. */
.form-section-label {
  display: flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px;
  color: var(--primary);
  margin: 22px 0 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
/* :first-of-type, not :first-child -- every one of these forms opens
   with a hidden CSRF <input> before the first visible field, so the
   label is never literally the first child. :first-of-type only cares
   about matching-tag siblings (other <div>s), so it still correctly
   finds "the first .form-section-label div" past a same-parent <input>
   that isn't a div at all. */
.form-section-label:first-of-type { margin-top: 0; }
.form-section-label i { font-size: 12px; }

/* Large by default — ~80% of the viewport width, regardless of which
   Bootstrap size class (or hand-set inline max-width) the modal markup
   itself asks for. Capped at 1400px so it doesn't stretch absurdly wide
   on an ultrawide monitor; left alone below the 576px breakpoint
   Bootstrap itself uses for sizing, since mobile needs to stay
   near-full-width, not literally 80% with extra margin eating the screen. */
@media (min-width: 576px) {
  .modal-dialog {
    width: 80vw !important;
    max-width: 1400px !important;
    margin: 2rem auto !important;
  }
}

/* Every modal starts anchored near the TOP of the viewport at the exact
   same offset, full stop — regardless of which Bootstrap positioning
   modifier its own markup happens to carry (.modal-dialog-centered's
   flex-centering, .modal-dialog-scrollable's height:calc(100% - Xrem)).
   Targets bare .modal-dialog, not a class-specific variant, so a modal
   can't opt back into a different resting position by keeping/dropping
   one of those classes. A centered modal that's also this large kept
   re-centering itself as content/images loaded (visibly jumping), and
   on a shorter viewport a centered tall modal could clip above the fold
   before you'd even scrolled; anchoring to the top is stable regardless
   of body height. */
.modal-dialog {
  display: block !important;
  align-items: unset !important;
  min-height: unset !important;
  height: auto !important;
}

/* ── MODAL / DROPDOWN / LIST-GROUP DARK MODE ─────────────────────── */
/* .modal-header/.modal-footer's divider is intentionally left alone here
   -- it's the brand-color rgba() set directly in the base rule above,
   the same in both themes, not a --border-derived value that would need
   a dark-mode override. */
[data-theme="dark"] .list-group-item { background: var(--card); border-color: var(--border); color: var(--text); }
[data-theme="dark"] .list-group-item-action:hover { background: var(--card-2); }
[data-theme="dark"] .dropdown-header { color: var(--muted); }
[data-theme="dark"] hr.dropdown-divider { border-color: var(--border); }

/* ── DATATABLES CUSTOM ──────────────────────────────────────────── */
/* Pill-shaped, icon-in-field search box — DrillCost's own "Search all
   fields" input. An inline SVG background icon rather than a JS-built
   button element: DataTables generates the <label>Search:<input></label>
   markup itself, so this is the only way to get the icon-in-search-box
   look without hooking every table's init to inject extra DOM. */
.dataTables_wrapper .dataTables_filter input {
  border-radius: 50rem;
  border: 1.5px solid var(--border);
  padding: 8px 16px 8px 38px;
  font-size: 13px;
  background: var(--card) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='6.5' cy='6.5' r='5'/%3E%3Cline x1='10.2' y1='10.2' x2='14.5' y2='14.5'/%3E%3C/svg%3E") no-repeat 14px center;
}
.dataTables_wrapper .dataTables_filter input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(var(--brand-500-rgb, 37,99,235),.15);
  outline: none;
}
.dataTables_wrapper .dataTables_length select {
  border-radius: 8px;
  border: 1.5px solid var(--border);
  padding: 4px 8px;
}
.paginate_button.current, .paginate_button.current:hover {
  background: var(--primary) !important;
  border-color: var(--primary) !important;
  color: #fff !important;
  border-radius: 6px;
}

/* DrillCost-style pagination: pill-shaped Prev/Next, gradient active
   page. DataTables 1.13 with the bootstrap5 integration renders
   <a class="page-link"> inside Bootstrap .page-item — style those
   directly rather than the legacy .paginate_button classes above
   (kept for any plain-DataTables table still using them). */
.dataTables_wrapper .dataTables_paginate .page-link {
  color: var(--text);
  background: var(--card);
  border-color: var(--border);
  border-radius: 6px;
  font-size: 12px;
  font-weight: 500;
  padding: 5px 12px;
  transition: all .15s ease;
}
.dataTables_wrapper .dataTables_paginate .page-item.active .page-link {
  background: linear-gradient(135deg, var(--primary-dark), var(--primary)) !important;
  color: #fff !important;
  border-color: var(--primary) !important;
  box-shadow: 0 2px 8px rgba(var(--brand-500-rgb, 37,99,235),.35);
}
.dataTables_wrapper .dataTables_paginate .page-item:not(.active):not(.disabled) .page-link:hover {
  background: rgba(var(--brand-500-rgb, 37,99,235),.08) !important;
  color: var(--primary) !important;
  border-color: var(--primary) !important;
}
.dataTables_wrapper .dataTables_paginate .page-item.disabled .page-link {
  color: var(--muted); background: var(--card-2); border-color: var(--border);
}
.dataTables_wrapper .dataTables_paginate .paginate_button.previous .page-link,
.dataTables_wrapper .dataTables_paginate .paginate_button.next .page-link {
  border-radius: 20px;
  padding: 5px 16px;
  font-weight: 600;
}

/* ── SELECTABLE DATATABLE (checkbox column + action toolbar) ────────
   See initServerDataTable()/initSelectableTable()/initTableActionToolbar()
   in assets/js/app.js — opt in with data-selectable on a
   data-server-view table. */
.dt-checkbox-col { width: 34px; text-align: center; }
table.dataTable td.dt-checkbox-col { cursor: default; }
.dt-action-toolbar {
  background: var(--card-2, #f4f6f8);
  border: 1px solid var(--border);
}
[data-theme="dark"] .dt-action-toolbar { background: var(--card-2); }

/* ── COLUMN FILTER ICON + POPOVER ─────────────────────────────────── */
.dt-filter-icon {
  cursor: pointer;
  color: #b0b8c1;
  font-size: 11px;
  margin-left: 4px;
  vertical-align: middle;
}
.dt-filter-icon:hover { color: var(--primary); }
/* Fixed red, not the accent — see the .table-card .table th scoped
   rule above for why (an accent-coloured "active" mark would blend
   into an accent-coloured header). Also covers this icon anywhere it
   renders outside a themed header. Targets the inner <i> directly too
   — same reasoning as the .table-card .table th version above. */
.dt-filter-icon.dt-filter-active,
.dt-filter-icon.dt-filter-active i { color: #d32f2f !important; font-size: 13px; }
.dt-filter-popover {
  position: absolute;
  z-index: 2000;
  width: 260px;
  background: var(--card, #fff);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 12px 32px rgba(0,0,0,.18);
  padding: 10px;
  font-size: 13px;
}
.dtfp-header {
  font-weight: 700;
  font-size: 11px;
  letter-spacing: .4px;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 8px;
}
.dtfp-search { margin-bottom: 8px; }
.dt-filter-popover .dtfp-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 2px;
  margin: 0;
  font-weight: 400;
  cursor: pointer;
}
.dt-filter-popover .dtfp-selectall {
  border-bottom: 1px solid var(--border);
  padding-bottom: 6px;
  margin-bottom: 4px;
  font-weight: 600;
}
.dtfp-list { max-height: 220px; overflow-y: auto; }
.dtfp-footer { display: flex; gap: 8px; margin-top: 10px; }
.dtfp-footer .btn { flex: 1; }

/* Date/amount columns' min/max (or from/to) popover -- see
   openRangeFilterPopover() in app.js. Same shell as the list popover
   above (.dt-filter-popover), just a form-labels-and-inputs body instead
   of a checkbox list. */
.dtfp-range-label {
  display: block;
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  margin: 8px 0 4px;
}
.dtfp-range-label:first-of-type { margin-top: 0; }

/* Bolder, DrillCost-style toolbar buttons for table-level actions
   (Export/Import/Clear Filters, built by initToolbarBase() in app.js) —
   filled pill-shaped colour instead of a plain square outline button,
   matching the ADD NEW / TEMPLATES / EXPORT row in the reference. */
.dt-action-toolbar .btn {
  border-radius: 20px !important;
  font-weight: 700;
  text-transform: uppercase;
  font-size: 11px;
  letter-spacing: .03em;
  padding: 6px 16px;
  box-shadow: 0 2px 6px rgba(0,0,0,.1);
  transition: all .2s ease;
}
.dt-action-toolbar .btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,.15); }
.btn-dt-export {
  background: #17a2b8;
  border-color: #17a2b8;
  color: #fff;
}
.btn-dt-export:hover { background: #148ea1; border-color: #148ea1; color: #fff; }
.dt-action-toolbar .btn-outline-primary {
  background: transparent;
  border-color: var(--primary);
  color: var(--primary);
}
.dt-action-toolbar .btn-outline-primary:hover {
  background: var(--primary); border-color: var(--primary); color: #fff;
}
.dt-action-toolbar .btn-outline-secondary {
  background: transparent;
  border-color: var(--border);
  color: var(--muted);
}
.dt-action-toolbar .btn-outline-secondary:hover {
  background: var(--card-2); color: var(--text); border-color: var(--muted);
}

/* Full-width buttons on small screens — the toolbar's Export/Import/
   View/Edit/Security/Delete pills (and .page-header's own action
   buttons, e.g. Students' "Enroll Student") size to their text and wrap
   into an uneven 2-then-3-then-1 grid on a phone; one full-width button
   per row is both easier to tap and reads cleaner. Both selectors are
   shared components used across ~60 list pages, so this covers all of
   them from here rather than touching each page individually. */
@media (max-width: 576px) {
  .dt-action-toolbar > .btn { flex: 1 1 100%; width: 100%; }
  .page-header .d-flex.gap-2 { width: 100%; flex-direction: column; }
  .page-header .d-flex.gap-2 > .btn { width: 100%; }
}

/* ── NAV TABS ───────────────────────────────────────────────────── */
.nav-tabs { border-bottom: 2px solid var(--border); }
.nav-tabs .nav-link {
  color: var(--muted);
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  font-size: 13.5px;
  font-weight: 500;
  padding: 10px 16px;
  transition: color .2s;
}
.nav-tabs .nav-link:hover { color: var(--primary); }
.nav-tabs .nav-link.active {
  color: var(--primary);
  border-bottom-color: var(--primary);
  background: transparent;
  font-weight: 600;
}

/* ── NAV PILLS (DrillCost-style pill tab bar, e.g. Edit Student's
   Personal/Academic/Guardian/Medical/Account tabs) — Bootstrap's own
   .nav-pills bakes a literal blue into --bs-nav-pills-link-active-bg at
   build time same as .btn-primary did, so this needs its own explicit
   override rather than following --bs-primary automatically. Inactive
   pills stay a neutral ghost/outline so the row doesn't turn into a
   wall of colour — only the active one gets the accent fill. ── */
.nav-pills .nav-link {
  border-radius: 50rem;
  padding: 6px 16px;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  background: var(--card-2);
  border: 1px solid var(--border);
  transition: all .15s ease;
}
.nav-pills .nav-link:hover:not(.active) {
  background: rgba(var(--brand-500-rgb, 37,99,235),.08);
  color: var(--primary);
  border-color: var(--primary);
}
.nav-pills .nav-link.active {
  background: linear-gradient(135deg, var(--primary-dark), var(--primary));
  color: #fff;
  border-color: var(--primary);
  box-shadow: 0 2px 8px rgba(var(--brand-500-rgb, 37,99,235),.3);
}
.nav-pills .nav-link.disabled {
  background: var(--card-2);
  color: var(--muted);
  opacity: .5;
  border-color: var(--border);
}

/* ── SEGMENTED TOGGLE (.btn-group of .btn-check radios + .btn-outline-*
   labels, e.g. waivers.php's Single Student/Class and Fee Item/Whole
   Invoice switchers) — same pill-tab treatment as .nav-pills above, so
   a radio-styled toggle reads as the same UI language as a real tab
   bar instead of Bootstrap's default connected-rectangle segmented-
   button look (solid Bootstrap-blue on the checked segment, square
   joined corners). Scoped with :has(.btn-check) rather than every
   .btn-group, so plain action-button groups (Edit/Delete icon pairs
   etc.) are untouched. */
.btn-group:has(.btn-check) {
  background: var(--card-2);
  border-radius: 50rem;
  padding: 3px;
  gap: 3px;
  border: 1px solid var(--border);
}
.btn-group:has(.btn-check) .btn {
  border-radius: 50rem !important;
  border: none !important;
  background: transparent;
  color: var(--muted);
  font-weight: 600;
  transition: all .15s ease;
}
.btn-group:has(.btn-check) .btn-check:checked + .btn {
  background: linear-gradient(135deg, var(--primary-dark), var(--primary)) !important;
  color: #fff !important;
  box-shadow: 0 2px 8px rgba(var(--brand-500-rgb, 37,99,235),.3);
}
.btn-group:has(.btn-check) .btn-check:not(:checked) + .btn:hover {
  background: rgba(var(--brand-500-rgb, 37,99,235),.08);
  color: var(--primary);
}

/* ── PRINT ──────────────────────────────────────────────────────── */
@media print {
  #sidebar, #topbar, .btn, .no-print { display: none !important; }
  #main { margin: 0 !important; padding: 0 !important; }
  .card { box-shadow: none !important; border: 1px solid #dee2e6 !important; }
}

/* ── RESPONSIVE ─────────────────────────────────────────────────── */
@media (max-width: 768px) {
  #sidebar            { transform: translateX(-100%); transition: transform .3s ease; }
  #sidebar.open       { transform: translateX(0); }
  #topbar, #main      { margin-left: 0 !important; }
  .stat-card h3       { font-size: 22px; }
  .table-responsive   { overflow-x: auto; }
}

/* Dashboard banner's "Enroll Student"/"Collect Payment" quick-action
   buttons sit side-by-side at their natural (text) width -- fine once
   they wrap, but two narrow pill buttons stacked awkwardly off-center is
   worse than just letting each fill the row. */
@media (max-width: 576px) {
  .dash-banner .d-flex.flex-wrap { width: 100%; }
  .dash-banner .d-flex.flex-wrap > .btn { flex: 1 1 100%; }
}

/* Filter bars across ~40 list pages all use this exact "row g-2 align-items-end"
   combination with a mix of .col-auto (dropdowns/buttons) and .col (search
   input) — on a phone those don't shrink enough to fit on one line and just
   overflow/clip instead of wrapping cleanly. Stacking every field to full
   width fixes all of them at once without touching each page individually. */
@media (max-width: 576px) {
  .row.g-2.align-items-end > .col-auto,
  .row.g-2.align-items-end > .col { flex: 0 0 100%; max-width: 100%; }
}

/* Every bare Bootstrap column (col-1..col-11, no responsive sm/md/lg
   suffix) full-width below 576px — this is the single biggest mobile
   pattern in the app: every "Add/Edit X" modal's field grid (row g-3 >
   col-6/col-8/col-4/...), the Settings page's own inline forms, KPI-card
   rows, canteen's plan-select/class-tap cards, collect.php's stat pairs
   — all ~60+ files that use a plain col-N with no breakpoint prefix,
   which today means "2 (or 3+) per row at EVERY width, phones included."
   col-N *with* an sm-/md-/lg- prefix is left alone (Bootstrap's own
   responsive behaviour already stacks those correctly); col-12 is a
   no-op here since it's already full width; col-auto (inline buttons,
   icon+label pairs) is deliberately excluded — those are meant to stay
   their natural width, not stretch. One rule instead of hand-editing
   every view file with this shape. */
@media (max-width: 576px) {
  .row > [class~="col-1"],  .row > [class~="col-2"],  .row > [class~="col-3"],
  .row > [class~="col-4"],  .row > [class~="col-5"],  .row > [class~="col-6"],
  .row > [class~="col-7"],  .row > [class~="col-8"],  .row > [class~="col-9"],
  .row > [class~="col-10"], .row > [class~="col-11"] {
    flex: 0 0 100%; max-width: 100%;
  }
}

/* ── SWEETALERT2 STRAY INPUT FIX ────────────────────────────────────
   SweetAlert2 renders an input/select/textarea/file/range element into
   every popup's DOM regardless of whether an `input` option was set —
   normally kept invisible via inline styles the library manages itself,
   but on some dialogs across this app one has shown up visibly as an
   empty, non-functional bar for no reason we could isolate line-by-line.
   Hiding these unconditionally is safe: a grep of every Swal.fire() call
   in the app found exactly one legitimate use of `input:` (the HR leave
   approval note) — that one opts back in via customClass:'swal-has-input'
   below, so it's unaffected. Any future dialog that intentionally needs
   an input must do the same. */
.swal2-input, .swal2-select, .swal2-textarea, .swal2-file, .swal2-range {
  display: none !important;
}
.swal-has-input .swal2-input,
.swal-has-input .swal2-select,
.swal-has-input .swal2-textarea,
.swal-has-input .swal2-file,
.swal-has-input .swal2-range {
  display: block !important;
}

/* ── ANIMATIONS ─────────────────────────────────────────────────── */
@keyframes fadeIn    { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes slideIn   { from { transform: translateX(-20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes pulse     { 0%,100% { transform: scale(1); } 50% { transform: scale(1.05); } }

.fade-in      { animation: fadeIn .4s ease-out; }
.slide-in     { animation: slideIn .3s ease-out; }
.pulse-anim   { animation: pulse 2s ease-in-out infinite; }

/* ── LOADING OVERLAY ────────────────────────────────────────────── */
.loading-overlay {
  position: fixed; inset: 0; background: rgba(255,255,255,.8);
  display: flex; align-items: center; justify-content: center;
  z-index: 9999;
}

/* Toastr must always render above an open Bootstrap modal (modal ~1055,
   backdrop ~1050) — otherwise a validation error fired while a modal is
   open renders invisibly behind it, which looks exactly like "nothing
   happened" when a form is submitted with a missing required field. */
#toast-container { z-index: 20000 !important; }
.spinner-ring {
  width: 48px; height: 48px; border: 4px solid #e0e0e0;
  border-top-color: var(--primary); border-radius: 50%;
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── TIMELINE ───────────────────────────────────────────────────── */
.timeline         { list-style: none; padding: 0; position: relative; }
.timeline::before { content: ''; position: absolute; left: 16px; top: 0; bottom: 0; width: 2px; background: var(--border); }
.timeline-item    { display: flex; gap: 16px; margin-bottom: 20px; }
.timeline-dot     { width: 32px; height: 32px; border-radius: 50%; background: var(--primary); color: #fff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 13px; z-index: 1; }
.timeline-body    { flex: 1; padding-top: 4px; }

/* ── NOTIFICATION BELL ──────────────────────────────────────────── */
.notif-dot { width: 8px; height: 8px; background: var(--danger); border-radius: 50%; position: absolute; top: 2px; right: 2px; }

/* ── GRADE COLORS ───────────────────────────────────────────────── */
.grade-A1, .grade-B2, .grade-B3 { color: var(--success) !important; font-weight: 700; }
.grade-C4, .grade-C5, .grade-C6 { color: var(--warning) !important; font-weight: 600; }
.grade-D7, .grade-E8             { color: var(--accent)  !important; font-weight: 600; }
.grade-F9                         { color: var(--danger)  !important; font-weight: 700; }

/* ── COLLAPSIBLE SIDEBAR ANIMATION ─────────────────────────────── */
.sb-group-items {
  transition: none; /* Bootstrap collapse handles animation */
}

/* Smooth chevron rotation */
.sb-group-btn .sb-chevron {
  transition: transform .22s cubic-bezier(.4,0,.2,1);
}

/* Group hover background */
.sb-group:hover > .sb-group-btn {
  color: rgba(255,255,255,.55);
}

/* Active group button highlight */
.sb-group-btn[aria-expanded="true"] {
  color: rgba(255,255,255,.8) !important;
  background: rgba(255,255,255,.05);
  border-radius: 7px;
  margin: 0 6px;
  padding-left: 7px;
}

/* Left accent line on open groups */
.sb-group-items {
  border-left: 2px solid rgba(255,255,255,.08);
  margin-left: 18px;
  margin-right: 6px;
  border-radius: 0 0 6px 6px;
  background: rgba(0,0,0,.12);
}

/* Tighter collapse animation */
.collapsing {
  transition: height .2s ease !important;
}
