/* Plus Jakarta Sans — used by the merged-in modern UI below. @import MUST stay at the very
   top of this file (before any rule) or the browser ignores it. */
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400;1,500&display=swap');

html:not([dir=rtl]) .app-brand-text {
    margin-left: 0.325rem !important;
}

.app-brand-logo.demo svg {
    width: 45px !important;
    height: 25px !important;
}

.app-brand-logo.demo {
    width: 45px !important;
    height: 25px !important;
}

.custom-option-icon .custom-option-body svg {
    all: revert !important;
}

.form-floating.input-group label {
    z-index: 10;
}

.light-style .form-floating > label {
    color: var(--m-primary, #004a80);
    opacity: 0.75;
    transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
}

.light-style .select2-container--default .select2-selection--single .select2-selection__rendered {
    color: #6f6b7d;
    padding-left: 0.875rem;
    padding-top: 1.100rem;
    padding-bottom: 0.625rem;
}

.light-style .select2-container--default .select2-selection--single .select2-selection__arrow {
    height: calc(3.5rem + 2px);
    position: absolute;
    width: 2rem;
    right: 1px;
    top: 0;   /* full-height box -> caret sits at the TRUE vertical centre (top:10px pushed it low,
                 so it stuck to the field's bottom border line) */
}

.light-style .select2-results__option[role=option][aria-selected=true] {
    background-color: var(--m-primary, #004a80);
    color: #fff;
}

.light-style .select2-container--default .select2-results__option--highlighted:not([aria-selected=true]) {
    background-color: rgba(0, 74, 128, 0.08) !important;
    color: var(--m-primary, #004a80) !important;
}

.light-style .select2-container--default .select2-selection--single {
    height: calc(3.5rem + 2px);
}

.light-style .input-group .select2-selection.select2-selection--single {
    border-right: none;
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

.dark-style .form-floating > label {
    color: var(--sem-primary-text, #6db9ec);   /* brightened brand blue — legible on dark cards */
    opacity: 0.95;
    transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
}

/* ---- Floating labels: ONE appearance, whatever state the field is in (2026-08-28) -------------
   The rules above only ever styled the RESTING label. The vendor sheets carry a more specific pair
   for the filled/focused state --
       core-dark.css        .form-floating > .form-control:not(:placeholder-shown) ~ label { opacity: .75 }
       theme-default-dark.css  ...same selector...                                  { color: #3a8fd0 }
   -- at (0,3,1) against our (0,2,1), so they win and a field renders a DIFFERENT label colour and
   opacity the moment it has content.

   That is not a rare edge. `:placeholder-shown` is false for:
     * any field with a value (so every populated edit modal),
     * every <select> (they are never placeholder-shown),
     * every type="date" input, EVEN WHEN EMPTY,
     * any field whose markup simply omits the placeholder attribute.
   So on a typical Edit modal roughly half the labels were one colour and half another, with no
   pattern a user could infer -- reported from the CTI Edit Participant modal, where Check In /
   Check Out (dates) sat visibly dimmer than Title / First Name beside them.

   Fixed by matching the SAME states at (0,4,1) and restating the resting values, rather than by
   reaching for !important -- which would only move the problem to whoever needs to override this
   next. Measured before and after with getComputedStyle on the live cascade. */
.light-style .form-floating > .form-control:focus ~ label,
.light-style .form-floating > .form-control:not(:placeholder-shown) ~ label,
.light-style .form-floating > .form-select ~ label,
.light-style .form-floating > textarea.form-control ~ label {
    color: var(--m-primary, #004a80);
    opacity: 0.75;
}
.dark-style .form-floating > .form-control:focus ~ label,
.dark-style .form-floating > .form-control:not(:placeholder-shown) ~ label,
.dark-style .form-floating > .form-select ~ label,
.dark-style .form-floating > textarea.form-control ~ label {
    color: var(--sem-primary-text, #6db9ec);
    opacity: 0.95;
}

.dark-style .select2-container--default .select2-selection--single .select2-selection__rendered {
    color: #d9dee3;
    padding-left: 0.875rem;
    padding-top: 1.100rem;
    padding-bottom: 0.625rem;
}
/* select2 "Select value" placeholder — the default #5e6692 is near-invisible on dark cards */
.dark-style .select2-selection__placeholder {
    color: #9aa6bf !important;
}

.dark-style .select2-container--default .select2-selection--single .select2-selection__arrow {
    height: calc(3.5rem + 2px);
    position: absolute;
    width: 2rem;
    right: 1px;
    top: 0;   /* full-height box -> caret centred vertically (top:10px pushed it low / onto the border) */
}

.dark-style .select2-results__option[role=option][aria-selected=true] {
    background-color: var(--m-primary, #3a8fd0);
    color: #fff;
}

.dark-style .select2-container--default .select2-results__option--highlighted:not([aria-selected=true]) {
    background-color: rgba(77, 163, 221, 0.12) !important;
    color: var(--sem-primary-text, #6db9ec) !important;
}

.dark-style .select2-container--default .select2-selection--single {
    height: calc(3.5rem + 2px);
}

.dark-style .input-group .select2-selection.select2-selection--single {
    border-right: none;
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

/* MULTI select2 inside a .form-floating (report-builder filters, mgmt_accounts Access Countries —
   2026-07-06 floating-label consistency pass). Selects always "have a value" so the label floats
   permanently; give the multi box the standard tall field height + top padding so the tag chips sit
   BELOW the floated label instead of under it. */
.form-floating .select2-container--default .select2-selection--multiple {
    min-height: calc(3.5rem + 2px);
    padding-top: 1.35rem;
}
.form-floating .select2-container--default .select2-selection--multiple .select2-selection__rendered {
    padding-top: 0.1rem;
}
/* Report-builder filters: the placeholder carries a usage HINT ("Contains.." / "Any"), distinct from
   the floating label. The app's floating labels are ALWAYS floated (the transform rules above), so the
   hint can show permanently below the label — same look as the select pickers' "Select value" ghost
   text. The #id scope beats the app-wide transparent-placeholder rule. */
#reportFilters .form-floating > .form-control::placeholder {
    color: var(--m-text-muted, #6a6d70);
    opacity: 0.75;
}
/* …and keep the input's floated-label padding even while empty/unfocused, so the always-visible hint
   sits BELOW the label instead of vertically centred over it (mirrors the typeahead always-float fix). */
#reportFilters .form-floating > .form-control {
    padding-top: 1.625rem;
    padding-bottom: 0.625rem;
}

.form-control.typeahead.tt-input, .form-control.typeahead.tt-hint {
    height: calc(3.5rem + 2px);
    line-height: 1.25;
    padding: 1rem 0.875rem;
}
  
.form-control.typeahead.tt-input::placeholder {
    color: transparent;
}

.form-control.typeahead.tt-input:focus, .form-control.typeahead.tt-input:not(:placeholder-shown),
.form-control.typeahead.tt-hint:focus, .form-control.typeahead.tt-hint:not(:placeholder-shown) {
    padding-top: 1.625rem;
    padding-bottom: 0.625rem;
}

.form-control.typeahead.tt-input:focus ~ label,
.form-control.typeahead.tt-input:not(:placeholder-shown) ~ label {
    opacity: 0.75;
    transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
}

.light-style .twitter-typeahead .tt-suggestion:hover, .light-style .twitter-typeahead .tt-suggestion:focus {
    text-decoration: none;
    color: var(--m-primary, #004a80);
    background-color: rgba(0, 74, 128, 0.08);
}

.dark-style .twitter-typeahead .tt-suggestion:hover, .light-style .twitter-typeahead .tt-suggestion:focus {
    text-decoration: none;
    background-color: rgba(77, 163, 221, 0.12) !important;
    color: #6db9ec !important;
}

/* Edit Stock dialog: wider than the default modal-xl so the two tall Packed With / Remarks textareas
   (which hold large amounts of data) have room side by side. Only widens on >=lg so small screens keep
   the Bootstrap default. #editModal .modal-dialog beats .modal-xl on specificity (id + class). */
@media (min-width: 992px) {
  #editModal .modal-dialog { max-width: min(1380px, 94vw); }
}

.sticky-actions {
    /* MUST stay sticky: the glass `.ui-glass .card{position:relative}` rule (added for the card
       light-edge) is more specific and would otherwise demote this to relative — which both breaks
       the pin-on-scroll AND lets the JS-set `top` (navbar.bottom + 1.5rem) shove the card down,
       misaligning it from the form. `!important` keeps it sticky; at rest it sits at the column top
       (aligned with the form), and on scroll it pins just below the fixed navbar. */
    position: sticky !important;
    top: 10px; /* fallback; loan_create.js / loan_edit.js set the real offset = navbar.bottom + 1.5rem */
    align-self: flex-start;  /* don't stretch to the row height (the col is a flex item) */
}

/* ── Primary-save block (loan_edit rail) ───────────────────────────────────────────────────────
   The rail's Save button used to be the app's only `btn-dark`, which the dark theme renders as a
   pale neutral — it read as disabled and was drowned by the five saturated Loan Progress buttons
   below it ("save loan button not obvious"). Save is now `btn-primary btn-lg` (the app-wide
   primary-save convention) inside this tinted, captioned panel: the panel + the size are what keep
   it distinct from "Mark Collected", which is also btn-primary but default-size and in its own
   section. Token-driven only — never hardcode hexes here (see docs/beta-ui-theme.md). */
.save-block {
    background: rgba(var(--m-primary-rgb), 0.06);
    border: 1px solid var(--m-border-strong);
    border-radius: var(--m-radius-sm);
    padding: 0.75rem;
}

/* Unsaved-changes state — set by the dirty tracker in loan_edit.js. */
.save-block.is-dirty {
    background: rgba(var(--m-primary-rgb), 0.12);
    border-color: rgba(var(--m-primary-rgb), 0.55);
}
.save-block.is-dirty #btnSaveLoan {
    box-shadow: 0 0 0 3px var(--m-ring), 0 4px 14px rgba(var(--m-primary-rgb), 0.35);
}

/* Short attention pulse when the form FIRST becomes dirty (3 iterations, then it rests on the
   steady ring above). Motion is opt-in so reduced-motion users just get the ring. */
@media (prefers-reduced-motion: no-preference) {
    .save-block.is-dirty #btnSaveLoan {
        animation: saveBlockPulse 1.1s ease-out 3;
    }
}
@keyframes saveBlockPulse {
    0%   { box-shadow: 0 0 0 0 rgba(var(--m-primary-rgb), 0.55), 0 4px 14px rgba(var(--m-primary-rgb), 0.35); }
    70%  { box-shadow: 0 0 0 10px rgba(var(--m-primary-rgb), 0),  0 4px 14px rgba(var(--m-primary-rgb), 0.35); }
    100% { box-shadow: 0 0 0 3px var(--m-ring),                   0 4px 14px rgba(var(--m-primary-rgb), 0.35); }
}

/* ⚠ The `.ui-glass .card p` selector in the glass skin (specificity 0,2,1) beats a bare
   `.save-dirty-note` (0,1,0), so the amber was silently losing to the card's default text colour —
   caught on a rendered page, invisible in the CSS. The second selector (0,3,1) is what actually
   carries the colour under the glass skin; the first keeps it working if glass is ever off.
   Any new coloured `<p>` inside a card needs the same treatment. */
.save-dirty-note,
.ui-glass .card p.save-dirty-note {
    margin: 0.5rem 0 0;
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.3;
    text-align: center;
    color: var(--sem-warning-text, var(--bs-warning));
}
/* Leading dot so the cue isn't colour-only. */
.save-dirty-note::before {
    content: "\2022\00a0";
}

/* Terminal / read-only loans: processLoanEdit() disables Save, so drop the emphasis entirely. */
.save-block:has(#btnSaveLoan:disabled) {
    background: transparent;
    border-color: var(--m-border);
}
/* The GLOW on a disabled primary is now killed app-wide (see "DISABLED primary buttons must not glow"
   in the glass-skin section). What stays here is the bit that is specific to this block: cancelling
   the `.is-dirty` attention PULSE, which no global rule knows about. `!important` for the same reason
   the global one needs it — the glass `.btn-primary` rules are `!important`. */
.save-block:has(#btnSaveLoan:disabled) #btnSaveLoan {
    box-shadow: none !important;
    animation: none !important;
}

/* ⚠⚠ `0 auto`, NOT `0`. The vendored sweetalert2.css carries a theme override
   `.swal2-modal.swal2-popup .swal2-title { max-width: 16em }` — 512px at the title's 32px
   font-size — so on a WIDE alert the title box is narrower than the popup. `text-align: center`
   then centres the text inside that narrow box, not inside the popup, and the heading sits left.
   The auto margin centres the capped box itself.

   ⚠ It only shows on a widened alert. A default swal is ~512px, so the 16em cap never bites and
     the title looks correct — which is why this survived: every alert in the app looked fine
     except `Swal.fire({ width: 760 })` in send_warehouse.js, where the heading sat 123px left.

   ⚠ Keep the vertical 0 (that is what this rule was added for) and keep the 16em cap: it is a
     readable line-length limit for long titles. Only the horizontal auto was missing.

   Measured live before/after on the 760px "Send to RMA Team" alert: title centre offset from popup
   centre −123px → 0px. */
.swal2-modal.swal2-popup .swal2-title {
    margin: 0 auto !important;
}

.tooltip-inner {
    white-space: pre-wrap;
}


.light-style .bootstrap-select .dropdown-menu a:not([href]):not(.active):not(:active):not(.selected):hover {
    color: var(--m-primary, #004a80) !important;
}
.dark-style .bootstrap-select .dropdown-menu a:not([href]):not(.active):not(:active):not(.selected):hover {
    color: var(--sem-primary-text, #6db9ec) !important;
}

/* Stat-circle cards (Stock / Loan / Training summaries) — enlarge the circle and
   shrink the count so 4-digit numbers (e.g. 2049) fit inside. Applies app-wide. */
.avatar.avatar-xl {
    width: 5rem;
    height: 5rem;
}
.avatar.avatar-xl .avatar-initial {
    font-size: 1.15rem;
    font-weight: 600;
}

/* ---- Dark-mode contrast fixes for custom-styled pages ----
   Some pages (e.g. the loan "invoice" create form) and inputs render with
   muted/light-assuming colours. Lift their text to a readable tone in dark mode. */
.dark-style .invoice-preview-card,
.dark-style .invoice-preview-card p,
.dark-style .invoice-preview-card h1,
.dark-style .invoice-preview-card h2,
.dark-style .invoice-preview-card h3,
.dark-style .invoice-preview-card h4,
.dark-style .invoice-preview-card h5,
.dark-style .invoice-preview-card h6,
.dark-style .invoice-preview-card td,
.dark-style .invoice-preview-card th {
    color: #d9dee3;
}
/* Crestron logo on the loan create/edit "invoice" card: its SVG paths are navy (#2e5786),
   nearly invisible on the dark card — recolour them white in dark mode (matches the nav-bar
   logo). Targets the swirl + wordmark inside the invoice card's .app-brand. */
.dark-style .invoice-preview-card .app-brand svg path[fill] {
    fill: #ffffff !important;
}

/* Crestron logo in the "Crestron / Third Party" radio selector of the Add-Model modal
   (pages/model.php + pages/stock.php, #divModalCrestronLogo). Same navy problem as the invoice
   card above: MEASURED 2.12:1 against the dark modal surface rgb(31,35,40), failing WCAG's 3:1
   for graphics. Of the five hardcoded #2e5786 brand marks in the app this was the only one with
   no dark-mode rule — the sidebar, mobile navbar, login card and invoice card all already have one.
   ⚠ Targets `path`, NOT `path[fill]` like its siblings: this mark has two paths and only the swirl
   carries a fill attribute. The other is the ™ glyph, which inherits BLACK and is just as
   invisible here — `path[fill]` would silently leave it behind. */
.dark-style #divModalCrestronLogo path {
    fill: #e9eef3 !important;
}
/* Readable input / date-picker text */
.dark-style input.form-control,
.dark-style textarea.form-control,
.dark-style .flatpickr-input,
.dark-style .dataTable td,
.dark-style .dataTable th {
    color: #d9dee3;
}
/* Switch/toggle that was near-white on the dark card */
.dark-style .form-check-input {
    background-color: var(--m-surface-2, #262b31);
    border-color: #4a525b;
}
.dark-style .form-check-input:checked {
    background-color: var(--m-primary, #3a8fd0);
    border-color: var(--m-primary, #3a8fd0);
}

/* FullCalendar event text — the theme renders event links in blue, which is
   unreadable on the coloured fills. Force white on the month/week event blocks
   (these always have a coloured background). The list view is left alone so its
   row text stays theme-correct in light mode. */
.fc-daygrid-event .fc-event-title,
.fc-daygrid-event .fc-event-time,
.fc-daygrid-block-event .fc-event-title,
.fc-timegrid-event .fc-event-title,
.fc-timegrid-event .fc-event-time,
.fc-daygrid-event .fc-event-title sup,
.fc-timegrid-event .fc-event-title sup {
    color: #fff !important;
}

/* Month-view event spacing — the vendor `fullcalendar.css` adds `margin-top:0.3rem` between stacked
   day-grid events (`.fc-daygrid-event-harness + .fc-daygrid-event-harness`). That margin only applies to
   the IN-FLOW single-day bars; FullCalendar positions multi-day bars ABSOLUTELY with a top computed as
   level×event-height that IGNORES the margin, so a multi-day bar (e.g. a class spanning several days) sits
   flush against the event above it while the single-day bars have a 0.3rem gap → uneven spacing. Zero the
   margin so every bar stacks uniformly at level×height (the same basis the absolute bars already use).
   Paired with `eventOrder:'duration,start,title'` in cti_calendar.js (single-day first, multi-day last). */
.fc .fc-daygrid-event-harness + .fc-daygrid-event-harness {
    margin-top: 0 !important;
}

/* Dark-mode FullCalendar grid lines — the day-cell borders wash out against the dark/glass
   backdrop, so the grid is invisible. Set the FC border variable AND force the colour directly
   on the cells/scrollgrid (the glass skin's translucent cells were blending the default away).
   Light mode is left to the theme default. */
.dark-style .fc {
    --fc-border-color: rgba(255, 255, 255, 0.18);
    --fc-today-bg-color: rgba(255, 255, 255, 0.05);
}
.dark-style .fc-theme-standard td,
.dark-style .fc-theme-standard th,
.dark-style .fc-theme-standard .fc-scrollgrid,
.dark-style .fc-scrollgrid-section > * {
    border-color: rgba(255, 255, 255, 0.18) !important;
}

/* Dark-mode FullCalendar toolbar buttons (today / prev-next / Month-Week-List). The rebranded Sneat
   dark theme (theme-default-dark.css) tints the view buttons with a pale primary fill via
   `.fc .fc-button-primary:not(.fc-prev-button):not(.fc-next-button){ background:rgba(primary,.16)
   !important }` (specificity 0,4,0) + mid-blue text — readable on a white light canvas but washed out
   on the dark/glass backdrop; the "today" button is also disabled (opacity .65) whenever the current
   view already contains today, making it fainter still. Beat that rule with the always-present
   `.ui-modern.ui-glass.dark-style` triple-class (0,5,0) so EVERY toolbar button gets a solid surface +
   visible border + legible text; the ACTIVE view button gets a solid brand fill and the disabled
   "today" a muted-but-legible look. Light mode keeps the theme default. */
.ui-modern.ui-glass.dark-style .fc .fc-button-primary {
    background-color: var(--m-surface-2, #262b31) !important;
    border: 1px solid var(--m-border-strong, #414851) !important;
    color: var(--m-text, #e2e5e8) !important;
    opacity: 1 !important;
    box-shadow: none !important;
}
.ui-modern.ui-glass.dark-style .fc .fc-button-primary:hover:not(:disabled) {
    background-color: var(--m-surface, #1f2328) !important;
    border-color: var(--m-primary, #3a8fd0) !important;
    color: var(--m-text, #e2e5e8) !important;
}
.ui-modern.ui-glass.dark-style .fc .fc-button-primary:not(:disabled).fc-button-active {
    background-color: var(--m-primary, #3a8fd0) !important;
    border-color: var(--m-primary, #3a8fd0) !important;
    color: #fff !important;
}
/* "today" when disabled (already viewing today): keep it clearly legible, just muted. */
.ui-modern.ui-glass.dark-style .fc .fc-button-primary:disabled {
    background-color: var(--m-surface-2, #262b31) !important;
    border-color: var(--m-border, #32383f) !important;
    color: var(--m-text-muted, #9aa1a8) !important;
    opacity: 1 !important;
}

/* FullCalendar toolbar button GEOMETRY + label (both themes). The vendor gives the ‹ › icon buttons a
   shorter height (38px) and the prev button an asymmetric 0 left-padding, so they don't line up with the
   today / view buttons (40px); it also `text-transform:capitalize`s the view buttons but leaves "today"
   lowercase. Normalize every toolbar button to one height + centered content + capitalize (so "today"
   reads "Today", matching Month/Week/List). */
.fc .fc-toolbar .fc-button {
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    min-height: 40px;
    padding-top: 0.45rem !important;
    padding-bottom: 0.45rem !important;
    text-transform: capitalize !important;
}
.fc .fc-toolbar .fc-prev-button,
.fc .fc-toolbar .fc-next-button {
    padding-left: 0.7rem !important;
    padding-right: 0.7rem !important;
}
/* Equal-width view buttons + legend badges — the labels differ in length (Month/Week/List,
   Zoom/Classroom/Overseas) but should read as one uniform set of boxes. Fit the widest with a
   min-width and centre the text (the toolbar buttons already centre via the inline-flex rule above;
   the `.cal-legend` container is on the calendar page's legend row). */
.fc .fc-toolbar .fc-dayGridMonth-button,
.fc .fc-toolbar .fc-timeGridWeek-button,
.fc .fc-toolbar .fc-listMonth-button {
    min-width: 5.5rem;
}
.cal-legend .badge {
    min-width: 6rem;
    display: inline-flex;      /* center the label BOTH axes — the badge has 0 vertical padding, so
                                  inline-block left the text top-aligned ("floating") in the 24px box */
    align-items: center;
    justify-content: center;
}

/* ---- Global navbar search (cassets/js/common.js) ---- */
.gs-wrap { position: relative; min-width: 260px; }
.gs-input {
    border: 0;
    box-shadow: none;
    background: transparent;
    padding-left: 0;
    height: 38px;
}
.gs-input:focus { border: 0; box-shadow: none; background: transparent; }
.global-search-results {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    min-width: 340px;
    background: #fff;
    border: 1px solid #d9dee3;
    border-top: none;
    border-radius: 0 0 0.375rem 0.375rem;
    box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.18);
    max-height: 60vh;
    overflow-y: auto;
    overflow-x: hidden;
    z-index: 1090;
    padding-bottom: 0.25rem;
}
.dark-style .global-search-results { background: var(--m-surface, #1f2328); border-color: var(--m-border, #32383f); }
.gsr-group-title {
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #a1acb8;
    padding: 0.55rem 0.95rem 0.2rem;
    font-weight: 600;
}
.gsr-item {
    display: flex;
    align-items: center;
    padding: 0.5rem 0.95rem;
    color: #566a7f;
    text-decoration: none;
    line-height: 1.2;
    overflow: hidden;               /* a single row never causes horizontal scroll */
}
.gsr-item > i { flex: 0 0 auto; }   /* icon keeps its size */
.dark-style .gsr-item { color: #d9dee3; }
.gsr-item:hover { background: #f5f5f9; color: #566a7f; }
.dark-style .gsr-item:hover { background: #2c3238; color: #fff; }
.gsr-label {
    font-weight: 500;
    flex: 1 1 auto;
    min-width: 0;                   /* allow the label to shrink + ellipsize */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.gsr-sub {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 50%;                 /* never let the sub crowd out the label */
    margin-left: auto;
    padding-left: 1rem;
    font-size: 0.8rem;
    color: #a1acb8;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-align: right;
}
.gsr-empty { padding: 0.75rem 0.95rem; color: #a1acb8; }

/* Floating hover-preview card for a search result (appended to <body>, position:fixed). */
.gsr-preview {
    position: fixed;
    z-index: 1095;                  /* above .global-search-results (1090) */
    width: 260px;
    max-width: 80vw;
    background: #fff;
    border: 1px solid #d9dee3;
    border-radius: 0.375rem;
    box-shadow: 0 0.5rem 1.25rem rgba(0, 0, 0, 0.22);
    padding: 0.55rem 0.7rem;
    font-size: 0.82rem;
    color: #566a7f;
    pointer-events: none;           /* never steal hover/clicks from the result list */
}
.gsr-prev-head {
    display: flex;
    align-items: center;
    font-weight: 600;
    color: #384551;
    padding-bottom: 0.4rem;
    margin-bottom: 0.4rem;
    border-bottom: 1px solid #eceef1;
    white-space: nowrap;
    overflow: hidden;
}
.gsr-prev-head > span { overflow: hidden; text-overflow: ellipsis; }
.gsr-prev-row { display: flex; gap: 0.75rem; padding: 0.12rem 0; }
.gsr-prev-k { flex: 0 0 38%; color: #a1acb8; }
.gsr-prev-v {
    flex: 1 1 auto;
    min-width: 0;
    text-align: right;
    word-break: break-word;
    font-weight: 500;
}
/* Muted second line under a value — mirrors the datatable's stacked cell (e.g. model / part_no). */
.gsr-prev-sub { display: block; font-weight: 400; font-size: 0.76rem; color: #a1acb8; line-height: 1.25; }
.dark-style .gsr-prev-sub { color: #9aa1a8; }
.ui-modern .gsr-prev-sub { color: var(--m-text-muted, #a1acb8) !important; }
.dark-style .gsr-preview { background: var(--m-surface, #1f2328); border-color: var(--m-border, #32383f); color: #d9dee3; }
.dark-style .gsr-prev-head { color: #fff; border-bottom-color: var(--m-border, #32383f); }
.dark-style .gsr-prev-k { color: #9aa1a8; }
.ui-modern .gsr-preview {
    background: var(--m-surface) !important;
    border-color: var(--m-border) !important;
    color: var(--m-text) !important;
    border-radius: var(--m-radius, 0.5rem) !important;
}
.ui-modern .gsr-prev-head { color: var(--m-text) !important; border-bottom-color: var(--m-border) !important; }
.ui-modern .gsr-prev-k { color: var(--m-text-muted, #a1acb8) !important; }

/* ============================================================================
   Sidebar collapse/pin toggle + brand alignment + collapsed-rail centering.
   Theme-level (not .ui-modern-scoped) so it holds regardless; the modern look's
   navy-sidebar toggle hover colour is in the MODERN UI block further below.
   (2026-06-22)
   ============================================================================ */

/* 1) Pin/collapse control — replace the theme's stray radio-dot with a clear
      directional chevron + a real button affordance (hover background). */
.layout-menu-toggle .menu-toggle-icon::before { content: "\ea60"; }                       /* chevron-left  = collapse */
.layout-menu-collapsed .layout-menu-toggle .menu-toggle-icon::before { content: "\ea61"; } /* chevron-right = expand   */
/* Scope the 30x30 rounded box ONLY to the SIDEBAR toggle (#layout-menu). The navbar
   hamburger in nav_bar.php is ALSO a `.layout-menu-toggle` (a <div> wrapping an
   <a class="nav-link"> + ti-menu-2 icon); the forced 30x30 box clipped that larger
   link and left the hover background off-centre from the icon. */
#layout-menu .layout-menu-toggle {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border-radius: 8px;
    transition: background-color .15s ease, color .15s ease;
}
#layout-menu .layout-menu-toggle .menu-toggle-icon { font-size: 1.125rem !important; }
.light-style #layout-menu .layout-menu-toggle:hover { background-color: rgba(67, 89, 113, 0.08); }
.dark-style #layout-menu .layout-menu-toggle:hover  { background-color: rgba(255, 255, 255, 0.08); }

/* Navbar hamburger (mobile/tablet, .navbar .layout-menu-toggle) — give it its OWN
   centred hover affordance on the link, so the rounded box is aligned around the icon. */
.navbar .layout-menu-toggle { display: flex; align-items: center; }
.navbar .layout-menu-toggle .nav-link {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    padding: 0 !important;
    border-radius: 8px;
    line-height: 1;
    transition: background-color .15s ease, color .15s ease;
}
.light-style .navbar .layout-menu-toggle .nav-link:hover { background-color: rgba(67, 89, 113, 0.08); }
.dark-style .navbar .layout-menu-toggle .nav-link:hover  { background-color: rgba(255, 255, 255, 0.08); }

/* 2) Line the brand (logo + pin) up with the detached navbar's search bar. Move the
      WHOLE brand block down ~15px (margin-top) rather than just the logo, so the menu
      shifts with it and the spacing below the logo stays even. */
#layout-menu .app-brand { margin-top: 15px; }

/* 2b) Menu items must FILL the rail width, not the vendor's fixed `width:16.25rem` (=260px, the full
      rail width) — combined with the modern skin's 0.65rem side margins that pushed each item ~10px
      PAST the right edge (clipped by the rail's overflow:hidden), so the active pill's right gap
      collapsed to ~4px while its left gap stayed ~24px (the "pill extends to the right" bug). The rail's
      .menu-inner is a flex column that doesn't stretch its children, so force `align-self:stretch` +
      `width:auto`: the item then fills the rail minus its margins and the pill is symmetric both sides.
      Skin-agnostic (the fixed width is vendor). Collapsed rail sets its own item/link sizing, unaffected. */
#layout-menu .menu-inner > .menu-item { width: auto !important; align-self: stretch !important; }

/* 3) Collapsed (icon-only) rail — center each item as a fixed-size pill so the
      active highlight and icons sit dead-centre. The theme otherwise anchors the
      pill to the right (tiny right gap, heavy left gap). Turned off while hovering,
      when the collapsed menu temporarily expands to show its labels. */
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .menu-inner > .menu-item {
    margin-left: 0 !important;
    margin-right: 0 !important;
}
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .menu-inner > .menu-item > .menu-link {
    width: 2.625rem !important;
    height: 2.625rem !important;
    margin: 0.15rem auto !important;
    padding: 0 !important;
    justify-content: center !important;
}
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .menu-inner > .menu-item > .menu-link > div { display: none !important; }
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .menu-inner .menu-icon { margin-right: 0 !important; }
/* ...and center the brand logo (hide the wordmark) so the swirl sits dead-centre too. */
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .app-brand {
    justify-content: center !important;
    padding-left: 0 !important;
    padding-right: 0 !important;
    /* The theme gives collapsed .app-brand a FIXED `width:5.25rem` (core.css) AND an asymmetric
       1rem-left margin, so the box (the modern divider is a border-bottom on it) overflowed the
       narrow rail to the RIGHT. Drop the fixed width to auto so the brand fills the rail, then
       equal margins centre the divider line. */
    width: auto !important;
    margin-left: 0.6rem !important;
    margin-right: 0.6rem !important;
}
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .app-brand-text { display: none !important; }
/* Hide the collapse/expand toggle in the collapsed rail. The theme only sets it to opacity:0,
   but our modern skin gives it a fixed 30px width, so it still takes flex space — and with
   justify-content:center on .app-brand that pushes the centred swirl logo off to the LEFT.
   display:none removes it from the layout so the logo sits dead-centre. Scoped to
   :not(.layout-menu-hover), so the toggle returns when the menu is hovered/expanded. */
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .app-brand .layout-menu-toggle { display: none !important; }

/* ============================================================================
   Sidebar CHROME consolidated into the rail (2026-07-10).
   The old top navbar's utilities now live inside #layout-menu: the global
   search pinned at the top, and a user card pinned at the bottom (avatar +
   name/role, notifications bell, and an up-popping profile menu with the
   Dark Mode switch + Log Out). Colours use the existing --m-sidebar-*/--g-sidebar-*
   tokens, which already flip per light/dark, so most rules need no mode variant.
   ============================================================================ */

/* Make the rail a column so the search sits at top, the scrollable menu grows,
   and the user card pins to the bottom. (.menu-inner is already flex:1 in Sneat.) */
#layout-menu.layout-menu { display: flex; flex-direction: column; }
#layout-menu .menu-inner { flex: 1 1 auto; }

/* ---- Search at the top of the rail ---- */
#layout-menu .menu-search { position: relative; z-index: 6; flex: 0 0 auto; }
#layout-menu .menu-search .gs-wrap { min-width: 0; width: 100%; }
#layout-menu .menu-search .gs-input {
  height: 40px;
  width: 100%;
  padding: 0 0.85rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.10);
  color: var(--m-sidebar-text, #dde6ee);
}
.ui-glass #layout-menu .menu-search .gs-input { background: rgba(255, 255, 255, 0.12); color: var(--g-sidebar-text, #e9eef3); }
/* Placeholder must stay legible on the translucent pill — mut(#9fb1c1) was too faint,
   so use the brighter sidebar-text tone dimmed slightly rather than the muted grey. */
#layout-menu .menu-search .gs-input::placeholder { color: var(--m-sidebar-text, #dde6ee); opacity: 0.85; }
.ui-glass #layout-menu .menu-search .gs-input::placeholder { color: var(--g-sidebar-text, #e9eef3); opacity: 0.85; }
#layout-menu .menu-search .gs-input:focus {
  background: rgba(255, 255, 255, 0.16);
  color: var(--m-sidebar-text, #dde6ee);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.12);
}
/* The results dropdown is injected inside .gs-wrap; keep it within the rail width. */
#layout-menu .menu-search .global-search-results { min-width: 0; left: 0; right: 0; }

/* ---- User card pinned at the bottom ---- */
#layout-menu .menu-user {
  flex: 0 0 auto;
  margin-top: auto;                 /* safety: pin to bottom even if .menu-inner isn't flex:1 */
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.6rem 0.75rem;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
#layout-menu .menu-user-card { flex: 1 1 auto; min-width: 0; order: 1; }   /* card left (avatar+name) */
#layout-menu .menu-user-toggle {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.4rem 0.5rem;
  border-radius: var(--m-radius-sm, 0.5rem);
  text-decoration: none;
  transition: background-color .18s ease;
}
#layout-menu .menu-user-toggle:hover { background: var(--m-sidebar-hover, rgba(255,255,255,0.06)); }
#layout-menu .menu-user-meta { min-width: 0; line-height: 1.2; }
#layout-menu .menu-user-name { color: var(--m-sidebar-text, #dde6ee); font-size: 0.9rem; }
.ui-glass #layout-menu .menu-user-name { color: var(--g-sidebar-text, #e9eef3); }
#layout-menu .menu-user-sub { color: var(--m-sidebar-mut, #9fb1c1); font-size: 0.75rem; }
.ui-glass #layout-menu .menu-user-sub { color: var(--g-sidebar-mut, #b3c1cd); }
#layout-menu .menu-user-caret { color: var(--m-sidebar-mut, #9fb1c1); font-size: 1rem; flex: 0 0 auto; transition: transform .18s ease; }
#layout-menu .menu-user-card.show .menu-user-caret { transform: rotate(180deg); }

/* Notifications bell in the card */
#layout-menu .menu-user-notif { flex: 0 0 auto; order: 2; }   /* bell to the right of the card */
#layout-menu .menu-user-notif-toggle {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  color: var(--m-sidebar-mut, #9fb1c1);
  text-decoration: none;
  transition: background-color .18s ease, color .18s ease;
}
.ui-glass #layout-menu .menu-user-notif-toggle { color: var(--g-sidebar-mut, #b3c1cd); }
#layout-menu .menu-user-notif-toggle:hover { background: var(--m-sidebar-hover, rgba(255,255,255,0.06)); color: #fff; }
#layout-menu .menu-user-notif-toggle .ti-bell { font-size: 1.25rem; }
#layout-menu .menu-user-notif-toggle .badge-notifications {
  position: absolute;
  top: 4px;
  right: 4px;
  transform: translate(35%, -35%);
  /* Force a CIRCLE (the default .badge padding made a tall 17x24 oval for a 2-digit count). */
  min-width: 18px;
  height: 18px;
  padding: 0 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  font-size: 0.62rem;
  line-height: 1;
}

/* The up-popping menus (profile + notifications) — .dropdown-menu is already themed by the
   .ui-modern/.ui-glass .dropdown-menu rules. ⚠ The rail is .menu-vertical{overflow:hidden} (vendor
   core.css) and can't be unset (the collapse width-transition needs it to clip labels), so BOTH menus
   must stay NARROWER than the 16.25rem rail or their far edge is clipped. Popper auto-flips the bell
   menu upward (it sits at the very bottom) and the profile menu is an explicit .dropup. */
#layout-menu .menu-user-card .dropdown-menu { min-width: 210px; max-width: 15rem; }
#layout-menu .menu-user-notif .dropdown-menu { min-width: 256px; max-width: 16rem; width: 16rem; }
/* The notifications list can be long (one row per overdue loan); cap it so the dropup stays on-screen
   and scrolls internally (the theme's perfect-scrollbar doesn't init on this relocated element). */
#layout-menu .menu-user-notif .dropdown-notifications-list { max-height: 44vh; overflow-y: auto; }
#layout-menu .menu-user-notif .dropdown-notifications-list .list-group-item { white-space: normal; }
/* Dark Mode switch row: the whole row is the .style-switcher-toggle trigger, so the switch is
   visual-only (the row's click flips the theme via assets/js/main.js). */
#layout-menu .menu-user .style-switcher-toggle .form-check-input { pointer-events: none; cursor: pointer; }
#layout-menu .menu-user .style-switcher-toggle { cursor: pointer; }

/* Collapsed icon-rail: hide the search + the card's text/caret/bell; show only the avatar. */
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .menu-search,
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .menu-user-meta,
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .menu-user-caret,
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .menu-user-notif { display: none !important; }
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .menu-user { justify-content: center; padding-inline: 0; }
.layout-menu-collapsed:not(.layout-menu-hover) #layout-menu .menu-user-toggle { justify-content: center; gap: 0; }

/* ---- Slim MOBILE-ONLY top bar (nav_bar.php is d-xl-none) ---- */
.layout-navbar .mobile-brand { text-decoration: none; }
.layout-navbar .mobile-brand .app-brand-logo svg path[fill] { fill: var(--m-primary, #004a80); }
.dark-style .layout-navbar .mobile-brand .app-brand-logo svg path[fill] { fill: #fff; }

/* ============================================================================
   Responsive / mobile fixes (theme-level, not skin-scoped). 2026-06-22.
   ============================================================================ */

/* (a) Off-canvas menu backdrop: the theme can leave `.layout-overlay` stuck as a
       ~30px, 0.5-opacity grey strip down the left edge on narrow screens. Force it
       fully hidden when the menu is closed, and full-screen when it's open. */
html:not(.layout-menu-expanded) .layout-overlay {
    opacity: 0 !important;
    width: 0 !important;
    pointer-events: none !important;
}
html.layout-menu-expanded .layout-overlay {
    display: block !important;
    width: 100% !important;
    height: 100% !important;
    opacity: 1 !important;
}

/* Dashboard "My Loans" cards are clickable links to the pre-filtered loan list — give them a
   pointer + subtle hover lift so it's obvious they're tappable (same intent as the statCard links). */
.dash-card-link { display: block; cursor: pointer; transition: transform .15s ease, box-shadow .15s ease; }
.dash-card-link:hover { transform: translateY(-2px); box-shadow: 0 0.5rem 1.25rem rgba(0,0,0,0.12); }

/* (a2) Off-screen `.offcanvas-end` panels sit translated past the right edge and cause a
        horizontal scrollbar. Clip the page horizontally — scoped to mobile so desktop
        `position: sticky` is unaffected.
        (`overflow-x: clip` and html-only don't clip the fixed panel; html+body hidden does.)
        ⚠ The example this used to name (mgmt_accounts' add/edit forms) is NO LONGER an offcanvas —
        both became modals and only the `offcanvasAddUser` id survives, which is misleading if you
        grep for it. `tech_schedule` (Add Event) is the only offcanvas left in the app, and it
        overflowed ~275px on DESKTOP, where this rule does not reach; it carries its own page-scoped
        clip rather than widening this one. Re-check that page if you ever make this global. */
@media (max-width: 767.98px) {
    html, body { overflow-x: hidden; }
}

/* (b) Compact KPI/stat tiles on phones: the full-width "big circle centred over a
       label" cards are far too tall stacked on mobile. Reflow them to a short
       horizontal row (circle left, label right). Targets any card-body holding an
       avatar-xl stat circle (dashboard, stock, loan, cti summary tiles). */
@media (max-width: 767.98px) {
    .card-body:has(.avatar-xl) {
        display: flex;
        align-items: center;
        gap: 1rem;
        padding: 1rem 1.25rem;
    }
    .card-body:has(.avatar-xl) > .d-flex {
        margin-bottom: 0 !important;
        justify-content: flex-start !important;
        flex: 0 0 auto;
    }
    .card-body:has(.avatar-xl) > .text-center { text-align: left !important; }
    .card-body:has(.avatar-xl) h5 { margin-bottom: 0; }
    .avatar.avatar-xl { width: 3.25rem !important; height: 3.25rem !important; }
    .avatar.avatar-xl .avatar-initial { font-size: 0.95rem !important; }
}

/* (c) DataTables top controls (length / search / Export / Add) misalign below the theme's xl
       breakpoint: they live in a nested grid (`.col-md-2` length + `.col-md-10 > .dt-action-buttons`,
       the latter `flex-direction:column`), so items in different containers never line up. FLATTEN
       the whole thing into ONE flex row with `display:contents` — length/search/buttons become
       siblings of the row and align uniformly; the search grows, and it all wraps into clean,
       aligned rows when it can't fit on one line. Covers everything below xl (the theme only goes
       inline at >=1200). */
@media (max-width: 1199.98px) {
    .dataTables_wrapper .row:has(.dataTables_length, .dt-action-buttons) {
        display: flex !important;
        flex-wrap: wrap !important;
        align-items: center !important;
        gap: .5rem !important;
        margin: 0 !important;
        padding: .6rem .25rem .8rem !important;   /* vertical breathing room */
    }
    /* dissolve the grid/wrapper layers so their children join the row's flex context */
    .dataTables_wrapper .row:has(.dataTables_length, .dt-action-buttons) > [class*="col-"],
    .dataTables_wrapper .col-md-2 .me-3,
    .dt-action-buttons { display: contents !important; }
    .dataTables_length { display: flex !important; align-items: center; margin: 0 !important; }
    .dataTables_length label { margin: 0; display: flex; align-items: center; }
    .dataTables_length select { width: auto; min-width: 52px; }
    .dataTables_filter { flex: 1 1 140px !important; min-width: 130px; margin: 0 !important; }
    .dataTables_filter label { width: 100%; margin: 0; }
    .dataTables_filter input { width: 100% !important; margin: 0 !important; }
    /* buttons must NOT grow (display:contents lets the theme's flex:1 1 auto make "Add" stretch
       to fill the row) — keep them compact so they sit inline / wrap as tidy chips */
    .dataTables_wrapper .dt-buttons,
    .dataTables_wrapper .add-new,
    .dataTables_wrapper .btn { flex: 0 0 auto !important; white-space: nowrap; }
}

/* (c3) DataTables Buttons "Export" popover: vendor buttons.bootstrap5.css paints the WRAPPER
       (div.dt-button-collection) as a hard-coded white panel (bg + border + 0.5rem padding) and
       flattens the inner .dropdown-menu to transparent. Inside the card the white frame was mostly
       clipped away; since the popover is portaled to <body> (buttons-popover.dt handler in
       common.js) it shows fully — a white halo around the menu in dark mode. Make the wrapper
       invisible and give the panel look back to the inner .dropdown-menu via the theme's own
       dropdown tokens (--bs-dropdown-*, defined on .dropdown-menu by core/core-dark — so light and
       dark both follow). !important: the per-page *_css.inc vendor links load AFTER common.css, so
       plain equal-specificity overrides would lose on cascade order. */
div.dt-button-collection {
    background-color: transparent !important;
    border: 0 !important;
    box-shadow: none !important;
    padding: 0 !important;
}
div.dt-button-collection div.dropdown-menu {
    background-color: var(--bs-dropdown-bg) !important;
    border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color) !important;
    border-radius: var(--bs-dropdown-border-radius) !important;
    box-shadow: var(--bs-dropdown-box-shadow) !important;
    padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x) !important;
}

/* (c2) Navbar global-search: its `.gs-wrap{min-width:260px}` is too wide on phones — it pushes the
        right-side icons (incl. the profile avatar) off the edge. Let it shrink on small screens. */
@media (max-width: 575.98px) {
    .gs-wrap { min-width: 0 !important; }
}

/* (d) Admin merge tools (mgmt_companies/contacts/models) — the two card-header action
       buttons ("Merge Selected into Main" btn-danger + "Suggest Duplicates"
       btn-outline-info) are inline-flex of different widths, so they wrap awkwardly on
       phones. Match them: both full-width, stacked, on mobile. The :has() combo is unique
       to those merge headers, so it won't touch ordinary card-header buttons. */
@media (max-width: 767.98px) {
    .card-header:has(.btn-danger):has(.btn-outline-info) > .btn {
        display: flex !important;
        width: 100% !important;
        margin-left: 0 !important;
        justify-content: center;
    }
    .card-header:has(.btn-danger):has(.btn-outline-info) > .btn + .btn { margin-top: .5rem !important; }
}

/* ============================================================================
   MODERN UI (merged in 2026-06-22, now the DEFAULT look — formerly the opt-in
   redesign/redesign.css skin). main.php / login.php hard-add the `.ui-modern`
   class to <html> (login also `page-login`), so every rule below always applies.
   Crestron brand palette (derived from www.crestron.com, 2026-07-06): silver
   #f1f1f1 canvas, charcoal #32363a type, slate-blue #27455c sidebar, logo-blue
   #004a80 actions; dark mode = matching charcoal family with brightened blue.
   Plus Jakarta Sans, soft radii, refined elevation, light + dark.
   To extend: add rules under `.ui-modern[.light-style|.dark-style]` using the
   tokens defined just below.
   ============================================================================ */

/* ---- 1. Design tokens (per theme) ---- */
.ui-modern.light-style {
  --m-bg:            #f1f1f1;   /* crestron.com body silver */
  --m-surface:       #ffffff;
  --m-surface-2:     #f6f7f8;
  --m-border:        #e4e6e8;
  --m-border-strong: #c8ccd0;
  --m-text:          #32363a;   /* Crestron charcoal (site-exact) */
  --m-text-muted:    #6a6d70;   /* Crestron gray (site-exact) */
  --m-heading:       #23272b;
  --m-primary:       #004a80;   /* Crestron logo blue (pixel-exact) */
  --m-primary-rgb:   0, 74, 128;
  --m-primary-hover: #003a66;
  --m-accent:        #0072ce;
  --m-sidebar-bg:    #27455c;   /* Crestron dark slate-blue (site-exact) */
  --m-sidebar-grad:  linear-gradient(180deg, #2d4f6b 0%, #223d52 100%);
  --m-sidebar-text:  #dde6ee;   /* non-active item label — legible on slate */
  --m-sidebar-mut:   #9fb1c1;   /* icons / submenu / toggle */
  --m-sidebar-hover: rgba(255, 255, 255, 0.06);
  --m-sidebar-active:#0072ce;
  --m-shadow-sm:     0 1px 2px rgba(35, 39, 43, 0.06), 0 1px 3px rgba(35, 39, 43, 0.05);
  --m-shadow-md:     0 4px 12px rgba(35, 39, 43, 0.08), 0 2px 4px rgba(35, 39, 43, 0.04);
  --m-shadow-lg:     0 12px 32px rgba(35, 39, 43, 0.14);
  --m-radius:        0.75rem;
  --m-radius-sm:     0.5rem;
  --m-ring:          rgba(0, 74, 128, 0.25);
}
.ui-modern.dark-style {
  --m-bg:            #16191d;   /* charcoal family derived from Crestron #32363a / #41484f */
  --m-surface:       #1f2328;
  --m-surface-2:     #262b31;
  --m-border:        #32383f;
  --m-border-strong: #414851;   /* echoes crestron.com steel #41484f */
  --m-text:          #e2e5e8;
  --m-text-muted:    #9aa1a8;
  --m-heading:       #f2f4f6;
  --m-primary:       #3a8fd0;   /* brightened logo blue for dark surfaces */
  --m-primary-rgb:   58, 143, 208;
  --m-primary-hover: #4da3dd;
  --m-accent:        #59c2ff;
  --m-sidebar-bg:    #14181c;
  --m-sidebar-grad:  linear-gradient(180deg, #1a2027 0%, #101418 100%);
  --m-sidebar-text:  #d9dee3;   /* non-active item label — legible on charcoal */
  --m-sidebar-mut:   #8f9aa5;   /* icons / submenu / toggle */
  --m-sidebar-hover: rgba(255, 255, 255, 0.05);
  --m-sidebar-active:#3a8fd0;
  --m-shadow-sm:     0 1px 2px rgba(0, 0, 0, 0.4);
  --m-shadow-md:     0 4px 14px rgba(0, 0, 0, 0.45);
  --m-shadow-lg:     0 16px 40px rgba(0, 0, 0, 0.6);
  --m-radius:        0.75rem;
  --m-radius-sm:     0.5rem;
  --m-ring:          rgba(77, 163, 221, 0.35);
}

/* ---- 2. Typography + canvas ---- */
.ui-modern,
.ui-modern body,
.ui-modern .menu,
.ui-modern .btn,
.ui-modern .form-control,
.ui-modern .form-select {
  font-family: 'Plus Jakarta Sans', -apple-system, 'Segoe UI', Roboto, system-ui, sans-serif;
}
.ui-modern body { background: var(--m-bg) !important; color: var(--m-text); letter-spacing: -0.002em; }
.ui-modern h1, .ui-modern h2, .ui-modern h3,
.ui-modern h4, .ui-modern h5, .ui-modern h6 { color: var(--m-heading); font-weight: 700; letter-spacing: -0.01em; }
.ui-modern .text-muted { color: var(--m-text-muted) !important; }
.ui-modern .table td,
.ui-modern .table th,
.ui-modern .avatar-initial,
.ui-modern .card .display-6,
.ui-modern .card h3 { font-variant-numeric: tabular-nums; }

/* ---- 3. Sidebar (deep navy, both modes) ---- */
.ui-modern #layout-menu.layout-menu,
.ui-modern .bg-menu-theme {
  background: var(--m-sidebar-grad) !important;
  border-right: 1px solid rgba(255, 255, 255, 0.04) !important;
  box-shadow: 0 0 24px rgba(0, 0, 0, 0.18);
}
.ui-modern #layout-menu .menu-inner-shadow { display: none; }
.ui-modern #layout-menu .app-brand {
  padding-block: 1.1rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  margin-bottom: 0.35rem;
}
.ui-modern #layout-menu .app-brand svg path[fill] { fill: #ffffff !important; }
.ui-modern #layout-menu .menu-toggle-icon,
.ui-modern #layout-menu .layout-menu-toggle i { color: var(--m-sidebar-mut) !important; }
.ui-modern #layout-menu .layout-menu-toggle:hover { background: var(--m-sidebar-hover) !important; color: #fff !important; }
.ui-modern #layout-menu .menu-inner > .menu-item { margin: 0.1rem 0.65rem; }
.ui-modern #layout-menu .menu-link {
  color: var(--m-sidebar-text) !important;
  border-radius: var(--m-radius-sm);
  font-weight: 500;
  transition: background-color .18s ease, color .18s ease;
}
.ui-modern #layout-menu .menu-inner > .menu-item > .menu-link { padding: 0.6rem 0.85rem; }
.ui-modern #layout-menu .menu-link .menu-icon { color: var(--m-sidebar-mut) !important; }
.ui-modern #layout-menu .menu-item:not(.active) > .menu-link:hover { background: var(--m-sidebar-hover) !important; color: #ffffff !important; }
.ui-modern #layout-menu .menu-item:not(.active) > .menu-link:hover .menu-icon { color: #fff !important; }
.ui-modern #layout-menu .menu-item.active > .menu-link {
  background: var(--m-sidebar-active) !important;
  color: #ffffff !important;
  font-weight: 600;
  box-shadow: 0 4px 12px rgba(0, 114, 206, 0.30);
}
.ui-modern #layout-menu .menu-item.active > .menu-link::before { display: none; }
.ui-modern.light-style #layout-menu .menu-item.active > .menu-link { background: var(--m-sidebar-active) !important; }
.ui-modern #layout-menu .menu-item.active > .menu-link .menu-icon { color: #fff !important; }
.ui-modern #layout-menu .menu-sub .menu-link { color: var(--m-sidebar-mut) !important; padding-block: 0.45rem; }
/* sub-item bullet = outline circle (active = filled brand dot) */
.ui-modern #layout-menu .menu-sub .menu-link::before {
  content: "" !important;
  width: 7px !important;
  height: 7px !important;
  border-radius: 50% !important;
  background: transparent !important;
  border: 1.5px solid var(--m-sidebar-mut) !important;
  top: 50% !important;
  left: 1.35rem !important;
  transform: translateY(-50%) !important;
}
.ui-modern #layout-menu .menu-sub .menu-link:hover::before { border-color: #fff !important; }
/* Active SUB-item (under an open parent): a SUBTLE highlight + the filled dot — NOT the
   full active pill/glow (that heavy treatment is for top-level items; on an indented
   sub-item it read as a weird floating glow). Kill the box-shadow explicitly. */
.ui-modern #layout-menu .menu-item.active .menu-sub .menu-item.active > .menu-link { color: #ffffff !important; background: var(--m-sidebar-hover) !important; box-shadow: none !important; }
.ui-modern #layout-menu .menu-sub .menu-item.active > .menu-link::before { display: inline-block !important; background: var(--m-sidebar-active) !important; border-color: var(--m-sidebar-active) !important; }
.ui-modern #layout-menu .menu-sub .menu-link:hover { color: #fff !important; }
.ui-modern #layout-menu .menu-item.open:not(.active) > .menu-toggle { background: var(--m-sidebar-hover) !important; color: #fff !important; }

/* ---- 4. Navbar ---- */
.ui-modern .layout-navbar .navbar-detached,
.ui-modern .navbar-detached {
  background: var(--m-surface) !important;
  border: 1px solid var(--m-border);
  border-radius: var(--m-radius);
  box-shadow: var(--m-shadow-sm);
  backdrop-filter: saturate(140%) blur(6px);
}
.ui-modern .layout-navbar { box-shadow: none !important; }
.ui-modern .navbar .nav-item .nav-link,
.ui-modern .navbar .search-toggler { color: var(--m-text-muted) !important; }
.ui-modern .navbar .nav-item .nav-link:hover { color: var(--m-primary) !important; }
.ui-modern #gsInput,
.ui-modern .gs-input {
  background: var(--m-surface-2) !important;
  border: 1px solid var(--m-border) !important;
  border-radius: 999px !important;
  height: 40px;
  padding-left: 0.95rem !important;
  color: var(--m-text);
}
.ui-modern #gsInput:focus,
.ui-modern .gs-input:focus { border-color: var(--m-primary) !important; box-shadow: 0 0 0 3px var(--m-ring) !important; background: var(--m-surface) !important; }
.ui-modern .global-search-results {
  background: var(--m-surface) !important;
  border-color: var(--m-border) !important;
  border-radius: var(--m-radius) !important;
  box-shadow: var(--m-shadow-lg) !important;
  margin-top: 0.4rem;
}
.ui-modern .gsr-item { color: var(--m-text) !important; }
.ui-modern .gsr-item:hover { background: var(--m-surface-2) !important; color: var(--m-primary) !important; }
.ui-modern .avatar-online::after { box-shadow: 0 0 0 2px var(--m-surface); }

/* ---- 5. Cards ---- */
.ui-modern .card {
  background: var(--m-surface) !important;
  border: 1px solid var(--m-border) !important;
  border-radius: var(--m-radius) !important;
  box-shadow: var(--m-shadow-sm) !important;
  transition: box-shadow .2s ease, transform .2s ease;
}
.ui-modern .card .card-header {
  background: transparent !important;
  border-bottom: 1px solid var(--m-border);
  font-weight: 600;
  color: var(--m-heading);
  padding: 1.1rem 1.25rem;
}
/* A card-body directly under a card-header was rendering with padding-top:0 (its bottom padding stayed
   1.5rem — asymmetric), so the first form field sat right ON the header's border line. Restore the top
   padding so content clears the header separator. */
.ui-modern .card .card-header + .card-body { padding-top: 1.5rem !important; }
.ui-modern .card .card-title { color: var(--m-heading); font-weight: 700; }
.ui-modern a.card:hover,
.ui-modern .card.cursor-pointer:hover,
.ui-modern .card-hover:hover { box-shadow: var(--m-shadow-md) !important; transform: translateY(-2px); }
.ui-modern .avatar .avatar-initial { border-radius: var(--m-radius-sm); font-weight: 700; }

/* ---- 6. Tables (incl. DataTables) ---- */
.ui-modern .table { color: var(--m-text); }
.ui-modern .table > :not(caption) > * > * { background: transparent; }
.ui-modern .table thead th,
.ui-modern table.dataTable thead th,
.ui-modern table.dataTable thead td {
  text-transform: uppercase;
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  font-weight: 700;
  color: var(--m-text-muted) !important;
  border-bottom: 1px solid var(--m-border-strong) !important;
  background: var(--m-surface-2) !important;
  padding-top: 0.85rem;
  padding-bottom: 0.85rem;
}
.ui-modern .table tbody td { border-bottom: 1px solid var(--m-border); padding-top: 0.8rem; padding-bottom: 0.8rem; vertical-align: middle; }
.ui-modern .table-hover tbody tr:hover > *,
.ui-modern table.dataTable.table-hover tbody tr:hover > * { background: rgba(var(--m-primary-rgb), 0.05) !important; }
.ui-modern table.dataTable td { color: var(--m-text); }
.ui-modern .dataTables_wrapper .form-control,
.ui-modern .dataTables_wrapper .form-select { border-radius: 999px; }
.ui-modern .dataTables_wrapper .pagination .page-link { border-radius: var(--m-radius-sm); margin: 0 2px; border: 1px solid var(--m-border); color: var(--m-text-muted); }
.ui-modern .dataTables_wrapper .pagination .page-item.active .page-link { background: var(--m-primary); border-color: var(--m-primary); color: #fff; }

/* ---- 7. Buttons ---- */
.ui-modern .btn { border-radius: var(--m-radius-sm); font-weight: 600; letter-spacing: 0.01em; transition: transform .12s ease, box-shadow .15s ease, background-color .15s ease, border-color .15s ease; }
.ui-modern .btn:active { transform: translateY(0); }
.ui-modern .btn:not(.btn-link):hover { transform: translateY(-1px); }
.ui-modern .btn:focus-visible { box-shadow: 0 0 0 3px var(--m-ring) !important; outline: none; }
.ui-modern .btn-primary { background: var(--m-primary); border-color: var(--m-primary); box-shadow: 0 2px 8px rgba(var(--m-primary-rgb), 0.3); }
.ui-modern .btn-primary:hover,
.ui-modern .btn-primary:focus { background: var(--m-primary-hover); border-color: var(--m-primary-hover); box-shadow: 0 6px 18px rgba(var(--m-primary-rgb), 0.4); }
.ui-modern .btn-outline-primary { color: var(--m-primary); border-color: var(--m-primary); }
.ui-modern .btn-outline-primary:hover { background: var(--m-primary); border-color: var(--m-primary); }

/* ---- 8. Forms / inputs / select2 ---- */

/* ── .form-section — a captioned hairline that groups fields inside a long form ────────────────────
   A small uppercase caption with a rule running to the right of it. Use it to break a big modal into
   named groups so there is something to orient by:

     <div class="col-12"><div class="form-section is-first">Unit</div></div>

   `.is-first` drops the top margin on the leading one. Put it in a `col-12` so it spans a `row`.

   WHEN TO USE IT — both tests must hold, or it makes the form worse rather than better:
     1. roughly SIX or more controls (below that there is nothing to get lost in), AND
     2. the fields fall into genuinely different KINDS of information, each nameable in one domain
        word. If a group cannot be named crisply the grouping is invented, and an invented grouping
        disorients more than a plain list.
   Send-to-Warehouse's New Box is the counter-example: nine controls, but they are all "the box"
   (RMA#, Case, dimensions, weight, remarks), so it is deliberately NOT sectioned — it is already
   segmented by a separate "Units in this box" card, which is better.

   ⚠ A heading whose fields are all conditionally rendered must sit INSIDE that condition, or the
   caption renders over nothing. On the Stock modals, Location + Set are SG-warehouse-only
   (`$sgStockFields`), so Add's "Storage" heading lives inside that gate; Edit's sits outside it
   because "Packed With" is ungated and always keeps the group non-empty.

   Introduced page-local on `stock.php` (2026-08-07) and promoted here immediately: a convention that
   lives in one page's <style> gets copy-pasted and drifts — which is how Add New Stock ended up
   `modal-lg` while Edit Stock was already `modal-xl` on the same page, unnoticed. */
.form-section {
  display: flex;
  align-items: center;
  gap: .625rem;
  margin-top: .45rem;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .5px;
  text-transform: uppercase;
  color: var(--bs-secondary-color);
}
.form-section.is-first { margin-top: 0; }
.form-section::after {
  content: "";
  flex: 1 1 auto;
  border-top: 1px solid var(--bs-border-color);
}

/* A `?` beside a `.form-section` caption, carrying the group's explanation in a tooltip.

   Why a tooltip and not a paragraph: guidance long enough to need three lines becomes the loudest
   element in a modal while being useful only on someone's first visit. The caption plus a ? reads
   as one line; the prose is one hover away.

   ⚠ It must set its OWN font-size. `.form-section` is .75rem and an icon inherits that, which is
   noticeably smaller than the glyph's optical weight next to uppercase letterspaced text -- and
   `.ti-xs` is not the fix here, because the vendor sheet declares that size `!important` and it
   would then be unadjustable from this file. */
.form-section .pt-hint {
  font-size: 1rem;
  line-height: 1;
  cursor: help;
  color: var(--bs-secondary-color);
  opacity: .65;
  transition: opacity .15s ease-in-out;
}
.form-section .pt-hint:hover,
.form-section .pt-hint:focus-visible {
  opacity: 1;
  color: var(--sem-primary-text, var(--m-primary));
}

/* Bootstrap caps a tooltip at `--bs-tooltip-max-width: 200px`, which turns a couple of sentences
   into a tall narrow column that is harder to read than the paragraph it replaced. Opt this one
   into a wider box via data-bs-custom-class, and left-align it -- centred text only reads well at
   one or two lines. */
/* An access link whose portal is switched OFF. The link is still shown -- staff legitimately
   pre-stage one before enabling -- but it must not READ as live, because sending it would give
   the dealer a 404. Struck through rather than hidden: hiding it looks like "no link exists",
   which is a different state with a different fix. */
.pt-link-dead { text-decoration: line-through; opacity: .6; }

.tooltip.pt-hint-tip { --bs-tooltip-max-width: 310px; }
.tooltip.pt-hint-tip .tooltip-inner { text-align: left; }

/* ── Forced drop-UP for a select2 panel — set by common.js, never by hand ──────────────────────────
   Applied on `select2:open` when opening downward would overflow the field's SCROLL CONTAINER (a
   height-capped `.modal-body`). select2's own above/below decision measures the WINDOW, so a field low
   in a capped modal opens downward, and the absolutely-positioned panel then EXTENDS the modal's
   scrollable area — a scrollbar appears on open, the panel covers the Save buttons, and scrolling it is
   useless because that moves the field it is anchored to. See the handler in common.js for the full why.

   ⚠ BOTH RULES ARE REQUIRED and either alone is silently useless. The class lands on the positioned
   `span.select2-container`, which has ZERO HEIGHT — the visible panel is `.select2-dropdown`, itself
   absolutely positioned inside it. Pin only the container and the panel still renders DOWNWARD from the
   field's top edge, straight over the field; the panel therefore needs `bottom: 0` against the container
   so it grows upward at whatever height its content is.
   ⚠ `!important` on all four declarations: select2 writes `top`/`left` inline and re-runs
   `_positionDropdown()` on scroll and resize, and only a stylesheet `!important` outranks inline.
   ⚠ `bottom: 100%` resolves against the select's own `.position-relative` wrapper (the `dropdownParent`
   set in common.js), whose height is the field's — so the panel's bottom edge lands on the field's top
   edge. Change `dropdownParent` and this needs rethinking. */
.select2-container.select2-drop-up-panel {
  top: auto !important;
  bottom: calc(100% + 2px) !important;
}
.select2-container.select2-drop-up-panel > .select2-dropdown {
  top: auto !important;
  bottom: 0 !important;
}

.ui-modern .form-control,
.ui-modern .form-select,
.ui-modern .select2-container--default .select2-selection--single,
.ui-modern .input-group-text {
  border-radius: var(--m-radius-sm) !important;
  border-color: var(--m-border-strong);
  background-color: var(--m-surface);
  color: var(--m-text);
}
.ui-modern .form-control:focus,
.ui-modern .form-select:focus { border-color: var(--m-primary); box-shadow: 0 0 0 3px var(--m-ring); }
.ui-modern.dark-style .form-control,
.ui-modern.dark-style .form-select { background-color: var(--m-surface-2); }
.ui-modern .form-control::placeholder { color: var(--m-text-muted); }
/* floating-label inputs need a transparent placeholder (the label doubles as it) */
.ui-modern .form-floating > .form-control::placeholder { color: transparent; }
.ui-modern .form-label,
.ui-modern .col-form-label { color: var(--m-text); font-weight: 600; }
.ui-modern .select2-container--default.select2-container--focus .select2-selection--single,
.ui-modern .select2-container--default.select2-container--open .select2-selection--single { border-color: var(--m-primary) !important; box-shadow: 0 0 0 3px var(--m-ring); }
.ui-modern .select2-dropdown { border-color: var(--m-border-strong); border-radius: var(--m-radius-sm); box-shadow: var(--m-shadow-md); }
/* Input-group seam: the radius above re-rounds ALL corners of select2 selections / form-controls
   (and buttons via `.ui-modern .btn`), which breaks the square INNER corners an input-group needs —
   so a field and its trailing "New" button join with mismatched sharp-vs-round edges (edit-stock
   modal). Re-square the inner corners: the field's RIGHT side, the trailing button's LEFT side. */
.ui-modern .input-group > .select2-container:not(:last-child) .select2-selection--single,
.ui-modern .input-group > .form-control:not(:last-child),
.ui-modern .input-group > .form-select:not(:last-child) {
  border-top-right-radius: 0 !important;
  border-bottom-right-radius: 0 !important;
}
.ui-modern .input-group > .btn:last-child,
.ui-modern .input-group > .input-group-text:last-child {
  border-top-left-radius: 0 !important;
  border-bottom-left-radius: 0 !important;
}
/* LEADING-addon case (the trailing rules above only handle a trailing button/addon): e.g. the
   loan-edit "Loan #" field = a `#` `.input-group-text` BEFORE the value `.form-control`. Square the
   addon's RIGHT corners and the following field's LEFT corners so the badge and the box meet flush
   instead of each keeping the modern full radius. */
.ui-modern .input-group > .input-group-text:not(:last-child) {
  border-top-right-radius: 0 !important;
  border-bottom-right-radius: 0 !important;
}
.ui-modern .input-group > .form-control:not(:first-child),
.ui-modern .input-group > .form-select:not(:first-child) {
  border-top-left-radius: 0 !important;
  border-bottom-left-radius: 0 !important;
}
/* select2 inside an input-group (e.g. the Add/Link-Participant modal's contact picker + "New
   Participant" button) must STRETCH to meet the trailing button. KEY GOTCHA: common.js wraps the
   <select> in a `<div class="position-relative">` BEFORE calling .select2(), so the input-group's
   flex child is that WRAPPER — not the `.select2-container`/`.form-select` — which is why the
   direct-child seam rules (and an earlier `.select2-container` width fix) never reached it and the
   field sat narrow with a gap before the button. Target the wrapper: make it the growing flex item,
   the select2 fill it, and square the wrapper's inner (right) corners so the edges sit flush. Theme-
   level (not skin-scoped) so it holds under modern + glass; the button's left corner is squared by
   the `.ui-modern .input-group > .btn:last-child` rule above. */
.input-group > .position-relative {
  flex: 1 1 auto;
  width: 1%;
  min-width: 0;
}
.input-group > .position-relative > .select2-container { width: 100% !important; }
.input-group > .position-relative:not(:last-child) .select2-selection--single {
  border-top-right-radius: 0 !important;
  border-bottom-right-radius: 0 !important;
}
/* Fallback for a select2 that is a DIRECT input-group child (not wrapped). */
.input-group > .select2-container { flex: 1 1 auto; width: auto !important; }
.ui-modern .form-check-input:checked { background-color: var(--m-primary); border-color: var(--m-primary); }
.ui-modern .form-check-input:focus { box-shadow: 0 0 0 3px var(--m-ring); }

/* ---- 9. Badges ---- */
.ui-modern .badge { border-radius: 0.4rem; font-weight: 600; letter-spacing: 0.01em; }
.ui-modern .badge.rounded-pill { border-radius: 999px; }

/* ============================================================================
   SEMANTIC STATUS COLORS (2026-07-06) — corporate retune of the Sneat defaults
   (success #28c76f / danger #ea5455 / warning #ff9f43 / info #00cfe8 read neon
   against the Crestron palette). Redefining --bs-*(-rgb) here fixes the .bg-* /
   .text-* utilities automatically (Bootstrap computes them from the rgb vars);
   .btn-* / *-label-* / .alert-* are compiled with literal hexes in the vendor
   CSS, so they're overridden explicitly below. Vendor-internal uses (validation
   icons etc.) keep the stock hues — same family, accepted. Status MEANINGS are
   unchanged (stock/loan/training JS maps reference classes, not hex).
   Dark-label text (--sem-*-text dark values) is brightened for AA contrast on
   the dark tinted fills — supersedes the old "DARK-MODE label contrast" block.
   ============================================================================ */
.ui-modern.light-style {
  --bs-success: #2e9e5b;  --bs-success-rgb: 46, 158, 91;
  --bs-danger:  #d64545;  --bs-danger-rgb:  214, 69, 69;
  --bs-warning: #e08e00;  --bs-warning-rgb: 224, 142, 0;
  --bs-info:    #0a9cc0;  --bs-info-rgb:    10, 156, 192;
  --sem-primary-text: #004a80;
  --sem-success-text: #27884e;
  --sem-danger-text:  #c13c3c;
  --sem-warning-text: #a96b00;
  --sem-info-text:    #0989a9;
}
.ui-modern.dark-style {
  --bs-success: #37b06a;  --bs-success-rgb: 55, 176, 106;
  --bs-danger:  #e05858;  --bs-danger-rgb:  224, 88, 88;
  --bs-warning: #efa11a;  --bs-warning-rgb: 239, 161, 26;
  --bs-info:    #17b1d6;  --bs-info-rgb:    23, 177, 214;
  --sem-primary-text: #6db9ec;
  --sem-success-text: #62d392;
  --sem-danger-text:  #ff8a8a;
  --sem-warning-text: #ffb95e;
  --sem-info-text:    #4cc9de;
}
/* Solid semantic buttons (vendor hexes are literal — override; hover = darken) */
.ui-modern .btn-success { background-color: var(--bs-success) !important; border-color: var(--bs-success) !important; }
.ui-modern .btn-danger  { background-color: var(--bs-danger)  !important; border-color: var(--bs-danger)  !important; }
.ui-modern .btn-warning { background-color: var(--bs-warning) !important; border-color: var(--bs-warning) !important; }
.ui-modern .btn-info    { background-color: var(--bs-info)    !important; border-color: var(--bs-info)    !important; }
.ui-modern .btn-success:hover, .ui-modern .btn-success:focus, .ui-modern .btn-success:active,
.ui-modern .btn-danger:hover,  .ui-modern .btn-danger:focus,  .ui-modern .btn-danger:active,
.ui-modern .btn-warning:hover, .ui-modern .btn-warning:focus, .ui-modern .btn-warning:active,
.ui-modern .btn-info:hover,    .ui-modern .btn-info:focus,    .ui-modern .btn-info:active { filter: brightness(0.92); }
.ui-modern .btn-outline-info { color: var(--bs-info) !important; border-color: var(--bs-info) !important; }
.ui-modern .btn-outline-info:hover { color: #fff !important; background-color: var(--bs-info) !important; }
/* Tinted "label" badges + buttons: fill from the per-mode rgb, text from --sem-*-text */
.ui-modern .badge.bg-label-primary, .ui-modern .btn-label-primary { background-color: rgba(var(--m-primary-rgb), 0.16) !important; color: var(--sem-primary-text) !important; }
.ui-modern .badge.bg-label-success, .ui-modern .btn-label-success { background-color: rgba(var(--bs-success-rgb), 0.16) !important; color: var(--sem-success-text) !important; }
.ui-modern .badge.bg-label-danger,  .ui-modern .btn-label-danger  { background-color: rgba(var(--bs-danger-rgb), 0.16)  !important; color: var(--sem-danger-text)  !important; }
.ui-modern .badge.bg-label-warning, .ui-modern .btn-label-warning { background-color: rgba(var(--bs-warning-rgb), 0.16) !important; color: var(--sem-warning-text) !important; }
.ui-modern .badge.bg-label-info,    .ui-modern .btn-label-info    { background-color: rgba(var(--bs-info-rgb), 0.16)    !important; color: var(--sem-info-text)    !important; }
/* label-button hover = solid fill + white text (the !important fills above would
   otherwise pin the tint and kill the vendor hover feedback) */
.ui-modern .btn-label-primary:hover { background-color: var(--m-primary)  !important; color: #fff !important; }
.ui-modern .btn-label-success:hover { background-color: var(--bs-success) !important; color: #fff !important; }
.ui-modern .btn-label-danger:hover  { background-color: var(--bs-danger)  !important; color: #fff !important; }
.ui-modern .btn-label-warning:hover { background-color: var(--bs-warning) !important; color: #fff !important; }
.ui-modern .btn-label-info:hover    { background-color: var(--bs-info)    !important; color: #fff !important; }
/* Neutral label variants — keep vendor fills, just fix dark text contrast */
.ui-modern.dark-style .btn-label-secondary, .ui-modern.dark-style .badge.bg-label-secondary { color: #b9c0cc !important; }
.ui-modern.dark-style .btn-label-dark,      .ui-modern.dark-style .badge.bg-label-dark      { color: #c5cad6 !important; }
/* Alerts (compiled with literal tints in vendor CSS) */
.ui-modern .alert-success { background-color: rgba(var(--bs-success-rgb), 0.16) !important; border-color: rgba(var(--bs-success-rgb), 0.3) !important; color: var(--sem-success-text) !important; }
.ui-modern .alert-danger  { background-color: rgba(var(--bs-danger-rgb), 0.16)  !important; border-color: rgba(var(--bs-danger-rgb), 0.3)  !important; color: var(--sem-danger-text)  !important; }
.ui-modern .alert-warning { background-color: rgba(var(--bs-warning-rgb), 0.16) !important; border-color: rgba(var(--bs-warning-rgb), 0.3) !important; color: var(--sem-warning-text) !important; }
.ui-modern .alert-info    { background-color: rgba(var(--bs-info-rgb), 0.16)    !important; border-color: rgba(var(--bs-info-rgb), 0.3)    !important; color: var(--sem-info-text)    !important; }

/* ---- 10. Dropdowns / modals / alerts (incl. SweetAlert) ---- */
.ui-modern .dropdown-menu { background: var(--m-surface) !important; border: 1px solid var(--m-border); border-radius: var(--m-radius); box-shadow: var(--m-shadow-lg); padding: 0.4rem; }
.ui-modern .dropdown-item { border-radius: var(--m-radius-sm); color: var(--m-text); padding: 0.5rem 0.75rem; }
.ui-modern .dropdown-item:hover { background: var(--m-surface-2); color: var(--m-primary); }
.ui-modern .dropdown-divider { border-color: var(--m-border); }
.ui-modern .modal-content { background: var(--m-surface); border: 1px solid var(--m-border); border-radius: calc(var(--m-radius) + 0.25rem); box-shadow: var(--m-shadow-lg); }
.ui-modern .modal-header,
.ui-modern .modal-footer { border-color: var(--m-border); }
.ui-modern .modal-backdrop.show { -webkit-backdrop-filter: blur(6px) saturate(120%); backdrop-filter: blur(6px) saturate(120%); opacity: 0.55; }
.ui-modern .swal2-popup { border-radius: var(--m-radius) !important; background: var(--m-surface) !important; box-shadow: var(--m-shadow-lg) !important; }
.ui-modern .swal2-popup .swal2-title,
.ui-modern .swal2-popup .swal2-html-container { color: var(--m-text) !important; }
.ui-modern .swal2-styled.swal2-confirm { border-radius: var(--m-radius-sm) !important; background: var(--m-primary) !important; }
.ui-modern .alert { border-radius: var(--m-radius-sm); }
.ui-modern hr,
.ui-modern .border,
.ui-modern .border-top,
.ui-modern .border-bottom { border-color: var(--m-border) !important; }

/* ---- 11. Busy overlay (inline-styled in main.php) ---- */
.ui-modern #appBusyOverlay { background: rgba(10, 13, 16, 0.62) !important; backdrop-filter: blur(4px) saturate(120%); }
.ui-modern #appBusyMsg { font-family: 'Plus Jakarta Sans', system-ui, sans-serif; font-weight: 600 !important; letter-spacing: 0.01em; }

/* ---- 11b. Busy overlay in LIGHT mode: brand blue, not white (2026-08-28) ----
   The light-mode scrim is only a 45% dark wash over a white page, so it lands around mid-grey —
   a white mark and a white label sit at very low contrast on it (seen on the live dashboard).
   Use the same blue the login page's brand mark uses.
   ⚠ main.php sets color:#fff INLINE on both, as the dark-mode default. An inline style always
   beats a stylesheet selector, so these two rules MUST keep !important — without it they are
   silently inert and the overlay stays white. */
.ui-modern.light-style #appBusyOverlay .cs-swirl { color: #2e5786 !important; }
/* Same blue as the mark, deliberately — they read as one unit. A darker navy was tried for
   contrast and was unnecessary: #2e5786 on the light veil measures 6.80:1, well past the
   4.5:1 needed for normal text, let alone this large bold heading. */
.ui-modern.light-style #appBusyMsg               { color: #2e5786 !important; }

/* ...and the scrim itself must flip with it. The shared scrim is a 45% DARK wash, which over a
   light page composites to mid-grey rgb(137,140,146) — measured on the live dashboard — the worst
   possible backdrop, scoring only 2.21:1 against the blue mark and 3.91:1 against the label.
   (In dark mode the identical wash goes near-black, which is why white reads well there.)
   A light veil with dark content is the correct light-mode loading state. Beats the existing
   `.ui-glass #appBusyOverlay` rule on specificity, and both carry !important. */
.ui-modern.light-style #appBusyOverlay,
.ui-glass.light-style  #appBusyOverlay {
  background: rgba(244, 246, 248, 0.82) !important;
  -webkit-backdrop-filter: blur(8px) saturate(115%);
          backdrop-filter: blur(8px) saturate(115%);
}

/* ---- 11a. Brand busy indicator — the Crestron swirl (replaced every spinner 2026-08-28) ----
   ONE definition of the mark lives in includes/brand_swirl.php; JS dialogs clone the node the
   busy overlay already renders (busySwirlHtml() in common.js) rather than carrying the path twice.
   fill is currentColor, so the mark takes its colour from whatever it sits in — the brand SVGs in
   side_menu.php / login.php hardcode #2e5786, which goes low-contrast on the dark overlay.
   ⚠ The overlay in main.php does NOT depend on these rules: it drives the animation from an inline
   style + inline keyframes so it still spins if this stylesheet fails to load. These classes are
   for every OTHER call site. */
/* Default size keeps the mark's own ~1.85 aspect, so it FILLS its box. A square box
   letterboxes it to 43% of the height — small and faint, which is how it first shipped. */
.cs-swirl { display: block; width: 88px; height: 48px; color: inherit; }

/* ⚠⚠ DO NOT `transform: rotate()` THIS MARK. The swirl is an OVAL — measured via getBBox() in a
   real browser, its ink is 41.78 x 21.66, an aspect of 1.93 — so a 360° rotation reads as an
   object TUMBLING end over end, not as a spinner. It was built that way first and it looked wrong.
   Instead the mark stays perfectly still and a highlight sweeps CLOCKWISE around it, via a conic
   -gradient mask whose start angle is animated. Partial alpha in the gradient = partial opacity of
   the mark, which is what makes it read as a sweep rather than a wipe. */
@property --cs-sweep { syntax: "<angle>"; initial-value: 0deg; inherits: false; }
@keyframes csSwirlSweep { to { --cs-sweep: 360deg; } }
@keyframes csSwirlPulse { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }

.cs-swirl--busy {
  -webkit-mask-image: conic-gradient(from var(--cs-sweep) at 50% 50%, rgba(0,0,0,.68) 0deg, rgba(0,0,0,1) 80deg, rgba(0,0,0,.68) 190deg, rgba(0,0,0,.68) 360deg);
          mask-image: conic-gradient(from var(--cs-sweep) at 50% 50%, rgba(0,0,0,.68) 0deg, rgba(0,0,0,1) 80deg, rgba(0,0,0,.68) 190deg, rgba(0,0,0,.68) 360deg);
  animation: csSwirlSweep 1.1s linear infinite;
}

/* Degrades safely: without @property, `var(--cs-sweep)` is invalid at computed-value time, the
   conic-gradient is dropped, and with it the whole mask — leaving the mark fully opaque and
   static. Visible and correct, just not animated. Never invisible. */

/* ⚠ A continuous animation is a vestibular trigger. Drop the mask entirely and substitute an
   opacity-only cue — the busy state must still be CONVEYED, not merely stop moving. */
@media (prefers-reduced-motion: reduce) {
  .cs-swirl--busy {
    -webkit-mask-image: none; mask-image: none;
    animation: csSwirlPulse 1.6s ease-in-out infinite;
  }
}

/* ---- 12. Login page (page-login, always light-style) ---- */
.ui-modern.page-login,
.ui-modern.page-login body { background: radial-gradient(1200px 600px at 50% -10%, #2a4a63 0%, #111c26 60%) fixed !important; min-height: 100vh; }
.ui-modern.page-login .authentication-wrapper.authentication-basic { min-height: 100vh; }
.ui-modern.page-login .authentication-inner { width: 100%; max-width: 420px; }
.ui-modern.page-login .card { background: var(--m-surface, #ffffff) !important; border: 1px solid var(--m-border) !important; border-radius: 1.1rem !important; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45) !important; }
.ui-modern.page-login .card-body { padding: 2.25rem 2rem; }
.ui-modern.page-login .app-brand svg { max-width: 100%; height: auto; }
.ui-modern.page-login .divider-text { color: #9aa1a8; font-size: 0.72rem; letter-spacing: 0.06em; }
.ui-modern.page-login .btn-primary { background: var(--m-primary); border-color: var(--m-primary); padding-block: 0.7rem; font-size: 1rem; }

/* ---- 13. Accessibility — respect reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
  .ui-modern *,
  .ui-modern *::before,
  .ui-modern *::after { transition-duration: 0.001ms !important; animation-duration: 0.001ms !important; }
}

/* ============================================================================
   APPLE GLASSMORPHISM (merged as DEFAULT 2026-06-22; formerly the opt-in
   glass/ skin). main.php/login.php hard-add the .ui-glass class, so every rule
   below always applies, layered on top of the modern look. Translucent
   backdrop-filter panels + hairline borders over a vibrant ambient body gradient.
   Extend under .ui-glass[.light-style|.dark-style] using the --g-* tokens.
   ============================================================================ */
/* ---- tokens ---- */
.ui-glass.light-style {
  --g-blur: 22px;
  --g-radius: 20px;
  --g-panel:        rgba(255, 255, 255, 0.55);
  --g-panel-2:      rgba(255, 255, 255, 0.42);   /* slightly clearer (nested) */
  --g-border:       rgba(255, 255, 255, 0.65);
  --g-border-soft:  rgba(255, 255, 255, 0.35);
  --g-shadow:       0 10px 34px rgba(39, 69, 92, 0.14);
  --g-shadow-lg:    0 20px 60px rgba(39, 69, 92, 0.22);
  --g-text:         #23272b;
  --g-text-muted:   #5b6167;
  --g-sidebar:      rgba(39, 69, 92, 0.60);   /* frosted Crestron slate-blue */
  --g-sidebar-text: #e9eef3;
  --g-sidebar-mut:  #b3c1cd;
}
.ui-glass.dark-style {
  --g-blur: 26px;
  --g-radius: 20px;
  --g-panel:        rgba(255, 255, 255, 0.09);
  --g-panel-2:      rgba(255, 255, 255, 0.06);
  --g-border:       rgba(255, 255, 255, 0.18);
  --g-border-soft:  rgba(255, 255, 255, 0.10);
  --g-shadow:       0 10px 34px rgba(0, 0, 0, 0.45);
  --g-shadow-lg:    0 22px 60px rgba(0, 0, 0, 0.6);
  --g-text:         #e8edf2;
  --g-text-muted:   #a8b2bb;
  --g-sidebar:      rgba(16, 20, 24, 0.55);   /* frosted charcoal */
  --g-sidebar-text: #e9eef3;
  --g-sidebar-mut:  #9fadb8;
}

/* ---- 1. Ambient backdrop (the vibrant surface the glass blurs) ---- */
/* All-blue Crestron glows (retuned 2026-07-06; formerly blue/purple/teal). */
.ui-glass.light-style body {
  background:
    radial-gradient(900px 520px at 12% -8%, rgba(0, 114, 206, 0.28), transparent 60%),
    radial-gradient(820px 520px at 92% 4%, rgba(39, 69, 92, 0.20), transparent 60%),
    radial-gradient(900px 600px at 70% 108%, rgba(14, 165, 233, 0.18), transparent 60%),
    linear-gradient(160deg, #eef3fc 0%, #e7ecf7 100%) !important;
  background-attachment: fixed !important;
}
.ui-glass.dark-style body {
  background:
    radial-gradient(820px 480px at 8% -6%, rgba(0, 114, 206, 0.35), transparent 58%),
    radial-gradient(760px 480px at 96% 0%, rgba(77, 163, 221, 0.24), transparent 58%),
    radial-gradient(900px 600px at 74% 112%, rgba(30, 120, 180, 0.26), transparent 58%),
    linear-gradient(160deg, #10161d 0%, #0b0f14 100%) !important;
  background-attachment: fixed !important;
}

/* ---- 2. Sidebar — frosted glass ---- */
.ui-glass #layout-menu.layout-menu,
.ui-glass .bg-menu-theme {
  background: var(--g-sidebar) !important;
  -webkit-backdrop-filter: blur(var(--g-blur)) saturate(160%);
  backdrop-filter: blur(var(--g-blur)) saturate(160%);
  border-right: 1px solid var(--g-border-soft) !important;
  box-shadow: var(--g-shadow);
}
.ui-glass #layout-menu .menu-link { color: var(--g-sidebar-text) !important; }
.ui-glass #layout-menu .menu-link .menu-icon,
.ui-glass #layout-menu .menu-sub .menu-link { color: var(--g-sidebar-mut) !important; }
.ui-glass #layout-menu .app-brand { border-bottom-color: var(--g-border-soft) !important; }
/* active item — luminous glass pill (Crestron blue; was blue→indigo) */
.ui-glass #layout-menu .menu-item.active > .menu-link {
  background: linear-gradient(135deg, rgba(0,114,206,0.92), rgba(0,74,128,0.88)) !important;
  box-shadow: 0 6px 20px rgba(0, 114, 206, 0.4) !important;
  color: #fff !important;
}
.ui-glass.dark-style #layout-menu .menu-item.active > .menu-link {
  background: linear-gradient(135deg, rgba(77,163,221,0.92), rgba(47,111,168,0.88)) !important;
  box-shadow: 0 6px 20px rgba(77, 163, 221, 0.35) !important;
}
/* But an active SUB-item (menu-sub) must NOT get that luminous gradient pill + glow — on an
   indented sub-row it reads as a weird floating glow. Give it a subtle highlight instead (the
   filled brand dot from the modern ::before still marks it active). */
.ui-glass #layout-menu .menu-item.active .menu-sub .menu-item.active > .menu-link {
  background: rgba(255, 255, 255, 0.08) !important;
  box-shadow: none !important;
  color: #fff !important;
}

/* ---- 3. Navbar — frosted ---- */
.ui-glass .layout-navbar .navbar-detached,
.ui-glass .navbar-detached {
  background: var(--g-panel) !important;
  -webkit-backdrop-filter: blur(var(--g-blur)) saturate(180%);
  backdrop-filter: blur(var(--g-blur)) saturate(180%);
  border: 1px solid var(--g-border) !important;
  border-radius: var(--g-radius) !important;
  box-shadow: var(--g-shadow) !important;
}
.ui-glass #gsInput,
.ui-glass .gs-input {
  background: var(--g-panel-2) !important;
  border: 1px solid var(--g-border-soft) !important;
}

/* ---- 4. Cards / panels — the headline frosted-glass surface ---- */
.ui-glass .card {
  background: var(--g-panel) !important;
  -webkit-backdrop-filter: blur(var(--g-blur)) saturate(180%);
  backdrop-filter: blur(var(--g-blur)) saturate(180%);
  border: 1px solid var(--g-border) !important;
  border-radius: var(--g-radius) !important;
  box-shadow: var(--g-shadow) !important;
  color: var(--g-text);
  transition: transform .25s cubic-bezier(0.16,1,0.3,1), box-shadow .25s ease;
}
.ui-glass .card .card-header { border-bottom-color: var(--g-border-soft) !important; color: var(--g-text); }
.ui-glass a.card:hover,
.ui-glass .card.cursor-pointer:hover,
.ui-glass .card-hover:hover {
  transform: translateY(-3px) scale(1.01);
  box-shadow: var(--g-shadow-lg) !important;
}
/* a subtle top "light reflection" edge on cards */
.ui-glass .card::before {
  content: "";
  position: absolute; inset: 0;
  border-radius: inherit;
  padding-top: 1px;
  background: linear-gradient(180deg, rgba(255,255,255,0.5), transparent 28%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  pointer-events: none; opacity: 0.6;
}
.ui-glass .card { position: relative; }
.ui-glass.dark-style .card::before { background: linear-gradient(180deg, rgba(255,255,255,0.18), transparent 26%); }

/* readable text on the glass */
.ui-glass .card, .ui-glass .card p, .ui-glass .card small,
.ui-glass .table { color: var(--g-text); }
.ui-glass .text-muted { color: var(--g-text-muted) !important; }

/* ---- 5. Tables: let the card glass show through ---- */
.ui-glass .table,
.ui-glass table.dataTable { background: transparent !important; }
.ui-glass .table thead th,
.ui-glass table.dataTable thead th {
  background: var(--g-panel-2) !important;
  border-bottom-color: var(--g-border-soft) !important;
  color: var(--g-text-muted) !important;
}
.ui-glass .table tbody td { border-bottom-color: var(--g-border-soft) !important; }
.ui-glass .table-hover tbody tr:hover > *,
.ui-glass table.dataTable.table-hover tbody tr:hover > * {
  background: rgba(0, 114, 206, 0.10) !important;
}

/* ---- 6. Buttons ---- */
.ui-glass .btn { border-radius: 12px; }
.ui-glass .btn-primary {
  background: linear-gradient(135deg, var(--m-accent, #0072ce), var(--m-primary, #004a80)) !important;   /* Crestron blue (was blue→indigo) */
  border: 0 !important;
  box-shadow: 0 6px 18px rgba(0, 114, 206, 0.4) !important;
}
.ui-glass .btn-primary:hover { box-shadow: 0 10px 26px rgba(0, 114, 206, 0.5) !important; transform: translateY(-1px); }
.ui-glass.dark-style .btn-primary {
  background: linear-gradient(135deg, #4da3dd, #2f6fa8) !important;
  box-shadow: 0 6px 18px rgba(77, 163, 221, 0.35) !important;
}
.ui-glass.dark-style .btn-primary:hover { box-shadow: 0 10px 26px rgba(77, 163, 221, 0.45) !important; }

/* ⭐ DISABLED primary buttons must not glow (app-wide, 2026-07-29).
   The four rules above set the glow with `!important`, which beats even the vendor theme's OWN
   `.btn-primary:disabled { box-shadow: none !important }` (theme-default.css:298 — same specificity,
   lost on source order). Net effect before this: EVERY disabled btn-primary in the app kept a full
   Crestron-blue glow and read as clickable; Bootstrap's `opacity:.65` was the only hint it wasn't.
   Found by measuring a rendered page — the CSS looks correct in isolation.
   Specificity must clear the highest glow rule (`.ui-glass.dark-style .btn-primary:hover` = 0,4,0),
   hence the explicit `:disabled:hover` / `.disabled:hover` pairs at 0,5,0. `.disabled` (class) is
   listed alongside `:disabled` (attribute) because Bootstrap uses the class form on `<a>` buttons,
   which cannot carry the attribute. `transform` is reset too so a disabled button never lifts. */
.ui-glass .btn-primary:disabled,
.ui-glass .btn-primary.disabled,
.ui-glass .btn-primary:disabled:hover,
.ui-glass .btn-primary.disabled:hover,
.ui-glass.dark-style .btn-primary:disabled,
.ui-glass.dark-style .btn-primary.disabled,
.ui-glass.dark-style .btn-primary:disabled:hover,
.ui-glass.dark-style .btn-primary.disabled:hover {
  box-shadow: none !important;
  transform: none !important;
}
/* tinted "label" buttons (e.g. the Cancel buttons) — give them the VISIBLE `--m-border-strong` border
   APP-WIDE, not just on opaque surfaces. The faint `--g-border-soft` was invisible on both light glass
   cards (the Class Details form's Cancel) AND the opaque white modal/popup, so a `btn-label-secondary`
   Cancel read as plain borderless text everywhere. One global rule keeps every Cancel/secondary button
   clearly bordered and consistent (cards + modals + swal popups). */
.ui-glass [class*="btn-label-"] {
  background: var(--g-panel-2) !important;
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border: 1px solid var(--m-border-strong) !important;
}

/* ---- 7. Inputs / selects ----
   ALL form fields follow the change-password / opaque-modal field style APP-WIDE (decided 2026-06-23):
   the original glass field used a translucent bg + faint `--g-border-soft` border that, on light glass
   CARDS (e.g. the Class Details form), was barely visible ("can't see the fields"). Route every field
   to the visible modern tokens — solid `--m-surface-2` bg + `--m-border-strong` border + `--m-text` —
   exactly like `.ui-glass .modal-content .form-control`. This makes the `.modal-content`/`.swal2-popup`
   field overrides redundant (kept, harmless) and means NEW fields get a visible border automatically. */
.ui-glass .form-control,
.ui-glass .form-select,
.ui-glass .select2-container--default .select2-selection--single,
.ui-glass .input-group-text {
  background-color: var(--m-surface-2) !important;
  border: 1px solid var(--m-border-strong) !important;
  color: var(--m-text) !important;
  border-radius: 12px !important;
}
.ui-glass .form-control:focus,
.ui-glass .form-select:focus,
.ui-glass .select2-container--default.select2-container--focus .select2-selection--single,
.ui-glass .select2-container--default.select2-container--open .select2-selection--single {
  border-color: var(--m-primary, #004a80) !important;
  box-shadow: 0 0 0 3px var(--m-ring, rgba(0, 74, 128, 0.25)) !important;
}
/* `input-group-merge` ("#" addon + value rendered as ONE merged field, e.g. the loan-edit "Loan #"):
   drop the INNER border between adjacent items so there's no divider line. The global field/addon
   `--m-border-strong` border above draws a seam otherwise. More specific than the base field rule
   (`.ui-glass .input-group-merge .…` = 3 classes) so it wins; the OUTER border is kept. */
.ui-glass .input-group-merge > .input-group-text:not(:last-child),
.ui-glass .input-group-merge > .form-control:not(:last-child) {
  border-right: 0 !important;
}
.ui-glass .input-group-merge > .input-group-text:not(:first-child),
.ui-glass .input-group-merge > .form-control:not(:first-child) {
  border-left: 0 !important;
}

/* Uniform badge height + vertically-centered content so text badges, icon-only badges, and the
   capacity badges all line up (avoids the status / bed-icon / capacity height mismatch). A FIXED height
   (not min-height) is required: an icon like ti-bed has a larger font-size than the badge text, so with
   min-height the icon badge ends up a hair taller. Vertical padding is zeroed (height + centering owns
   the box); horizontal padding is kept. Applies app-wide; badge content is short/single-line. */
.badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 1.5rem;
  padding-top: 0;
  padding-bottom: 0;
  line-height: 1;
  vertical-align: middle;
}
.badge > .ti { font-size: 0.95rem; }

/* ---- 8. Badges ---- */
.ui-glass .badge[class*="bg-label-"] {
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  border: 1px solid var(--g-border-soft);
}
/* The pale bg-label-* tint "flushes" into the page because --g-border-soft is a translucent WHITE edge
   (invisible on the light panel). Give the badge a CONTRASTING edge per theme so it's clearly delineated
   from the background in both light and dark mode. */
.ui-glass.light-style .badge[class*="bg-label-"] { border-color: rgba(35, 39, 43, 0.16); }
.ui-glass.dark-style  .badge[class*="bg-label-"] { border-color: rgba(255, 255, 255, 0.22); }

/* ---- 9. Dropdowns / modals — heavy frosted glass ---- */
.ui-glass .dropdown-menu {
  /* dropdown MENUS carry readable content, so they must be near-opaque — the translucent
     --g-panel let them blend into the page behind. Keep a light frost + the shadow. */
  -webkit-backdrop-filter: blur(12px) saturate(160%);
  backdrop-filter: blur(12px) saturate(160%);
  border: 1px solid var(--g-border) !important;
  border-radius: 16px !important;
  box-shadow: var(--g-shadow-lg) !important;
}
.ui-glass.light-style .dropdown-menu { background: rgba(255, 255, 255, 0.97) !important; }
.ui-glass.dark-style  .dropdown-menu { background: rgba(31, 35, 40, 0.97) !important; }
.ui-glass .dropdown-item { color: var(--g-text); border-radius: 10px; }
.ui-glass .modal-content {
  /* Modals are CONTENT surfaces (edit forms) — they must be OPAQUE, same rule as the
     swal2 popup + dropdown menu below. A translucent panel + backdrop-filter:blur made every
     edit modal look frosted/washed-out ("all edit modals blurry"). Keep the glass border/radius/
     shadow, but a solid background and NO backdrop-filter so the form is crisp and readable. */
  background: var(--m-surface, #ffffff) !important;
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
  border: 1px solid var(--g-border) !important;
  border-radius: 24px !important;
  box-shadow: var(--g-shadow-lg) !important;
  color: var(--g-text);
}
.ui-glass.light-style .modal-content { background: var(--m-surface, #ffffff) !important; }
.ui-glass.dark-style  .modal-content { background: var(--m-surface, #1f2328) !important; }
/* Inputs INSIDE the now-opaque modal: the glass skin gives form fields a translucent-WHITE
   background + border (fine over a frosted panel) — but on the solid modal they're white-on-white
   and the field looks borderless. Force a solid background + a visible border (modern tokens). More
   specific than `.ui-glass .form-control`, so it wins. */
.ui-glass .modal-content .form-control,
.ui-glass .modal-content .form-select,
.ui-glass .modal-content .select2-container--default .select2-selection--single,
.ui-glass .modal-content .input-group-text {
  background-color: var(--m-surface-2) !important;
  border: 1px solid var(--m-border-strong) !important;
  color: var(--m-text) !important;
}
.ui-glass .modal-content .form-control:focus,
.ui-glass .modal-content .form-select:focus,
.ui-glass .modal-content .select2-container--default.select2-container--focus .select2-selection--single,
.ui-glass .modal-content .select2-container--default.select2-container--open .select2-selection--single {
  border-color: var(--m-primary, #004a80) !important;
}
.ui-glass .modal-header, .ui-glass .modal-footer { border-color: var(--g-border-soft) !important; }
/* Frosted blur behind modals (re-enabled 2026-06-23 by request). The earlier "edit modal is blurry"
   bug was NOT this blur per se — it was modals TRAPPED in a card's stacking context so they got blurred
   too; that's fixed by the move-to-body `show.bs.modal` handler (common.js), so the modal now paints
   ABOVE the backdrop and stays crisp while only the page behind is frosted. Keep the dim scrim + blur. */
.ui-glass .modal-backdrop.show { background: rgba(10, 16, 30, 0.45); -webkit-backdrop-filter: blur(6px) saturate(120%); backdrop-filter: blur(6px) saturate(120%); opacity: 1; }

/* SweetAlert backdrop — match the Bootstrap .modal-backdrop frosted scrim above so EVERY dialog
   (Swal alerts + confirms like "Mark as collected?") blurs the page behind it, consistent with the
   Bootstrap modals (e.g. Add New CTI Class). The swal2 popup itself stays fully opaque (below).
   Global (not skin-scoped) so it applies to every page; the body always carries ui-glass anyway. */
.swal2-container.swal2-backdrop-show {
  background: rgba(10, 16, 30, 0.45) !important;
  -webkit-backdrop-filter: blur(6px) saturate(120%);
  backdrop-filter: blur(6px) saturate(120%);
}

.ui-glass .swal2-popup {
  /* Alerts must be FULLY OPAQUE: a translucent popup (a) lets the page bleed through and
     (b) makes the SweetAlert success-icon mask pieces translucent too, so the green ring
     shows through as a "line cutting across the circle". Solid bg = clean icon + readable. */
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
  border: 1px solid var(--g-border) !important;
  border-radius: 22px !important;
}
.ui-glass.light-style .swal2-popup { background: var(--m-surface, #ffffff) !important; }
.ui-glass.dark-style  .swal2-popup { background: var(--m-surface, #1f2328) !important; }

/* SweetAlert auto-dismiss countdown bar (every ShowAlert sets timerProgressBar:true). SweetAlert's
   default rgba(0,0,0,.2) strip is ~invisible on the white/glass popup, so theme it: a clearly-visible
   primary-coloured bar, a touch taller, tucked into the popup's rounded bottom corners.

   ⚠⚠ THE ROUNDING BELONGS TO THE CONTAINER, NOT THE BAR. sweetalert2.css already gives
   `.swal2-timer-progress-bar-container` `overflow:hidden` plus a HARD-CODED
   `border-bottom-*-radius: 5px` -- SweetAlert's OWN default popup radius. We override the popup to
   22px, so the container kept 5px corners that visibly protrude past the popup's curve at the
   bottom-left and bottom-right. That is the "bar doesn't line up with the corner" defect.

   ⚠ Rounding the BAR instead (what this rule used to do) cannot work: the bar is the element whose
   width animates to zero, so its rounded end travels inward as it shrinks rather than staying in the
   corner -- and a 22px radius on a 0.35em-tall strip deforms it into an ellipse.

   ⚠⚠ THE CONTAINER RADIUS MUST TRACK `.swal2-popup`'s. Two skins set it, so both are mirrored
   below. Change one and you must change the other, or the corner gap comes back. */
.swal2-timer-progress-bar {
  height: 0.35em !important;
  background: var(--m-primary, #004a80) !important;   /* brand blue in both modes (was --bs-primary/#696cff) */
  border-radius: 0 !important;                        /* the CONTAINER clips it -- see above */
}
.swal2-timer-progress-bar-container { height: 0.35em; }
/* Mirrors `.ui-modern .swal2-popup { border-radius: var(--m-radius) }` above. */
.ui-modern .swal2-timer-progress-bar-container {
  border-bottom-left-radius: var(--m-radius) !important;
  border-bottom-right-radius: var(--m-radius) !important;
}
/* Mirrors `.ui-glass .swal2-popup { border-radius: 22px }` above. Glass is the default skin and
   both classes sit on <html>, so this one is what actually renders. */
.ui-glass .swal2-timer-progress-bar-container {
  border-bottom-left-radius: 22px !important;
  border-bottom-right-radius: 22px !important;
}
/* Bootstrap form fields injected into a custom-`html` SweetAlert (e.g. the Reservation reserve
   dialog's seats + Remarks inputs) need the SAME visible-border treatment as MODAL fields: the
   swal2 popup is opaque (above), but the glass skin's `.form-control` uses a translucent-white bg +
   border that is invisible white-on-white on the solid popup -> the field looks "flushed out"
   (borderless). Mirror the `.ui-glass .modal-content .form-control` fix for `.swal2-popup`. */
.ui-glass .swal2-popup .form-control,
.ui-glass .swal2-popup .form-select,
.ui-glass .swal2-popup .input-group-text {
  background-color: var(--m-surface-2) !important;
  border: 1px solid var(--m-border-strong) !important;
  color: var(--m-text) !important;
}
.ui-glass .swal2-popup .form-control:focus,
.ui-glass .swal2-popup .form-select:focus {
  border-color: var(--m-primary, #004a80) !important;
}
/* belt-and-braces: pin the success-icon mask pieces to the popup colour (they normally track
   it, but make sure so no ring-line shows through) */
.ui-glass.light-style .swal2-success-circular-line-left,
.ui-glass.light-style .swal2-success-circular-line-right,
.ui-glass.light-style .swal2-success-fix { background-color: var(--m-surface, #ffffff) !important; }
.ui-glass.dark-style .swal2-success-circular-line-left,
.ui-glass.dark-style .swal2-success-circular-line-right,
.ui-glass.dark-style .swal2-success-fix { background-color: var(--m-surface, #1f2328) !important; }

/* ---- 10. KPI stat circles get a soft glow ---- */
.ui-glass .avatar.avatar-xl .avatar-initial,
.ui-glass .avatar .avatar-initial { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18); }

/* ---- 11. Busy overlay — frosted ---- */
.ui-glass #appBusyOverlay {
  background: rgba(10, 16, 30, 0.45) !important;
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  backdrop-filter: blur(10px) saturate(140%);
}

/* ---- 12. Login page — single floating glass card on the ambient backdrop ---- */
.ui-glass.page-login .card {
  background: var(--g-panel) !important;
  -webkit-backdrop-filter: blur(30px) saturate(180%);
  backdrop-filter: blur(30px) saturate(180%);
  border: 1px solid var(--g-border) !important;
  border-radius: 26px !important;
  box-shadow: var(--g-shadow-lg) !important;
}

/* ---- 13. Accessibility: drop blur where motion/transparency is unwanted ---- */
@media (prefers-reduced-transparency: reduce) {
  .ui-glass .card,
  .ui-glass .navbar-detached,
  .ui-glass .dropdown-menu,
  .ui-glass .modal-content { -webkit-backdrop-filter: none; backdrop-filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .ui-glass .card { transition: none; }
}

/* ---- 14. Print: always print LIGHT, even from dark mode (2026-07-06). Forces the
   light token values + a white canvas so dark surfaces don't waste toner / print
   as unreadable blocks. Vendor core-dark still styles some chrome, but cards,
   tables and text (token-driven) dominate what actually prints. ---- */
@media print {
  .ui-glass body,
  .ui-modern body { background: #ffffff !important; }
  .ui-modern.dark-style {
    --m-bg: #ffffff; --m-surface: #ffffff; --m-surface-2: #f6f7f8;
    --m-border: #e4e6e8; --m-border-strong: #c8ccd0;
    --m-text: #32363a; --m-text-muted: #6a6d70; --m-heading: #23272b;
    --m-primary: #004a80; --m-primary-rgb: 0, 74, 128;
  }
  .ui-glass.dark-style {
    --g-panel: #ffffff; --g-panel-2: #f6f7f8;
    --g-text: #23272b; --g-text-muted: #5b6167;
    --g-border: #e4e6e8; --g-border-soft: #e4e6e8;
  }
  .ui-modern.dark-style .card,
  .ui-glass.dark-style .card { background: #ffffff !important; color: #32363a !important; box-shadow: none !important; }
  .ui-modern.dark-style .table td,
  .ui-modern.dark-style .table th,
  .ui-modern.dark-style .dataTable td,
  .ui-modern.dark-style .dataTable th { color: #32363a !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   BATCH MODAL — header fields beside a growing, editable child list  (`.bx-*`, 2026-08-11)

   One modal shape, used wherever an operator enters a few CONSTANT fields and then builds a LIST of
   things that share them:
     • Send to Warehouse ▸ New Box     — box identity + the units packed in it
     • Stock ▸ Add New Stock           — one receipt + the units received on it

   ⚠ PROMOTED HERE FROM `pages/send_warehouse.php`, where it was page-scoped as `.wbox-*`. Adding the
   Stock batch modal would have made it the second copy, and this codebase has already paid for that
   pattern once — `.form-section` was independently reinvented FOUR times in four different shapes
   before anyone noticed. Every rule below is the measured, argued version from that page; the
   reasoning is kept verbatim because it is what stops someone "simplifying" it back into a bug.

   ⚠ NOTHING HERE IS SCOPED TO A MODAL ID. The two rules that used `#wboxModal` did so to win a
   specificity contest, not for scoping — see each note. Opt a modal in with `.bx-modal`.

   ── THE HEIGHT CHAIN ────────────────────────────────────────────────────────────────────────────
   Bound the CONTENT to the viewport and let ONLY the list rows absorb the slack, so the header
   fields, the adder and Save never leave the screen.
   ⚠ A `max-height` on the rows alone is NOT enough and cannot be: the modal still grows past the
   screen, `.modal` is itself `overflow-y:auto`, and its scrollbar looks exactly like the page's.
   ⚠ EVERY ancestor in the chain needs `min-height: 0`. A flex item defaults to `min-height: auto`,
   refuses to shrink below its content, and ONE missing link makes the whole cap silently do nothing.
   `.modal-body` keeps `overflow-y: auto` as a LAST RESORT (not `hidden`): on a very short viewport
   the rows bottom out at their floor and the outer scroll returns rather than clipping Save away. */
.bx-modal .modal-content { max-height: calc(100vh - 3.5rem); }
.bx-modal .modal-body { display: flex; flex-direction: column; min-height: 0; overflow-y: auto; }
.bx-form { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }

/* Chrome trimmed to buy the rows room, from MEASUREMENT rather than taste: at a 911px viewport the
   modal sat pinned at its cap with only ~148px (≈2.3 rows) for the list, and padding was the single
   biggest non-content block at 96px (`p-md-5` = 3rem top AND bottom). 1.5rem plus the two margin
   trims returns ~64px, i.e. ~3.3 rows. These modals are deliberately tighter than the app's other
   `p-3 p-md-5` modals — they are the only ones carrying an editable child table. */
.bx-modal .modal-content { padding: 1.5rem !important; }
@media (max-width: 767.98px) { .bx-modal .modal-content { padding: 1rem !important; } }
.bx-modal .modal-body > .text-center:first-child { margin-bottom: 1rem !important; }  /* was mb-4 */
.bx-modal .bx-form > .text-center { margin-top: 1rem !important; }                    /* was mt-4 */
@media (max-width: 575.98px) {
  /* `.modal-dialog`'s margin drops from 1.75rem to 0.5rem below sm. */
  .bx-modal .modal-content { max-height: calc(100vh - 1rem); }
}

/* ── FIELDS BESIDE THE LIST AT lg+ ──────────────────────────────────────────────────────────────
   Extra width does NOT add rows on its own — the count is bound by HEIGHT and the modal is already
   pinned at its cap. What width buys is taking the header fields OUT of the vertical stack, which
   hands that height to the list (measured ~3 rows -> ~8).
   ⚠ Hand-rolled instead of a Bootstrap `.row` + `col-lg-*`, on purpose. A `.row` wraps, and when it
   does `align-content: stretch` pads BOTH lines equally — so below lg the fields column would grow
   dead space instead of letting the list absorb the slack. One flex container whose DIRECTION
   switches has neither problem: column below lg, row at lg+. */
.bx-cols { display: flex; flex-direction: column; gap: 1rem; min-height: 0; flex: 1 1 auto; }
.bx-cols > .bx-col-fields { min-height: 0; }
.bx-cols > .bx-col-list { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
@media (min-width: 992px) {
  .bx-cols { flex-direction: row; align-items: stretch; gap: 1.25rem; }
  /* ⚠ NO `overflow-y: auto` on the fields column. It was added as insurance and produced a stray
     HORIZONTAL scrollbar: per spec, when one axis is not `visible` the other computes from `visible`
     to `auto`, so `overflow-y` alone silently switches on `overflow-x` — and `.row`'s negative gutter
     margins make the row wider than its container, which is enough to trip it. Set BOTH axes or
     neither. */
  .bx-cols > .bx-col-fields { flex: 0 0 40%; max-width: 40%; }
  .bx-cols > .bx-col-list { flex: 1 1 60%; min-width: 0; }
  /* ⚠⚠ THE FIELDS COLUMN MUST NEVER BE SHRUNK BELOW ITS CONTENT (2026-08-11).
     `.bx-cols` is `flex: 1 1 auto; min-height: 0`, which lets the row shrink to whatever height is
     left over. That is right for the LIST column — it has its own internal scroller — and wrong for
     the FIELDS column, which has nothing to scroll and simply PAINTS OVER whatever follows it.
     Measured on Add New Stock: the row got 515px, the fields column needed 604px, and the adder
     spilled 89px past the row directly on top of the checkbox and the Save/Cancel buttons.
     It never showed on New Box because there the fields column is the SHORTER of the two — so the
     bug was latent in the shared block from the day it was written, and only the second user of the
     pattern could reveal it.
     `min-content` on the row makes the flex container refuse to shrink below its tallest item's
     content, so the row grows instead and `.modal-body`'s `overflow-y: auto` becomes the last-resort
     scroller exactly as designed. ⚠ Scoped to lg+ ON PURPOSE: below this breakpoint `.bx-cols` is a
     COLUMN, where `min-content` would be fields + list stacked and would force the outer scrollbar on
     every small screen. There, shrinking the list IS the correct behaviour. */
  .bx-cols { min-height: min-content; }
  .bx-cols > .bx-col-fields { min-height: auto; }
  /* Opt-in wider fields column, for a modal whose ADDER carries more than a couple of controls.
     ⚠ A MODIFIER rather than a change to the 40% default, on purpose: the two users of this block
     want different splits. Add New Stock's adder has four pickers (two of them paired on one line at
     this breakpoint), and at 40% that pair measured ~155px each including its "New" button — usable,
     but tight enough that a longer location name clips in the closed picker. New Box's adder is two
     stacked selects and needs none of it, while its list carries three columns of editable inputs
     that would be the ones paying for the extra width. Widening the default would have taken 8% off
     a table that never asked for it. */
  .bx-cols.bx-fields-wide > .bx-col-fields { flex: 0 0 48%; max-width: 48%; }
  .bx-cols.bx-fields-wide > .bx-col-list { flex: 1 1 52%; }
}

.bx-list {
  display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto;
  border: 1px solid var(--m-border-strong, var(--bs-border-color));
  border-radius: var(--bs-border-radius);
}
/* The head and the adder stay OUTSIDE the scroll box: capping the whole card would push the adder
   out of reach exactly when the list is long enough to need it, and those pickers are select2s whose
   dropdown is parented to their own wrapper — so keeping them out here is also what stops an open
   dropdown being clipped. */
.bx-list-head { padding: .5rem .75rem; border-bottom: 1px solid var(--bs-border-color); }
.bx-list-scroll { flex: 1 1 auto; min-height: 8rem; overflow-y: auto; }
.bx-list-table { margin-bottom: 0; }
/* Compact rows, the biggest single lever on how much of the list you can SEE: measured, a row was
   65px (a 38px input inside 25.6px of cell padding), so a 156px region showed 2.4 of them. At 34px +
   9.6px it is ~44px, i.e. ~3.5 rows in the same space. An editable grid is the one place in this app
   where a sub-38px control is right — it is a dense table of short values, not a form field.
   ⚠ The row-input height lives ONCE, here. A second `.bx-list-table .form-control { height: … }`
   anywhere later in this file silently wins on source order at equal specificity, which is exactly
   how the compact rows once measured 65px with nothing wrong in the new rule — there were simply two
   of them. If a size here refuses to change, grep for a duplicate selector BEFORE touching
   specificity or reaching for `!important`. */
.bx-list-table > :not(caption) > * > * { padding: .3rem .5rem; vertical-align: middle; }
.bx-list-table .form-control, .bx-list-table .form-select { height: 2.125rem; }
.bx-list-table th { font-size: .75rem; text-transform: uppercase; letter-spacing: .4px; }
/* Pin the header inside that scroll box.
   ⚠⚠ THE `!important` IS LOAD-BEARING, AND IT TOOK THREE GOES TO FIND OUT WHY. Rows kept scrolling
   VISIBLY THROUGH the header ("double text showing"), which reads as a z-index bug and is purely a
   cascade problem. Enumerating every matching rule in the live DOM named the winner:

       .ui-glass .table thead th { background: var(--g-panel-2) !important; }   (this file)

   `--g-panel-2` is a TRANSLUCENT glass shade (rgba(255,255,255,0.06)), and because the declaration
   is `!important` it beats plain declarations NO MATTER HOW SPECIFIC. So an attempt without
   `!important` lost however it was written.
   ⚠ Specificity still decides between two `!important` declarations, which is why `.modal` is in the
   selector: `.ui-glass .table thead th` is (0,2,2), and `.bx-list-table > thead > tr > th` alone is
   only (0,1,3) — which LOSES. With `.modal` it is (0,2,3) and wins. The original used `#wboxModal`
   for this; the extra class does the same job without tying the rule to one page.
   ⚠ `--m-surface-2` (not `--m-surface`) is the token: the theme's own OPAQUE table-header shade, so
   the pinned header looks like every other table header instead of merging into the card behind it.
   ⚠ The bottom rule is an inset box-shadow, NOT `border-bottom`: under `border-collapse: collapse`
   that border belongs to the collapsed edge the header SHARES with the first row, so it scrolls away
   with that row. A shadow is painted by the sticky element itself and stays. */
.modal .bx-list-table > thead > tr > th {
  position: sticky;
  top: 0;
  z-index: 2;
  background-color: var(--m-surface-2, var(--m-surface, #ffffff)) !important;
  box-shadow: inset 0 -1px 0 var(--bs-border-color);
}
.bx-list-empty td { padding: 1.1rem .5rem !important; text-align: center; color: var(--bs-secondary-color); }
/* A read-only cell is a fact, not a field — it must not compete with the inputs beside it. */
.bx-cell-note { white-space: nowrap; font-size: .8125rem; }

/* Standalone panel: a full border + radius matching the list card, not a lone `border-top`. */
.bx-adder {
  margin-top: 1rem;
  padding: .75rem;
  border: 1px solid var(--m-border-strong, var(--bs-border-color));
  border-radius: var(--bs-border-radius);
}
/* "or" between two independent ways to add — a rule with the word in a gap, so the blocks read as
   ALTERNATIVES rather than as steps 1 and 2 of one sequence. */
.bx-adder-or {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: .6rem 0;
  color: var(--bs-secondary-color);
  font-size: .75rem;
  text-transform: uppercase;
  letter-spacing: .5px;
}
.bx-adder-or::before,
.bx-adder-or::after { content: ""; flex: 1 1 auto; border-top: 1px solid var(--bs-border-color); }

/* ── FORCE EVERY ADDER DROPDOWN ABOVE ITS FIELD ─────────────────────────────────────────────────
   The adder is the LAST thing in a modal capped to the viewport, so a downward panel has nowhere to
   go: with a few hundred options it pushes past the modal's bottom edge and GROWS `.modal-body`'s
   scrollable area — a scrollbar appears that cannot usefully be scrolled, because scrolling it moves
   the field the panel is anchored to.
   ⚠⚠ select2 DOES flip by itself, but it measures the WINDOW, not the modal. Given room below the
   field in the viewport it opens downward — correct by its own rules, and wrong here. That is why
   this looks "fixed" on a SHORT window while still broken on a tall one: on the short one select2 had
   already chosen `above` and the override was doing nothing at all. Verify a forced layout in the
   condition that would otherwise DEFEAT it.
   ⚠ TWO RULES ARE NEEDED and one alone is silently useless. select2's positioned container is
   `span.select2-container` (appended beside the field inside common.js's `.position-relative`
   wrapper) and it has ZERO HEIGHT — the visible panel is `.select2-dropdown`, itself absolutely
   positioned inside it. Pinning only the container puts its collapsed box at the field's top edge and
   the panel still renders DOWNWARD from there, over the field. The panel needs `bottom: 0` against
   that container so it grows UPWARD at whatever height its content is.
   ⚠ `!important` on every one: select2 writes `top`/`left` INLINE and re-runs `_positionDropdown()`
   on scroll and resize, and only a stylesheet `!important` outranks an inline declaration. (This is
   also why no id is needed here — inline vs `!important` is not a specificity contest.)
   ⚠ Identifying the container: the FIELD's container also matches `.select2-container`, so it must be
   excluded. It carries the source element's own `select2` class and the appended one does not, which
   is the discriminator used here; the `:has()` rule says the same thing structurally and is kept
   SEPARATE so an engine without `:has()` drops only that one and not both. */
.bx-adder .position-relative > .select2-container:not(.select2) {
  top: auto !important;
  bottom: calc(100% + 2px) !important;
}
.bx-adder .position-relative > .select2-container:not(.select2) > .select2-dropdown {
  top: auto !important;
  bottom: 0 !important;
}
.bx-adder .position-relative > .select2-container:has(> .select2-dropdown) {
  top: auto !important;
  bottom: calc(100% + 2px) !important;
}
.bx-adder .position-relative > .select2-container:has(> .select2-dropdown) > .select2-dropdown {
  top: auto !important;
  bottom: 0 !important;
}
/* select2 toggles `--above`/`--below` on the FIELD for its corner radii, and it is now sometimes
   wrong because the panel is forced above regardless. Square the joined edges so field and panel
   still read as one control either way. */
.bx-adder .select2-container--open .select2-selection--single {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

/* ── Dealer-roster sweep drop zone ──────────────────────────────────────────────────────────────
   ⚠ Colours come from the theme tokens, never hardcoded hexes — see docs/ui-theme.md. A hardcoded
     value here would look right in light mode and wrong in dark, which is the trap that section
     exists to prevent. */
.sweep-drop {
  border: 1px dashed var(--bs-border-color);
  border-radius: .5rem;
  padding: 1.25rem 1rem;
  text-align: center;
  cursor: pointer;
  transition: border-color .15s ease, background-color .15s ease;
}
.sweep-drop:hover { border-color: var(--bs-primary); }
.sweep-drop.is-dragover {
  border-color: var(--bs-primary);
  background-color: rgba(var(--bs-primary-rgb), .06);
}
