

/* added: user directive -- self-host Inter (matching Tabler's own reference font exactly)
   instead of importing from rsms.me or Google Fonts on every page. Consistent with this repo's
   own precedent of self-hosting Tabler/Bootstrap rather than loading them from a CDN once real
   UI work began. Single variable-font file (fetched from Google's own CDN once, latin subset
   only -- this app's UI is English-only) covers weights 400-700 in one download; Google's own
   css2 API serves discrete per-weight @font-face blocks all pointing at this same file. */
@font-face {
  font-family: "Inter Var";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/assets/fonts/inter/inter-latin.woff2") format("woff2");
}

:root,
[data-bs-theme="light"] {
  --bs-primary: var(--flx-navy);
  --bs-primary-rgb: 8, 18, 32;
  --bs-primary-text-emphasis: #1e40af;
  --bs-primary-bg-subtle: #dbeafe;
  --bs-primary-border-subtle: #93c5fd;
  --bs-secondary: #b5b5b5;
  --bs-secondary-rgb: 181, 181, 181;
  --bs-secondary-text-emphasis: #484848;
  --bs-secondary-bg-subtle: #f0f0f0;
  --bs-secondary-border-subtle: #e1e1e1;
  --bs-success: #03810b;
  --bs-success-rgb: 3, 129, 11;
  --bs-success-text-emphasis: #013404;
  --bs-success-bg-subtle: #cde6ce;
  --bs-success-border-subtle: #9acd9d;
  --bs-info: var(--flx-slate);
  --bs-info-rgb: 37, 99, 235;
  --bs-info-text-emphasis: #1e40af;
  --bs-info-bg-subtle: #dbeafe;
  --bs-info-border-subtle: #93c5fd;
  --bs-warning: var(--flx-amber);
  --bs-warning-rgb: 245, 173, 62;
  --bs-warning-text-emphasis: #624519;
  --bs-warning-bg-subtle: #fdefd8;
  --bs-warning-border-subtle: #fbdeb2;
  --bs-danger: #d61731;
  --bs-danger-rgb: 214, 23, 49;
  --bs-danger-text-emphasis: #560914;
  --bs-danger-bg-subtle: #f7d1d6;
  --bs-danger-border-subtle: #efa2ad;
  --bs-light: #f0f0f0;
  --bs-light-rgb: 240, 240, 240;
  --bs-light-text-emphasis: #606060;
  --bs-light-bg-subtle: #fcfcfc;
  --bs-light-border-subtle: #f9f9f9;
  --bs-dark: var(--flx-navy);
  --bs-dark-rgb: 8, 18, 32;
  --bs-dark-text-emphasis: var(--flx-navy);
  --bs-dark-bg-subtle: #c8cdd6;
  --bs-dark-border-subtle: #93a0b2;

  --bs-link-color: var(--flx-steel);
  --bs-link-color-rgb: 43, 52, 70;
  --bs-link-hover-color: var(--flx-navy);
  --bs-link-hover-color-rgb: 8, 18, 32;
  --bs-link-decoration: none;
}

/* FLX-368 -- unified radius/elevation/motion tokens. Border-radius across the app was ad hoc
   (3px/4px/6px/8px/10px/12px scattered across components with no shared scale), the single
   biggest source of the "looks like it came from different libraries" inconsistency. Overriding
   Bootstrap's own --bs-border-radius-* variables here cascades a consistent scale into every
   .btn/.card/.form-control/.modal-content/.dropdown-menu/.badge app-wide without touching any
   template. Theme-agnostic (radius/motion are the same in light and dark; only shadow color
   opacity differs, since dark surfaces need a stronger shadow to read against a dark background). */
:root {
  --bs-border-radius-sm: .375rem;
  --bs-border-radius:    .625rem;
  --bs-border-radius-lg: .875rem;
  --bs-border-radius-xl: 1.25rem;
  --bs-border-radius-2xl:1.25rem;
  --bs-border-radius-pill: 50rem;

  --flx-shadow-xs: 0 1px 2px rgba(8,18,32,.06);
  --flx-shadow-sm: 0 1px 3px rgba(8,18,32,.08), 0 1px 2px rgba(8,18,32,.04);
  --flx-shadow-md: 0 6px 16px rgba(8,18,32,.10), 0 2px 6px rgba(8,18,32,.06);
  --flx-shadow-lg: 0 16px 40px rgba(8,18,32,.14), 0 4px 10px rgba(8,18,32,.06);
  --flx-ease: cubic-bezier(.4,0,.2,1);

  /* modified: FLX-729 -- card-header band colour, user-specified 2026-08-02 | was: #111827
     (FLX-713). Lives as a token because the header treatment below derives six other values
     from it (button inversions in both themes, muted text), and those silently stop pairing
     if the band is changed in one place only. */
  --flx-card-header-bg: #215399;
  --flx-card-header-muted: #CBD5E1;
}
[data-bs-theme="dark"] {
  --flx-shadow-xs: 0 1px 2px rgba(0,0,0,.35);
  --flx-shadow-sm: 0 1px 4px rgba(0,0,0,.5), 0 0 0 1px rgba(255,255,255,.04);
  --flx-shadow-md: 0 8px 20px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.04);
  --flx-shadow-lg: 0 20px 48px rgba(0,0,0,.65), 0 0 0 1px rgba(255,255,255,.05);
}

/* Consistent, smooth interaction feedback across every shared component -- was previously only
   set piecemeal on a handful of individual elements, so buttons/menus/inputs felt inconsistent
   in responsiveness. Applied to the base Bootstrap classes so it's automatic on every page. */
.btn, .form-control, .form-select, .form-check-input, .nav-link, .page-link,
.dropdown-item, .list-group-item-action, .card {
  transition: box-shadow .15s var(--flx-ease), transform .1s var(--flx-ease),
              background-color .15s var(--flx-ease), border-color .15s var(--flx-ease),
              color .15s var(--flx-ease);
}
.btn:not(:disabled):not(.disabled):active { transform: translateY(1px); }
.dropdown-menu, .popover, .modal-content, .offcanvas { box-shadow: var(--flx-shadow-lg); }
.badge { font-weight: 600; }

.btn-primary {
  --bs-btn-color: #fff;
  --tblr-btn-color: #fff;
  --bs-btn-bg: var(--flx-navy);
  --tblr-btn-bg: var(--flx-navy);
  --bs-btn-border-color: var(--flx-navy);
  --tblr-btn-border-color: var(--flx-navy);
  --bs-btn-hover-color: #fff;
  --tblr-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--flx-navy);
  --tblr-btn-hover-bg: var(--flx-navy);
  --bs-btn-hover-border-color: var(--flx-navy);
  --tblr-btn-hover-border-color: var(--flx-navy);
  --bs-btn-focus-shadow-rgb: 8, 18, 32;
  --tblr-btn-focus-shadow-rgb: 8, 18, 32;
  --bs-btn-active-color: #fff;
  --tblr-btn-active-color: #fff;
  --bs-btn-active-bg: var(--flx-slate);
  --tblr-btn-active-bg: var(--flx-slate);
  --bs-btn-active-border-color: var(--flx-slate);
  --tblr-btn-active-border-color: var(--flx-slate);
  --bs-btn-disabled-color: #fff;
  --tblr-btn-disabled-color: #fff;
  --bs-btn-disabled-bg: var(--flx-navy);
  --tblr-btn-disabled-bg: var(--flx-navy);
  --bs-btn-disabled-border-color: var(--flx-navy);
  --tblr-btn-disabled-border-color: var(--flx-navy);
}

.btn-outline-primary {
  --bs-btn-color: var(--flx-navy);
  --tblr-btn-color: var(--flx-navy);
  --bs-btn-border-color: var(--flx-navy);
  --tblr-btn-border-color: var(--flx-navy);
  --bs-btn-focus-shadow-rgb: 8, 18, 32;
  --tblr-btn-focus-shadow-rgb: 8, 18, 32;
  --bs-btn-hover-color: #fff;
  --tblr-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--flx-navy);
  --tblr-btn-hover-bg: var(--flx-navy);
  --bs-btn-hover-border-color: var(--flx-navy);
  --tblr-btn-hover-border-color: var(--flx-navy);
  --bs-btn-active-color: #fff;
  --tblr-btn-active-color: #fff;
  --bs-btn-active-bg: var(--flx-navy);
  --tblr-btn-active-bg: var(--flx-navy);
  --bs-btn-active-border-color: var(--flx-navy);
  --tblr-btn-active-border-color: var(--flx-navy);
  --bs-btn-disabled-color: var(--flx-navy);
  --tblr-btn-disabled-color: var(--flx-navy);
  --bs-btn-disabled-bg: transparent;
  --tblr-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: var(--flx-navy);
  --tblr-btn-disabled-border-color: var(--flx-navy);
}
.btn-secondary {
  --bs-btn-color: #000000;
  --tblr-btn-color: #000000;
  --bs-btn-bg: #b5b5b5;
  --tblr-btn-bg: #b5b5b5;
  --bs-btn-border-color: #b5b5b5;
  --tblr-btn-border-color: #b5b5b5;
  --bs-btn-hover-color: #000000;
  --tblr-btn-hover-color: #000000;
  --bs-btn-hover-bg: #c0c0c0;
  --tblr-btn-hover-bg: #c0c0c0;
  --bs-btn-hover-border-color: #bcbcbc;
  --tblr-btn-hover-border-color: #bcbcbc;
  --bs-btn-focus-shadow-rgb: 27, 27, 27;
  --tblr-btn-focus-shadow-rgb: 27, 27, 27;
  --bs-btn-active-color: #000000;
  --tblr-btn-active-color: #000000;
  --bs-btn-active-bg: #c4c4c4;
  --tblr-btn-active-bg: #c4c4c4;
  --bs-btn-active-border-color: #bcbcbc;
  --tblr-btn-active-border-color: #bcbcbc;
  --bs-btn-disabled-color: #000000;
  --tblr-btn-disabled-color: #000000;
  --bs-btn-disabled-bg: #b5b5b5;
  --tblr-btn-disabled-bg: #b5b5b5;
  --bs-btn-disabled-border-color: #b5b5b5;
  --tblr-btn-disabled-border-color: #b5b5b5;
}
.btn-outline-secondary {
  --bs-btn-color: #b5b5b5;
  --tblr-btn-color: #b5b5b5;
  --bs-btn-border-color: #b5b5b5;
  --tblr-btn-border-color: #b5b5b5;
  --bs-btn-focus-shadow-rgb: 181, 181, 181;
  --tblr-btn-focus-shadow-rgb: 181, 181, 181;
  --bs-btn-hover-color: #000000;
  --tblr-btn-hover-color: #000000;
  --bs-btn-hover-bg: #b5b5b5;
  --tblr-btn-hover-bg: #b5b5b5;
  --bs-btn-hover-border-color: #b5b5b5;
  --tblr-btn-hover-border-color: #b5b5b5;
  --bs-btn-active-color: #000000;
  --tblr-btn-active-color: #000000;
  --bs-btn-active-bg: #b5b5b5;
  --tblr-btn-active-bg: #b5b5b5;
  --bs-btn-active-border-color: #b5b5b5;
  --tblr-btn-active-border-color: #b5b5b5;
  --bs-btn-disabled-color: #b5b5b5;
  --tblr-btn-disabled-color: #b5b5b5;
  --bs-btn-disabled-bg: transparent;
  --tblr-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: #b5b5b5;
  --tblr-btn-disabled-border-color: #b5b5b5;
}
.btn-success {
  --bs-btn-color: #fff;
  --tblr-btn-color: #fff;
  --bs-btn-bg: #03810b;
  --tblr-btn-bg: #03810b;
  --bs-btn-border-color: #03810b;
  --tblr-btn-border-color: #03810b;
  --bs-btn-hover-color: #fff;
  --tblr-btn-hover-color: #fff;
  --bs-btn-hover-bg: #036e09;
  --tblr-btn-hover-bg: #036e09;
  --bs-btn-hover-border-color: #026709;
  --tblr-btn-hover-border-color: #026709;
  --bs-btn-focus-shadow-rgb: 217, 236, 218;
  --tblr-btn-focus-shadow-rgb: 217, 236, 218;
  --bs-btn-active-color: #fff;
  --tblr-btn-active-color: #fff;
  --bs-btn-active-bg: #026709;
  --tblr-btn-active-bg: #026709;
  --bs-btn-active-border-color: #026108;
  --tblr-btn-active-border-color: #026108;
  --bs-btn-disabled-color: #fff;
  --tblr-btn-disabled-color: #fff;
  --bs-btn-disabled-bg: #03810b;
  --tblr-btn-disabled-bg: #03810b;
  --bs-btn-disabled-border-color: #03810b;
  --tblr-btn-disabled-border-color: #03810b;
}
.btn-outline-success {
  --bs-btn-color: #03810b;
  --tblr-btn-color: #03810b;
  --bs-btn-border-color: #03810b;
  --tblr-btn-border-color: #03810b;
  --bs-btn-focus-shadow-rgb: 3, 129, 11;
  --tblr-btn-focus-shadow-rgb: 3, 129, 11;
  --bs-btn-hover-color: #fff;
  --tblr-btn-hover-color: #fff;
  --bs-btn-hover-bg: #03810b;
  --tblr-btn-hover-bg: #03810b;
  --bs-btn-hover-border-color: #03810b;
  --tblr-btn-hover-border-color: #03810b;
  --bs-btn-active-color: #fff;
  --tblr-btn-active-color: #fff;
  --bs-btn-active-bg: #03810b;
  --tblr-btn-active-bg: #03810b;
  --bs-btn-active-border-color: #03810b;
  --tblr-btn-active-border-color: #03810b;
  --bs-btn-disabled-color: #03810b;
  --tblr-btn-disabled-color: #03810b;
  --bs-btn-disabled-bg: transparent;
  --tblr-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: #03810b;
  --tblr-btn-disabled-border-color: #03810b;
}

.btn-info {
  --bs-btn-color: #fff;
  --tblr-btn-color: #fff;
  --bs-btn-bg: var(--flx-slate);
  --tblr-btn-bg: var(--flx-slate);
  --bs-btn-border-color: var(--flx-slate);
  --tblr-btn-border-color: var(--flx-slate);
  --bs-btn-hover-color: #fff;
  --tblr-btn-hover-color: #fff;
  --bs-btn-hover-bg: #1d4ed8;
  --tblr-btn-hover-bg: #1d4ed8;
  --bs-btn-hover-border-color: #1d4ed8;
  --tblr-btn-hover-border-color: #1d4ed8;
  --bs-btn-focus-shadow-rgb: 37, 99, 235;
  --tblr-btn-focus-shadow-rgb: 37, 99, 235;
  --bs-btn-active-color: #fff;
  --tblr-btn-active-color: #fff;
  --bs-btn-active-bg: #1e40af;
  --tblr-btn-active-bg: #1e40af;
  --bs-btn-active-border-color: #1e40af;
  --tblr-btn-active-border-color: #1e40af;
  --bs-btn-disabled-color: #fff;
  --tblr-btn-disabled-color: #fff;
  --bs-btn-disabled-bg: var(--flx-slate);
  --tblr-btn-disabled-bg: var(--flx-slate);
  --bs-btn-disabled-border-color: var(--flx-slate);
  --tblr-btn-disabled-border-color: var(--flx-slate);
}

.btn-outline-info {
  --bs-btn-color: var(--flx-slate);
  --tblr-btn-color: var(--flx-slate);
  --bs-btn-border-color: var(--flx-slate);
  --tblr-btn-border-color: var(--flx-slate);
  --bs-btn-focus-shadow-rgb: 37, 99, 235;
  --tblr-btn-focus-shadow-rgb: 37, 99, 235;
  --bs-btn-hover-color: #fff;
  --tblr-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--flx-slate);
  --tblr-btn-hover-bg: var(--flx-slate);
  --bs-btn-hover-border-color: var(--flx-slate);
  --tblr-btn-hover-border-color: var(--flx-slate);
  --bs-btn-active-color: #fff;
  --tblr-btn-active-color: #fff;
  --bs-btn-active-bg: var(--flx-slate);
  --tblr-btn-active-bg: var(--flx-slate);
  --bs-btn-active-border-color: var(--flx-slate);
  --tblr-btn-active-border-color: var(--flx-slate);
  --bs-btn-disabled-color: var(--flx-slate);
  --tblr-btn-disabled-color: var(--flx-slate);
  --bs-btn-disabled-bg: transparent;
  --tblr-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: var(--flx-slate);
  --tblr-btn-disabled-border-color: var(--flx-slate);
}
.btn-warning {
  --bs-btn-color: #000000;
  --tblr-btn-color: #000000;
  --bs-btn-bg: var(--flx-amber);
  --tblr-btn-bg: var(--flx-amber);
  --bs-btn-border-color: var(--flx-amber);
  --tblr-btn-border-color: var(--flx-amber);
  --bs-btn-hover-color: #000000;
  --tblr-btn-hover-color: #000000;
  --bs-btn-hover-bg: #f7b95b;
  --tblr-btn-hover-bg: #f7b95b;
  --bs-btn-hover-border-color: #f6b551;
  --tblr-btn-hover-border-color: #f6b551;
  --bs-btn-focus-shadow-rgb: 37, 26, 9;
  --tblr-btn-focus-shadow-rgb: 37, 26, 9;
  --bs-btn-active-color: #000000;
  --tblr-btn-active-color: #000000;
  --bs-btn-active-bg: #f7bd65;
  --tblr-btn-active-bg: #f7bd65;
  --bs-btn-active-border-color: #f6b551;
  --tblr-btn-active-border-color: #f6b551;
  --bs-btn-disabled-color: #000000;
  --tblr-btn-disabled-color: #000000;
  --bs-btn-disabled-bg: var(--flx-amber);
  --tblr-btn-disabled-bg: var(--flx-amber);
  --bs-btn-disabled-border-color: var(--flx-amber);
  --tblr-btn-disabled-border-color: var(--flx-amber);
}
.btn-outline-warning {
  --bs-btn-color: var(--flx-amber);
  --tblr-btn-color: var(--flx-amber);
  --bs-btn-border-color: var(--flx-amber);
  --tblr-btn-border-color: var(--flx-amber);
  --bs-btn-focus-shadow-rgb: 245, 173, 62;
  --tblr-btn-focus-shadow-rgb: 245, 173, 62;
  --bs-btn-hover-color: #000000;
  --tblr-btn-hover-color: #000000;
  --bs-btn-hover-bg: var(--flx-amber);
  --tblr-btn-hover-bg: var(--flx-amber);
  --bs-btn-hover-border-color: var(--flx-amber);
  --tblr-btn-hover-border-color: var(--flx-amber);
  --bs-btn-active-color: #000000;
  --tblr-btn-active-color: #000000;
  --bs-btn-active-bg: var(--flx-amber);
  --tblr-btn-active-bg: var(--flx-amber);
  --bs-btn-active-border-color: var(--flx-amber);
  --tblr-btn-active-border-color: var(--flx-amber);
  --bs-btn-disabled-color: var(--flx-amber);
  --tblr-btn-disabled-color: var(--flx-amber);
  --bs-btn-disabled-bg: transparent;
  --tblr-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: var(--flx-amber);
  --tblr-btn-disabled-border-color: var(--flx-amber);
}
.btn-danger {
  --bs-btn-color: #fff;
  --tblr-btn-color: #fff;
  --bs-btn-bg: #d61731;
  --tblr-btn-bg: #d61731;
  --bs-btn-border-color: #d61731;
  --tblr-btn-border-color: #d61731;
  --bs-btn-hover-color: #fff;
  --tblr-btn-hover-color: #fff;
  --bs-btn-hover-bg: #b6142a;
  --tblr-btn-hover-bg: #b6142a;
  --bs-btn-hover-border-color: #ab1227;
  --tblr-btn-hover-border-color: #ab1227;
  --bs-btn-focus-shadow-rgb: 249, 220, 224;
  --tblr-btn-focus-shadow-rgb: 249, 220, 224;
  --bs-btn-active-color: #fff;
  --tblr-btn-active-color: #fff;
  --bs-btn-active-bg: #ab1227;
  --tblr-btn-active-bg: #ab1227;
  --bs-btn-active-border-color: #a11125;
  --tblr-btn-active-border-color: #a11125;
  --bs-btn-disabled-color: #fff;
  --tblr-btn-disabled-color: #fff;
  --bs-btn-disabled-bg: #d61731;
  --tblr-btn-disabled-bg: #d61731;
  --bs-btn-disabled-border-color: #d61731;
  --tblr-btn-disabled-border-color: #d61731;
}
.btn-outline-danger {
  --bs-btn-color: #d61731;
  --tblr-btn-color: #d61731;
  --bs-btn-border-color: #d61731;
  --tblr-btn-border-color: #d61731;
  --bs-btn-focus-shadow-rgb: 214, 23, 49;
  --tblr-btn-focus-shadow-rgb: 214, 23, 49;
  --bs-btn-hover-color: #fff;
  --tblr-btn-hover-color: #fff;
  --bs-btn-hover-bg: #d61731;
  --tblr-btn-hover-bg: #d61731;
  --bs-btn-hover-border-color: #d61731;
  --tblr-btn-hover-border-color: #d61731;
  --bs-btn-active-color: #fff;
  --tblr-btn-active-color: #fff;
  --bs-btn-active-bg: #d61731;
  --tblr-btn-active-bg: #d61731;
  --bs-btn-active-border-color: #d61731;
  --tblr-btn-active-border-color: #d61731;
  --bs-btn-disabled-color: #d61731;
  --tblr-btn-disabled-color: #d61731;
  --bs-btn-disabled-bg: transparent;
  --tblr-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: #d61731;
  --tblr-btn-disabled-border-color: #d61731;
}
.btn-light {
  --bs-btn-color: #000000;
  --tblr-btn-color: #000000;
  --bs-btn-bg: #f0f0f0;
  --tblr-btn-bg: #f0f0f0;
  --bs-btn-border-color: #f0f0f0;
  --tblr-btn-border-color: #f0f0f0;
  --bs-btn-hover-color: #000000;
  --tblr-btn-hover-color: #000000;
  --bs-btn-hover-bg: #cccccc;
  --tblr-btn-hover-bg: #cccccc;
  --bs-btn-hover-border-color: #c0c0c0;
  --tblr-btn-hover-border-color: #c0c0c0;
  --bs-btn-focus-shadow-rgb: 36, 36, 36;
  --tblr-btn-focus-shadow-rgb: 36, 36, 36;
  --bs-btn-active-color: #000000;
  --tblr-btn-active-color: #000000;
  --bs-btn-active-bg: #c0c0c0;
  --tblr-btn-active-bg: #c0c0c0;
  --bs-btn-active-border-color: #b4b4b4;
  --tblr-btn-active-border-color: #b4b4b4;
  --bs-btn-disabled-color: #000000;
  --tblr-btn-disabled-color: #000000;
  --bs-btn-disabled-bg: #f0f0f0;
  --tblr-btn-disabled-bg: #f0f0f0;
  --bs-btn-disabled-border-color: #f0f0f0;
  --tblr-btn-disabled-border-color: #f0f0f0;
}
.btn-outline-light {
  --bs-btn-color: #f0f0f0;
  --tblr-btn-color: #f0f0f0;
  --bs-btn-border-color: #f0f0f0;
  --tblr-btn-border-color: #f0f0f0;
  --bs-btn-focus-shadow-rgb: 240, 240, 240;
  --tblr-btn-focus-shadow-rgb: 240, 240, 240;
  --bs-btn-hover-color: #000000;
  --tblr-btn-hover-color: #000000;
  --bs-btn-hover-bg: #f0f0f0;
  --tblr-btn-hover-bg: #f0f0f0;
  --bs-btn-hover-border-color: #f0f0f0;
  --tblr-btn-hover-border-color: #f0f0f0;
  --bs-btn-active-color: #000000;
  --tblr-btn-active-color: #000000;
  --bs-btn-active-bg: #f0f0f0;
  --tblr-btn-active-bg: #f0f0f0;
  --bs-btn-active-border-color: #f0f0f0;
  --tblr-btn-active-border-color: #f0f0f0;
  --bs-btn-disabled-color: #f0f0f0;
  --tblr-btn-disabled-color: #f0f0f0;
  --bs-btn-disabled-bg: transparent;
  --tblr-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: #f0f0f0;
  --tblr-btn-disabled-border-color: #f0f0f0;
}

.btn-dark {
  --bs-btn-color: #fff;
  --tblr-btn-color: #fff;
  --bs-btn-bg: var(--flx-navy);
  --tblr-btn-bg: var(--flx-navy);
  --bs-btn-border-color: var(--flx-navy);
  --tblr-btn-border-color: var(--flx-navy);
  --bs-btn-hover-color: #fff;
  --tblr-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--flx-navy);
  --tblr-btn-hover-bg: var(--flx-navy);
  --bs-btn-hover-border-color: var(--flx-navy);
  --tblr-btn-hover-border-color: var(--flx-navy);
  --bs-btn-focus-shadow-rgb: 8, 18, 32;
  --tblr-btn-focus-shadow-rgb: 8, 18, 32;
  --bs-btn-active-color: #fff;
  --tblr-btn-active-color: #fff;
  --bs-btn-active-bg: var(--flx-slate);
  --tblr-btn-active-bg: var(--flx-slate);
  --bs-btn-active-border-color: var(--flx-slate);
  --tblr-btn-active-border-color: var(--flx-slate);
  --bs-btn-disabled-color: #fff;
  --tblr-btn-disabled-color: #fff;
  --bs-btn-disabled-bg: var(--flx-navy);
  --tblr-btn-disabled-bg: var(--flx-navy);
  --bs-btn-disabled-border-color: var(--flx-navy);
  --tblr-btn-disabled-border-color: var(--flx-navy);
}

.btn-outline-dark {
  --bs-btn-color: var(--flx-navy);
  --tblr-btn-color: var(--flx-navy);
  --bs-btn-border-color: var(--flx-navy);
  --tblr-btn-border-color: var(--flx-navy);
  --bs-btn-focus-shadow-rgb: 8, 18, 32;
  --tblr-btn-focus-shadow-rgb: 8, 18, 32;
  --bs-btn-hover-color: #fff;
  --tblr-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--flx-navy);
  --tblr-btn-hover-bg: var(--flx-navy);
  --bs-btn-hover-border-color: var(--flx-navy);
  --tblr-btn-hover-border-color: var(--flx-navy);
  --bs-btn-active-color: #fff;
  --tblr-btn-active-color: #fff;
  --bs-btn-active-bg: var(--flx-navy);
  --tblr-btn-active-bg: var(--flx-navy);
  --bs-btn-active-border-color: var(--flx-navy);
  --tblr-btn-active-border-color: var(--flx-navy);
  --bs-btn-disabled-color: var(--flx-navy);
  --tblr-btn-disabled-color: var(--flx-navy);
  --bs-btn-disabled-bg: transparent;
  --tblr-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: var(--flx-navy);
  --tblr-btn-disabled-border-color: var(--flx-navy);
}

h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
  font-weight: 400;
}
a {
  text-decoration: var(--bs-link-decoration);
}
.aspect-ratio-21x9 {
  aspect-ratio: 21/9;
}
.aspect-ratio-1x1 {
  aspect-ratio: 1/1;
}
.storage-dropzone {
  border: 2px dashed var(--bs-border-color);
  border-radius: 1rem;
  padding: 2.5rem 1.5rem;
  text-align: center;
  background-color: var(--bs-tertiary-bg);
  transition: 0.2s;
  cursor: pointer;
}
.storage-dropzone:hover {
  border-color: var(--bs-primary);
  background-color: var(--bs-primary-bg-subtle);
}
.progress-ring-circle {
  transition: stroke-dashoffset 0.35s;
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
}
.stat-card {
  transition: transform 0.2s;
}
.stat-card:hover {
  transform: translateY(-2px);
  background-color: var(--bs-tertiary-bg) !important;
}
.chart-bar {
  flex: 1;
  background-color: var(--bs-primary-bg-subtle);
  border-radius: 4px 4px 0 0;
  transition: height 0.5s;
  position: relative;
}
.chart-bar.active {
  background-color: var(--bs-primary);
}
.chart-bar:hover::after {
  content: attr(data-value);
  position: absolute;
  top: -25px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 10px;
  background: var(--bs-dark);
  color: #fff;
  padding: 2px 4px;
  border-radius: 4px;
}
.color-swatch {
  background-color: #212529;
  width: 18px;
  height: 18px;
  cursor: pointer;
  box-shadow: 0 0 0 1px #dee2e6;
}
.btn-social {
  transition: background-color 0.2s;
}
.btn-social:hover {
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-body-color) !important;
}
.product-card {
  transition:
    transform 0.3s,
    box-shadow 0.3s;
}
.product-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.1) !important;
}
.product-img {
  transition: transform 0.5s;
}
.product-card:hover .product-img {
  transform: scale(1.05);
}
.badge-overlay {
  z-index: 10;
}
.btn-wishlist {
  z-index: 10;
  background: #fff;
  border: none;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  color: var(--bs-secondary);
  transition: 0.2s;
}
.btn-wishlist:hover {
  color: var(--bs-danger);
  transform: scale(1.1);
}
.color-swatch.active {
  box-shadow: 0 0 0 2px var(--bs-primary) !important;
}
.max-w-450 {
  max-width: 450px;
}
.max-w-500 {
  max-width: 500px;
}
.credit-card-visual {
  background: linear-gradient(135deg, #1e293b 0, #0f172a 100%);
  aspect-ratio: 1.58/1;
}
.credit-card-visual::before {
  content: "";
  position: absolute;
  top: -20%;
  right: -10%;
  width: 150px;
  height: 150px;
  background: rgba(255, 255, 255, 0.03);
  border-radius: 50%;
}
.chip-icon {
  width: 40px;
  height: 30px;
  background: linear-gradient(135deg, #d4af37 0, #f1c40f 100%);
  border-radius: 4px;
}
.card-number {
  letter-spacing: 2px;
}
.action-icon-circle {
  width: 48px;
  height: 48px;
  transition: transform 0.2s;
}
.action-icon-circle:hover {
  transform: translateY(-3px);
}
.transaction-icon {
  width: 36px;
  height: 36px;
}
.weather-card-header {
  background: linear-gradient(180deg, #4facfe 0, #00f2fe 100%);
}
.weather-card-header::after {
  content: "";
  position: absolute;
  top: -20px;
  right: -20px;
  width: 120px;
  height: 120px;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 50%;
  filter: blur(20px);
}
.weather-icon-lg {
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.1));
}
.forecast-item {
  min-width: 80px;
  transition: 0.2s;
}
.forecast-item:hover {
  background: var(--bs-tertiary-bg);
  transform: translateY(-3px);
}
.forecast-scroll {
  scrollbar-width: none;
}
.forecast-scroll::-webkit-scrollbar {
  display: none;
}
.category-item {
  padding: 12px;
  border-radius: 12px;
  background-color: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  transition: 0.2s;
}
.category-item:hover {
  background-color: var(--bs-tertiary-bg);
  transform: translateY(-2px);
}
.category-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}

:root {
  /* modified: user directive -- match Tabler's own reference font exactly. Tabler's compiled
     CSS drives body/heading font-family off its own --tblr-font-sans-serif ("Inter Var, Inter,
     ..."), not these --bs-* variables, so this block was already inert for the actual rendered
     font -- kept in sync with Inter anyway so it isn't misleading if anything ever reads it. */
  --bs-font-sans-serif: "Inter Var", Inter, system-ui, sans-serif;
  --bs-font-monospace: "JetBrains Mono", "SFMono-Regular", Menlo, monospace;
  --bs-body-font-family: var(--bs-font-sans-serif);
  --bs-body-font-weight: 400;
  --bs-font-family: "Inter Var", Inter, system-ui, sans-serif;
}

*, *::before, *::after {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.brand-primary {
  color: var(--bs-primary);
}
.brand-secondary {
  color: var(--bs-warning);
}

/* modified: FLX-1107 -- fluid instead of a fixed 36px cap. | was: max-height 36px, width auto
   Measured, not estimated: the mark is 300x75 in every version, but the ink inside it moved.
   The old wordmark filled 68px of the 75px canvas; the new lockup is a 47px wordmark over an
   11px tagline. At a 36px cap that renders the wordmark at 22.6px where it used to be 32.6px --
   31% smaller for the same rule -- and the tagline at 5.3px, which is not readable at any
   viewing distance. The cap was sized for a mark that no longer exists.
   max-width keeps it inside whatever narrow container it lands in, and height:auto lets the
   aspect ratio do the work rather than a second hard number that has to agree with the first. */
.brand-logo {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: clamp(2.25rem, 3.6vw, 3.5rem);
}

/* added: user directive -- remove the 36px cap specifically on login/forgot/reset-password
   (.flx-auth-logo wraps the logo on all three, same pattern). .brand-logo is used app-wide
   (sidebar, footer, etc.) so the base rule above can't just be dropped without checking every
   other call site -- scoped override instead. */
.flx-auth-logo .brand-logo {
  max-height: none;
}

.brand-logo-mini {
  width: 36px;
  height: 36px;
  object-fit: contain;
}

.brand-logo-light { display: block; }
.brand-logo-dark  { display: none;  }
[data-bs-theme="dark"] .brand-logo-light { display: none  !important; }
[data-bs-theme="dark"] .brand-logo-dark  { display: block !important; }

.link-muted {
  color: #6b7280;
  text-decoration: none;
}
.link-muted:hover {
  color: var(--flx-steel);
  text-decoration: underline;
}

.bg-body-tertiary {
  --bs-bg-opacity: 1;
  background-color: var(--flx-stone) !important;
}
.alert-success {
  color: var(--bs-light);
  padding: 10px;
  margin-bottom: 2em;
  border-radius: 6px;
  font-size: small;
  background: var(--bs-success);
}
.alert-error {
  color: var(--bs-light);
  padding: 10px;
  margin-bottom: 2em;
  border-radius: 6px;
  font-size: small;
  background: var(--bs-danger);
}
/* added: FLX-714 -- this app defines its own standalone alert blocks (see the FLX-698 note: they
   render correctly without Bootstrap's .alert base class), and there was no warning variant --
   only success and error. The invite flow now has a genuine third outcome that is neither: the
   user WAS created but the invitation email did not go out. Forcing that into .alert-error would
   tell an admin the operation failed and invite a retry that then collides with the membership
   that already exists. Uses --flx-amber, the palette's warning colour, with dark text because
   amber cannot carry white text at an accessible contrast ratio. */
.alert-warning {
  color: var(--flx-navy);
  padding: 10px;
  margin-bottom: 2em;
  border-radius: 6px;
  font-size: small;
  background: var(--flx-amber);
}

/* added: user directive -- status badges (Active/Inactive/Pending/etc, ~19 files, every one using
   Tabler's compiled `.badge` + a solid `.bg-success`/`.bg-danger`/`.bg-warning` utility) should
   read as a soft tinted pill with a small colored dot, not a solid saturated chip -- reference
   screenshot showed a light-green pill, dark-green text, dot marker. Rather than touching markup
   in ~19 files, this overrides the 3 utility classes globally wherever they're used inside a
   .badge: `--bs-success-bg-subtle`/`--bs-success-text-emphasis` (and the warning/danger
   equivalents) are Bootstrap 5.3's own real subtle/emphasis pair, already defined as this file's
   own CSS custom properties above (:root, ~line 29) matching the official 7-color palette exactly
   -- reused here, not invented. `.badge` is already `display:inline-flex` with a `gap` in Tabler's
   compiled CSS, so the ::before dot drops in as a flex item with no extra layout rules needed. */
.badge.bg-success, .badge.bg-danger, .badge.bg-warning {
  font-weight: 600;
  border: none;
}
.badge.bg-success { background-color: var(--bs-success-bg-subtle) !important; color: var(--bs-success-text-emphasis) !important; }
.badge.bg-danger  { background-color: var(--bs-danger-bg-subtle)  !important; color: var(--bs-danger-text-emphasis)  !important; }
.badge.bg-warning { background-color: var(--bs-warning-bg-subtle) !important; color: var(--bs-warning-text-emphasis) !important; }
.badge.bg-success::before, .badge.bg-danger::before, .badge.bg-warning::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background-color: currentColor;
  flex-shrink: 0;
}
/* added: FLX-688 -- toolbar search/filter controls in a list-page toolbar.
   modified: FLX-729 -- the inline style="max-width:220px" copies this replaced are now gone from
   the pages this ticket touched, and the class is also valid on a Tabler .input-icon wrapper
   (the input inside is width:100%, so the cap lands identically either way). */
.flx-toolbar-search {
  max-width: 240px;
}
/* added: FLX-729 -- a tenant/scope <select> in a toolbar. Without a floor it collapses to the
   width of the shortest option and the real tenant names clip; without a ceiling one long
   tenant name pushes the search box off the row. */
.flx-toolbar-select {
  min-width: 12.5rem;
  max-width: 16rem;
}

.max-w-350 {
  max-width: 350px;
}
.material-icons {
  font-size: 14px;
}
.dataTables_length {
  display: flex !important;
  justify-content: flex-end;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
}
.dataTables_length label {
  display: flex !important;
  align-items: center;
  gap: 6px;
  margin: 0;
  white-space: nowrap;
}
.dataTables_length select {
  width: auto !important;
  display: inline-block;
}
.sai {
  min-width: 40px;
  font-size: large;
}
.activity-text,
.text-x-small {
  font-size: x-small;
}

/* fixed: FLX-1217 -- scoped to the Asset Library modal on events-edit.php, which is the grid this
   rule was written for. The Media Library page (modules/website/media.php) reuses id="mediaGrid",
   and an ID outranks .flx-media-grid { display:grid } below, so the library rendered as a flex-wrap
   box with content-sized tiles inside a 60vh scroll box: a large image became one full-width tile.
   FLX-867 and FLX-1092 treated the symptom; this was the cause. | was: #mediaGrid { ... } */
#flowlogicxMediaModal #mediaGrid {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-start;
    align-content: flex-start;
    width: 100%;
    min-height: 50vh;
    max-height: 60vh;
    overflow-y: auto;
    overflow-x: hidden;
    box-sizing: border-box;
    padding: 5px;
    transition: opacity 0.18s ease;
}

.media-item {
    flex: 0 0 calc(20% - 10px);
    margin: 5px;
    aspect-ratio: 1 / 1;
    border: 1px solid #ddd;
    border-radius: 6px;
    overflow: hidden;
    cursor: pointer;
    background: #f8f9fa;
    position: relative;
}

.media-item img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    pointer-events: auto;
    opacity: 0;
    transition: opacity 0.3s ease;
}
.media-item img.img-loaded {
    opacity: 1;
}

.media-item video {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
}

.media-global-badge {
    position: absolute;
    top: 5px;
    right: 5px;
    background: rgba(13, 110, 253, 0.82);
    color: #fff;
    font-size: 9px;
    font-weight: 600;
    letter-spacing: 0.04em;
    padding: 2px 5px;
    border-radius: 3px;
    pointer-events: none;
    z-index: 3;
    text-transform: uppercase;
}

.media-play-badge {
    position: absolute;
    bottom: 6px;
    left: 6px;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-size: 11px;
    padding: 2px 6px;
    border-radius: 4px;
    pointer-events: none;
    z-index: 2;
}

.media-file-placeholder {
    position: absolute;
    top: 0; left: 0; right: 0; bottom: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    background: #f0f2f5;
    color: #6c757d;
    padding: 10px;
    text-align: center;
}

.media-file-placeholder i {
    font-size: 30px;
}

.media-file-placeholder span {
    font-size: 10px;
    line-height: 1.3;
    word-break: break-all;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.media-load-sentinel {
    width: 100%;
    height: 1px;
    flex-basis: 100%;
}

.media-item:hover {
    transform: scale(1.03);
    box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
}

.media-item.selected {
    outline: 3px solid #0d6efd;
    outline-offset: -3px;
}

.media-overlay {
    position: absolute;
    top: 6px;
    right: 6px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    background: rgba(0, 0, 0, 0.5);
    padding: 6px;
    border-radius: 6px;
    opacity: 0;
    transition: opacity 0.2s ease;
    pointer-events: none;
    z-index: 4;
}

.media-item:hover .media-overlay {
    opacity: 1;
}

.media-overlay button {
    background: transparent;
    border: none;
    color: #fff;
    font-size: 14px;
    cursor: pointer;
    transition: transform 0.1s ease;
    pointer-events: auto;
}

.media-overlay button:hover {
    transform: scale(1.1);
    color: var(--flx-amber);
}

.media-info {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 5px 7px;
    background: rgba(0, 0, 0, 0.62);
    color: #fff;
    font-size: 10px;
    line-height: 1.4;
    opacity: 0;
    transform: translateY(4px);
    transition: opacity 0.18s ease, transform 0.18s ease;
    pointer-events: none;
    z-index: 3;
}

.media-item:hover .media-info {
    opacity: 1;
    transform: translateY(0);
}

.select-checkbox {
    display: none;
}

.media-item.selected::after {
    content: '\2713';
    position: absolute;
    top: 6px;
    left: 6px;
    width: 22px;
    height: 22px;
    background: var(--flx-amber);
    color: #fff;
    font-size: 13px;
    font-weight: 700;
    border-radius: 50%;
    text-align: center;
    line-height: 22px;
    z-index: 5;
    pointer-events: none;
}

.media-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 16px;
    font-size: 14px;
    color: #333;
}

@media (max-width: 992px) {
  .media-item { flex: 0 0 calc(25% - 10px); }
}
@media (max-width: 768px) {
  .media-item { flex: 0 0 calc(33.33% - 10px); }
}
@media (max-width: 576px) {
  .media-item { flex: 0 0 calc(50% - 10px); }
}

#sidebar .nav-link {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  /* added: sidebar icon/text alignment fix -- every nav-link (leaf or collapsible-header)
     is now the same flex row, so icon vertical-centering is identical everywhere instead of
     depending on whether that particular item happened to add d-flex align-items-center
     itself. Fixes a real, user-reported misalignment between Dashboard and every collapsible
     section header (Driving School/Website Management/dynamic Modules items like Events). */
  display: flex;
  align-items: center;
  /* fixed: user-reported bug -- expand/collapse "isn't smooth". This element's own width/padding/
     margin/font-size all switch abruptly between the expanded and icon-rail states (see the
     body.sidebar-collapsed #sidebar-menu .nav-link overrides further below) with no transition of
     their own, so every row snapped into its new size instantly while #sidebar's width was still
     mid-slide. Declared once here (unconditional, so it applies whichever direction the state is
     changing) rather than duplicated in the media-query block. */
  /* modified: FLX-764 -- background-color/box-shadow added to this existing list rather than
     declared in a second, more specific rule for group headers. `transition` is a shorthand, so a
     later `#sidebar .nav-link[data-bs-toggle="collapse"]{transition:...}` would have REPLACED all
     five properties above and silently undone the smoothness fix this comment describes. Adding
     them here also gives every nav row -- top level, sub-item and flyout alike -- one eased hover. */
  transition: width 0.2s ease, padding 0.2s ease, margin 0.2s ease, font-size 0.15s ease,
    border-radius 0.2s ease, background-color 0.22s ease-in-out, box-shadow 0.22s ease-in-out;
}
#sidebar .nav-link > i:first-child,
#sidebar .nav-link > svg:first-child {
  /* fa-fw already sizes to 1.28571429em, but not every icon in nav_side.php/nav_renderer.php
     carries that class -- this is the same effect applied uniformly at the container level, so
     every icon occupies an identical-width column regardless of glyph (fa-tachometer is a
     visually narrower glyph than fa-car/fa-globe/fa-calendar at the same font-size, which is
     why text columns didn't line up even after the flex fix alone). */
  flex-shrink: 0;
  width: 1.25rem;
  text-align: center;
  transition: font-size 0.15s ease;
}
#sidebar .nav-link .arrow {
  /* was implicitly positioned by justify-content-between on individual .nav-link elements
     (now removed from markup in favor of this one shared rule) */
  margin-left: auto;
}

#sidebar .sidebar-header {
  margin-bottom: 1rem !important;
}

/* added: FLX-679 -- mobile hamburger (.navbar-toggler, Tabler's real mobile nav-collapse
   trigger) had no positioning at all, so it rendered as a plain block child stacked ABOVE the
   logo instead of beside it -- .container-fluid's default flex layout couldn't help because
   .navbar-toggler and the logo's wrapper (.w-100, deliberately full-width so the collapsed
   menu content below it spans the sidebar) are siblings, and w-100 by definition claims the
   whole row, pushing the toggler onto its own line. Absolute-positioning the toggler to the
   header's own top-right corner sidesteps that entirely, regardless of how tall the collapsed
   menu becomes once opened -- reserved padding-right on sidebar-header keeps the logo link from
   ever underlapping it. */
@media (max-width: 991.98px) {
  /* fixed: user-reported bug, round 2 -- the first attempt at this fix (below) still lost to
     the base (unconditional) `#sidebar { max-width: 270px; }` rule further down this file
     (~line 2113): identical specificity (`#sidebar` selector, both sides), neither side
     !important, and CSS resolves an exact specificity tie by SOURCE ORDER, not by which media
     query is currently active -- the base rule sits later in the file, so it kept winning at
     every viewport width regardless of this media query matching. Confirmed live via the user's
     own DevTools screenshot (this rule shown struck through, the base rule shown active).
     `!important` makes this win unconditionally, the way a media-query override actually needs
     to for a property also set unconditionally elsewhere. Mobile top bar was capped at 270px
     wide (the desktop expanded sidebar's own max-width, never scoped to desktop only), leaving a
     dead white gap next to the hamburger toggle. At this breakpoint #sidebar is repurposed as a
     full-width top bar, not a narrow sidebar -- "tablets and up" (>=992px, this file's existing
     desktop breakpoint) keep the 270px cap unaffected. */
  #sidebar {
    max-width: none !important;
  }
  #sidebar .container-fluid {
    position: relative;
  }
  #sidebar .navbar-toggler {
    /* fixed: user-reported bug, same root cause as #sidebar's new overflow-y:auto below --
       position:absolute was relative to .container-fluid, which now scrolls internally along
       with the rest of #sidebar's content once the menu expands past one screen. Fixed
       (viewport-relative) instead, so the toggle stays reachable to close the menu at any
       scroll position. */
    position: fixed;
    top: 14px;
    right: 16px;
    z-index: 3;
  }
  #sidebar .sidebar-header {
    padding-right: 3.5rem !important;
  }

  /* fixed: user directive found a real bug -- position:sticky stayed pinned only partway down
     the scroll (until roughly "Revenue This Month"), then scrolled away with the page, leaving
     only .flx-topbar behind. #sidebar and .page are siblings directly under <body> with no
     unusual overflow/height constraints anywhere in Tabler's or this app's own CSS (checked
     both directly) -- sticky's own containing-block rules are exactly the kind of thing that
     can misbehave in ways that are hard to fully pin down across browsers/engines. position:fixed
     sidesteps the whole problem class: always pinned to the viewport, never dependent on a
     parent's remaining scrollable height. .page gets matching padding-top (below) so real
     content never renders underneath the now out-of-flow fixed bar. */
  #sidebar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1030;
    /* fixed: user-reported bug -- the expanded mobile nav menu (#sidebar-menu) had no way to
       scroll to items past the viewport bottom (cut off after "Error Log", rest of Driving
       School/Website Management unreachable). A position:fixed element is removed from the
       page's own scroll flow entirely -- the collapsed header row fit fine, but once the full
       nav list expands (taller than one screen), there's no mechanism to reach the rest of it
       without the fixed element scrolling internally. */
    max-height: 100dvh;
    overflow-y: auto;
  }
  /* fixed: user directive found a visible gap below the fixed mobile bars -- was
     sidebarHeight + a hardcoded 60px assumption for .flx-topbar's real height, which can be
     slightly off from the literal. Now uses --flx-mobile-fixed-bars-height, both bars' real
     measured heights summed in JS (body_end_plugins.php), no assumption either way. */
  .page {
    padding-top: var(--flx-mobile-fixed-bars-height, 0px);
  }
  .flx-topbar {
    position: fixed;
    top: var(--flx-topbar-sticky-offset, 0px);
    left: 0;
    right: 0;
    z-index: 1020;
  }
}

/* .flx-topbar is sticky on desktop (>=992px, see the mobile override above for <992px, which
   uses position:fixed instead). The vertical sidebar there is its own fixed side column, not
   stacked above .page-wrapper, so a simple top:0 sticky works correctly and doesn't share
   mobile's bug. */
.flx-topbar {
  position: sticky;
  top: var(--flx-topbar-sticky-offset, 0px);
  z-index: 1020;
}

/* modified: user directive -- brought back down to 18px (was 32px per an earlier design-review
   pass on the locked artifact; too large in the real rendered header once judged live). Button
   hit-target itself stays 44px regardless of icon size. */
.flx-collapse-toggle {
  width: 44px;
  height: 44px;
  border-radius: 8px;
  border: none;
  background: transparent;
  color: #7c8ba1;
}
.flx-collapse-toggle i {
  font-size: 18px;
}
.flx-collapse-toggle:hover {
  background-color: rgba(255,255,255,0.08);
  color: #fff;
}

.flx-section-label {
  font-size: .68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: #64748b !important;
  padding: 1rem .75rem .4rem !important;
  margin: 0 !important;
}

/* Tabler's .form-footer defaults to margin-top:2rem, stacking on top of the recaptcha
   wrapper's own mb-3 above it and making the gap above the Login button roughly 3x the gap
   between every other field -- confirmed by removing it live. Only login.php uses
   .form-footer today, so this is scoped to its own form rather than overridden globally. */
#loginForm .form-footer {
  margin-top: 0;
}

/* Quick Create sits on the sidebar's own navy background, which since FLX-678 is also
   --tblr-primary (the app-wide brand primary) -- .btn-primary here would render navy-on-navy
   and effectively disappear. Give it the accent blue used for active nav state instead. */
#sidebar .btn-primary {
  background-color: var(--flx-blue) !important;
  border-color: var(--flx-blue) !important;
}

@media (min-width: 992px) {
  /* modified: FLX-677/678 -- #sidebar is now a Tabler `.navbar-vertical` (fixed position, own
     width), not a Bootstrap grid column. Tabler's own compiled CSS offsets the content column via
     `.navbar-vertical.navbar-expand-lg ~ .page { padding-left: 15rem }` -- a general-sibling
     selector, which only works now that <aside id="sidebar"> and <div class="page"> are true
     siblings (see layout_top.php's FLX-678 fix -- the aside used to be nested inside .page,
     which meant this offset never applied at all and content rendered underneath the fixed
     sidebar). Only the icon-only "collapsed" override below still needs custom CSS, since Tabler
     has no built-in collapsed-to-icon-rail mode, and it must target .page's own padding-left the
     same way, not a margin-left on .navbar/.page-wrapper (those are children of .page now, not
     its siblings, so a margin-left rule on them never matched either). Breakpoint moved
     768px->992px to match navbar-expand-lg (below that width the sidebar is already in its own
     mobile-collapse mode, where this desktop icon-rail feature doesn't apply). */
  #sidebar {
    transition: width 0.25s ease;
  }

  /* added: user directive -- keeps .page's offset in sync with #sidebar's own new max-width
     (270px, see the #sidebar rule itself further below) instead of Tabler's compiled default of
     15rem/240px, which no longer matches once #sidebar's width changed -- the mismatch is exactly
     what left the dead gap / content-overlap the user reported.
     fixed: user-reported bug -- expand/collapse "isn't smooth". #sidebar itself already
     transitioned its own `width` (below), but .page's `padding-left` had no transition at all, so
     the actual page content snapped to its new position instantly while the rail was still
     halfway through its own 0.25s slide -- the two visibly fell out of sync every time. Same
     0.25s/ease here keeps them moving together. */
  .navbar-expand-lg.navbar-vertical ~ .page {
    padding-left: 270px !important;
    transition: padding-left 0.25s ease;
  }

  /* #sidebar itself now supplies the left/right gutter directly (its own `padding: 0 1em`) --
     Bootstrap's default .container-fluid gutter would stack on top of that and double the inset,
     so it's zeroed here for both sidebar states, not just collapsed. */
  #sidebar .container-fluid {
    padding-left: 0 !important;
    padding-right: 0 !important;
  }

  /* fixed: user directive -- #sidebar's own padding:0 1em (added for the expanded-mode gutter
     fix) is only meant for that state. Collapsed relies entirely on flexbox centering the 40px
     icon buttons within the full 72px rail (justify-content:center on .nav-item, see below) --
     zeroing this outer padding here gives that centering math the full width to work with, same
     net 16px-per-side result, explicitly by state rather than left applying unconditionally. */
  body.sidebar-collapsed #sidebar {
    width: 72px !important;
    padding: 0 !important;
  }

  body.sidebar-collapsed .navbar-expand-lg.navbar-vertical ~ .page {
    padding-left: 72px !important;
  }

  body.sidebar-collapsed .sidebar-logo-full { display: none !important; }
  body.sidebar-collapsed .sidebar-logo-mini { display: flex !important; }

  /* fixed: user-reported bug (round 2) -- the first pass tried to hit centering by zeroing
     .container-fluid's padding and relying on margin:0 auto on a fixed-width .nav-link. That's
     fragile: Bootstrap's own gutter padding can win back on some pages, and if the link's own
     width doesn't evenly divide into whatever width is actually left, auto margins resolve to 0
     and the icon jams flush left instead of centering (exactly the bug reported: icons "pushed
     too far to the left", active state "not centered"). Centering now happens via flexbox on
     every ancestor in the chain (`align-items:center` all the way down), which centers a child
     correctly regardless of the parent's own padding/width -- no arithmetic to get wrong.
     `.container-fluid`'s own padding is zeroed unconditionally above (not repeated here) now that
     #sidebar itself supplies the real gutter via its own padding in both states. */
  body.sidebar-collapsed #sidebar #sidebar-menu,
  body.sidebar-collapsed #sidebar .nav.flex-column,
  body.sidebar-collapsed #sidebar > .container-fluid > .w-100 {
    padding-left: 0 !important;
    padding-right: 0 !important;
  }
  /* fixed: user-requested flyout feature -- every icon-rail-only rule below is now scoped to
     `#sidebar-menu` (the original nav container) instead of `#sidebar` (the whole aside,
     including the new #sidebarFlyout panel added as its sibling). #sidebarFlyout holds a group's
     real <ul> node moved out of #sidebar-menu on click (see body_end_plugins.php) -- scoping
     these selectors this way means anything moved into the flyout automatically stops matching
     them and reverts to looking like a normal expanded nav row, with zero extra override rules
     needed to undo font-size:0/display:none/etc for that one context. */
  /* fixed: user-reported bug -- "icons not vertically spaced consistently". Was margin-bottom:8px
     on the individual .nav-link (below), which some nav-items apply directly and others (the
     Admin Settings block's <li><div><a>...) apply one DOM level deeper inside an extra wrapper
     div -- normal (non-flex) margin-collapsing rules govern that inner case, flex-item margin
     rules govern the direct case, and relying on two different collapsing behaviors to land on
     the same visual gap is exactly the kind of fragile setup that reads as "inconsistent" the
     moment either one doesn't collapse quite the way the other does. `gap` on this shared
     flex-column parent sets the spacing once, structurally, with no margin-collapsing involved
     at all -- every child gets the identical gap regardless of its own internal markup. */
  body.sidebar-collapsed #sidebar-menu .nav.flex-column {
    display: flex !important;
    align-items: center !important;
    gap: 8px;
  }
  /* fixed: user-reported bug -- this rule's `display:flex !important` has higher specificity
     than Bootstrap's own `.d-none{display:none!important}`, so it was overriding it and reviving
     several permanently-hidden placeholder items (Lifecycle/Analytics/Data Library/Settings/
     Get Help -- all deliberately `d-none` in nav_side.php, never meant to render at all) as a
     visibly distorted, inset cluster of icons in the collapsed rail. `:not(.d-none)` keeps this
     rule from ever touching an item that's supposed to stay hidden. */
  body.sidebar-collapsed #sidebar-menu .nav-item:not(.d-none) {
    width: 100%;
    display: flex !important;
    justify-content: center !important;
  }

  body.sidebar-collapsed #sidebar .sidebar-header {
    flex-direction: column !important;
    align-items: center !important;
    justify-content: center !important;
    padding-left: 0 !important;
    padding-right: 0 !important;
    gap: 0.4rem;
  }
  body.sidebar-collapsed #sidebar-toggle-btn {
    margin-left: 0 !important;
  }

  /* modified: round 2 -- 44px in a 64px rail left only 10px of gutter per side (and less once
     Bootstrap's container padding fought it, see above), reading as "cramped"/"jammed" against
     the Tabler reference's generously padded 40px icon buttons in a 72px rail (16px gutter per
     side, matching Tabler's own default nav-link padding rhythm). Centering is now the parent
     flexbox's job (see .nav.flex-column/.nav-item above), not this element's own margin -- so
     no margin-left/right here at all, just vertical spacing between rows. */
  body.sidebar-collapsed #sidebar-menu .nav-link {
    font-size: 0 !important;
    display: flex !important;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0 !important;
    margin: 0;
    border-radius: 6px;
  }
  body.sidebar-collapsed #sidebar-menu .nav-link i,
  body.sidebar-collapsed #sidebar-menu .nav-link svg {
    font-size: 1.1rem !important;
    margin: 0 !important;
  }

  body.sidebar-collapsed #sidebar-menu .nav-link .arrow { display: none !important; }

  /* fixed: round 3 -- round 2's fix relied on the `.nav-tree` class being present on every
     submenu <ul>, but nav_renderer.php's dynamic Modules submenus (Events etc.) had a structural
     bug (a stray nested <ul> from calling itself recursively without threading its wrapper
     classes down) that produced a genuinely different DOM shape from the hardcoded Driving
     School/Website Management blocks -- confirmed live via a user screenshot showing exactly
     those dynamic submenus still rendering, narrower and indented, never hidden. The markup bug
     is fixed at the source (nav_renderer.php, nav_side.php's Admin Settings block), but this rule
     is now also structural rather than class-name-dependent: every `.collapse` panel inside the
     sidebar IS an accordion submenu (id="collapse-1"/"collapse-creator"/"collapse-drivingschool"/
     "collapse-website"/"collapse-booking"/dynamic "menu{id}") except `#sidebar-menu` itself (the
     outer mobile navbar-toggler target, explicitly excluded) -- hiding all of them wholesale here
     doesn't depend on any of their internal markup/classes matching a convention, so a future
     nav block with a slightly different internal shape can't reintroduce this bug. Any accordion
     section left open before collapsing (nothing re-collapses them on page load, only the toggle
     button's own click handler does) would otherwise show a second, indented icon column next to
     the top-level one -- the Tabler reference rail shows top-level icons only, no nested children
     inline. */
  /* fixed: user-reported bug -- "icon shifts left" + "icons below get pushed down and snap back"
     on click. Root cause: Bootstrap's Collapse.hide()/show() TEMPORARILY REMOVES the `.collapse`
     class itself (swapping it for `.collapsing` mid-transition, then restoring `.collapse` once
     done) -- confirmed by reading bootstrap.bundle.min.js's own Collapse source. This selector
     only ever matched `.collapse`, so for that ~350ms window the panel stopped matching it
     entirely and briefly became a real, visible, layout-participating block again (Bootstrap's
     own `.collapsing{height:...}`, not display:none) sitting beside the toggle icon inside the
     same flex `.nav-item` row -- shoving the icon left to make room, and however much height it
     picked up pushed every `.nav-item` below it down until the transition finished. Adding
     `.collapsing` closes that gap regardless of which class Bootstrap's JS has the element in at
     any given instant. (Bootstrap's own click handler is now also stopped from running at all in
     this state via `e.stopPropagation()` in body_end_plugins.php, which should prevent this from
     triggering in the first place -- this rule is the belt-and-suspenders backstop.) */
  body.sidebar-collapsed #sidebar-menu .collapse,
  body.sidebar-collapsed #sidebar-menu .collapsing { display: none !important; }

  /* scoped to #sidebar-menu (not #sidebar) so this doesn't also hide #sidebarFlyoutTitle, its
     own h3 living outside #sidebar-menu in the new flyout panel. */
  body.sidebar-collapsed #sidebar-menu h3 { display: none !important; }

  body.sidebar-collapsed #sidebar-menu .sidebar-badge { display: none !important; }

  /* fixed: round 2 -- reverted the custom left-edge accent bar from round 1. Compared side by
     side against the Tabler reference, Tabler has no separate rail indicator at all -- the
     current page is shown purely by the centered rounded-rect `.active` background already
     defined earlier in this file (#sidebar .nav-link.active), which now centers correctly inside
     the icon button since centering is flexbox-driven (see above), not a fixed-width/margin:auto
     calculation. A group whose child page is current is suppressed here too (see the
     `body.sidebar-collapsed #sidebar .nav-item.active::after` rule further below) -- the
     collapsed rail shows no group-level indicator at all, matching the Tabler reference. */

  body.sidebar-collapsed #sidebar .btn-primary {
    font-size: 0 !important;
    width: 40px;
    height: 40px;
    padding: 0 !important;
    border-radius: 10px !important;
    margin: 0.5rem auto !important;
  }
  body.sidebar-collapsed #sidebar .btn-primary svg {
    margin: 0 !important;
    font-size: 1rem !important;
  }
}

/* added: user-requested flyout feature -- secondary panel beside the collapsed icon rail for a
   group's real children (Creator Admin/Driving School/Admin Settings/dynamic Modules), which the
   rail itself has no room to show inline. Markup: nav_side.php (#sidebarFlyout, a sibling of
   #sidebar-menu inside <aside id="sidebar">). Behavior: body_end_plugins.php moves the clicked
   group's real <ul> node into #sidebarFlyoutBody (not a clone) and back out again on close.
   Faded/slid via opacity+transform (not display:none/block) so it animates in and out instead of
   popping -- matches the smoothness fix applied to the rail itself just above. Left offset (72px)
   matches the collapsed rail's own width; nothing here applies unless body.sidebar-collapsed. */
.sidebar-flyout {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 72px;
  width: 240px;
  background-color: var(--flx-navy);
  border-left: 1px solid rgba(255,255,255,0.08);
  box-shadow: 6px 0 16px rgba(0,0,0,0.25);
  z-index: 1029;
  padding: 1.25rem 1rem;
  overflow-y: auto;
  opacity: 0;
  visibility: hidden;
  transform: translateX(-8px);
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0s linear 0.15s;
  pointer-events: none;
}
body.sidebar-collapsed .sidebar-flyout.show {
  opacity: 1;
  visibility: visible;
  transform: translateX(0);
  transition: opacity 0.15s ease, transform 0.15s ease;
  pointer-events: auto;
}
.sidebar-flyout-title {
  font-size: .68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: #64748b;
  padding: 0 .5rem .75rem;
  margin: 0;
}
/* marks which rail icon the open flyout belongs to -- a light outline rather than the solid
   `.active` pill fill, so it reads as "this group's panel is open" without being confused with
   ".active" (current page) or ".nav-parent-active"/native active-group styling. */
body.sidebar-collapsed #sidebar-menu .nav-link.flyout-open {
  background-color: rgba(255,255,255,0.08);
  box-shadow: inset 0 0 0 2px var(--flx-blue);
}

#sidebar .arrow {
  display: inline-block;
  transition: transform 0.2s ease;
}
#sidebar .nav-link[aria-expanded="true"] .arrow {
  transform: rotate(90deg);
}

/* redesigned: user directive -- do not invent a custom sub-nav treatment, use Tabler's own
   real pattern instead of the previous dashed-connector-line design (never a Tabler pattern to
   begin with). Tabler's vendored tabler.min.css already ships the real mechanism for "this
   group contains the current page": `.navbar-expand-lg .nav-item.active` (position:relative)
   plus its own `::after` (border-left, top:0, bottom:0 in a `.navbar-vertical` context) -- see
   the `#sidebar` custom-property override further below that recolors it (Tabler's default
   pulls from `--tblr-primary`, which this app repoints to navy for its own unrelated reasons --
   navy-on-navy would be invisible against the sidebar's own navy background). All that's needed
   here is plain, undecorated child rows -- no border/dash graphics of our own. */
#sidebar .nav-tree .nav-link {
  font-size: 0.875rem;
}

/* added: FLX-764 -- ONE LEFT EDGE for the whole sidebar. Three different offsets existed, all
   measured from #sidebar's content edge: the module switcher at 0 (FLX-762 dropped its mx-3), the
   tenant badge at 16px (mx-3), and every group sub-list at 16px on top of its link padding (ms-3 on
   the nav-tree <ul>). Both utility classes are now gone from the markup, and this pins the
   remaining variable -- .nav-link's horizontal padding, which otherwise comes from Tabler's
   --tblr-nav-link-padding-x -- to the same 0.75rem .sidebar-module-btn and the switcher rows use.
   Top-level links, sub-nav links, the tenant badge and the module switcher now share one left edge
   and, via the existing #sidebar .nav-link > i:first-child column, one icon centre line. */
#sidebar .nav-link {
  padding-left: 0.75rem;
  padding-right: 0.75rem;
}

/* added: FLX-764 -- an expanded group renders exactly like the open module switcher: one outline
   enclosing header and children, a lighter header, full-bleed rows, brand-blue active row with a
   white rail. All six hardcoded groups and renderMenu()'s dynamic output share the same shape
   (<li class="nav-item"> > <a class="nav-link" data-bs-toggle="collapse"> + <div class="collapse">),
   so a single :has() selector covers every group in the sidebar, hardcoded or dynamic.
   The outline is deliberately bound to the EXPANDED state -- a border around all six collapsed
   headers at once would be far heavier than the nav it is meant to organise. */
/* fixed: FLX-764 round 2 -- user-reported: "the wrap isn't showing and there's a slight delay;
   after the menu is fully expanded the wrap appears; feels glitchy." Three separate causes, all of
   them mine:
   1. TIMING. Bootstrap's Collapse does not hold `.show` during the animation -- it swaps the
      element to `.collapsing` for the full duration and applies `.show` only on transitionend. So
      keying the outline purely on `.collapse.show` GUARANTEED it could not appear until the motion
      had already finished, then popped in. `.collapsing` is matched here too, so the outline is
      present from the first frame in both directions. This is the same class-swap trap already
      documented for the collapsed rail higher up this file -- second time it has bitten.
   2. LAYOUT SHIFT. The outline was a real `border`, which adds 2px to the element's box the instant
      it appears -- a jump no easing can hide, mid-height-animation. It is now an inset box-shadow:
      paint-only, so it cross-fades against the height motion instead of resizing anything. The
      header divider is an inset shadow for the same reason.
   3. NO TRANSITION AT ALL. The base state now declares a transparent shadow so there is something
      to interpolate FROM; a property that only exists in the open state cannot animate.
   CRITICAL SELECTOR NOTE: Bootstrap does not merely ADD `.collapsing` -- it REMOVES `.collapse`
   first (`classList.remove('collapse'); classList.add('collapsing')`) and restores it on
   transitionend. So `:has(> .collapse)` matches NOTHING for the whole animation. Every rule below
   therefore matches `:is(.collapse, .collapsing)`, never `.collapse` alone -- otherwise the radius,
   the clip and the transition itself would all blink out for exactly the 260ms they are needed. */
#sidebar .nav-item:has(> :is(.collapse, .collapsing)) {
  border-radius: 0.5em;
  overflow: hidden;
  margin-bottom: 0.15rem;
  box-shadow: inset 0 0 0 1px transparent;
  transition: box-shadow 0.22s ease-in-out;
}
#sidebar .nav-item:has(> :is(.collapse.show, .collapsing)) {
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 25%);
}
/* renderMenu() recurses, so a dynamic module with two levels of children emits a group inside a
   group. Outlining both would nest boxes inside boxes; only the outermost group is drawn. Cheap
   insurance -- no tenant menu is known to nest this deep today, but the renderer allows it and the
   failure mode is ugly rather than obvious. */
#sidebar .nav-tree .nav-item:has(> :is(.collapse, .collapsing)) {
  box-shadow: none;
  border-radius: 0;
  overflow: visible;
  margin-bottom: 0;
}
/* The header needs no :has() -- Bootstrap flips aria-expanded at click time, before the animation
   starts, so this is the one part that was always correctly timed. Its transition is declared on
   the shared #sidebar .nav-link rule far above rather than here: that rule already carries a
   width/padding/margin/font-size/border-radius transition from an earlier "expand/collapse isn't
   smooth" fix, and a second `transition` declaration at higher specificity would REPLACE the
   shorthand and silently undo it. */
#sidebar .nav-item:has(> :is(.collapse, .collapsing)) > .nav-link[aria-expanded="true"] {
  background-color: rgb(255 255 255 / 6%);
  box-shadow: inset 0 -1px 0 rgb(255 255 255 / 18%);
  border-radius: 0;
}
#sidebar .nav-item:has(> :is(.collapse, .collapsing)) > :is(.collapse, .collapsing) > .nav-tree {
  background-color: #152036;
}
/* Tabler's native "this group holds the current page" accent (.nav-item.active::after, a left
   border spanning the item) is redundant once the group is open -- the active child row is painted
   brand blue with its own rail two pixels away. Suppressed while open OR opening; it still does its
   real job on a collapsed group, which is the case it exists for. */
#sidebar .nav-item:has(> :is(.collapse.show, .collapsing))::after {
  display: none !important;
}
/* Bootstrap's own `.collapsing{transition:height .35s ease}` is the motion everything above is
   timed against. `ease` front-loads and drags at the end, which is most of what read as "not
   smooth"; ease-in-out at a slightly shorter duration lands with the 0.22s shadow fade. */
#sidebar-menu .collapsing {
  transition: height 0.26s ease-in-out;
}
/* added: FLX-764 round 2 -- the module switcher had NO motion whatsoever. It is a Bootstrap
   dropdown, not a collapse, and since Bootstrap skips Popper inside a .navbar (see FLX-762 round 3)
   showing it is a bare display toggle. That is the "module selector isn't acting like the rest"
   the user reported. An animation, not a transition, because a display:none -> block change has no
   interpolable start state; a keyframe animation simply runs when the element appears. */
@keyframes flx-nav-reveal {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
#sidebar .sidebar-module-menu.show {
  animation: flx-nav-reveal 0.22s ease-in-out;
}
/* added: FLX-764 round 2 -- nothing in this stylesheet honoured prefers-reduced-motion before now,
   and this ticket is the one that introduced real movement to the nav. */
@media (prefers-reduced-motion: reduce) {
  #sidebar .nav-item:has(> :is(.collapse, .collapsing)),
  #sidebar .nav-link,
  #sidebar-menu .collapsing {
    transition: none;
  }
  #sidebar .sidebar-module-menu.show {
    animation: none;
  }
}
/* Rows: same geometry as .sidebar-module-menu .dropdown-item. The icon column is already supplied
   by the existing #sidebar .nav-link > i:first-child rule, so only padding and the rail are set
   here. colour needs !important solely because #sidebar .nav-link's own colour carries it. */
#sidebar .nav-tree .nav-link {
  position: relative;
  display: flex;
  align-items: center;
  padding: 0.4rem 0.75rem;
  border-radius: 0;
}
#sidebar .nav-tree .nav-link::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 3px;
  background-color: transparent;
}
#sidebar .nav-tree .nav-link:hover {
  background-color: rgb(255 255 255 / 10%);
  border-radius: 0;
}
#sidebar .nav-tree .nav-link:hover::before {
  background-color: rgb(255 255 255 / 35%);
}
#sidebar .nav-tree .nav-link.active {
  background-color: var(--flx-blue);
  color: #fff !important;
  border-radius: 0;
}
#sidebar .nav-tree .nav-link.active::before {
  background-color: #fff;
}
/* The collapsed icon rail hides the group's children with display:none, but the classes Bootstrap
   uses to track open state remain on the element -- so the :has() selectors above still match there
   and would draw a box, and clip, around a lone 44px icon. Everything is unwound for that state.
   Note this matches on :is(.collapse, .collapsing) rather than the open state specifically: the
   base rule above applies overflow:hidden and a radius to EVERY group regardless of open state
   (they have to exist in the closed state for the shadow to have something to transition from), so
   the unwind has to be just as broad or a collapsed group keeps them in the rail.
   #sidebarFlyout itself is safe either way -- it is a direct child of <aside id="sidebar">, not of
   any group <li>, so no ancestor overflow:hidden can clip it. */
body.sidebar-collapsed #sidebar .nav-item:has(> :is(.collapse, .collapsing)) {
  box-shadow: none;
  border-radius: 0;
  overflow: visible;
  margin-bottom: 0;
}
body.sidebar-collapsed #sidebar .nav-item:has(> :is(.collapse, .collapsing)) > .nav-link[aria-expanded="true"] {
  background-color: transparent;
  box-shadow: none;
}

/* fixed: user directive -- was a light card-style box (border + background), left over from
   before #sidebar became permanently dark-themed and was flattened to plain text (no border/bg).
   modified: user directive reintroduced a bordered card treatment -- supersedes that flattening. */
.sidebar-tenant-badge {
  /* modified: FLX-764 -- padding-x 0.5rem -> 0.75rem. Dropping mx-3 put this badge's BOX on the
     shared left edge, but its contents still started 4px inside every other row's, so the building
     icon sat a hair left of the module switcher's and the nav's icon column. Matching .nav-link's
     0.75rem lines the content up too, not just the border. | was: padding: 0.5rem 0.5rem */
  padding: 0.5rem 0.75rem;
  margin-bottom: 1em !important;
  border: 1px solid rgb(255 255 255 / 25%);
  border-radius: 0.5em;
}
.sidebar-tenant-icon {
  font-size: 0.8rem;
  color: #7c8ba1;
  flex-shrink: 0;
}
.sidebar-tenant-name {
  font-size: 0.78rem;
  line-height: 1.2;
}
.sidebar-tenant-role {
  font-size: 0.7rem;
  line-height: 1.2;
}

body.sidebar-collapsed .sidebar-tenant-badge {
  display: none !important;
}

/* added: FLX-708 (Epic FLX-703) -- the module switcher, directly under the workspace switcher.
   Deliberately styled to match .sidebar-tenant-badge above rather than looking like a form
   control: tenant and module are the app's two context dimensions and should read as one pair of
   controls, not as a label plus a dropdown. Sits on the dark #sidebar (var(--flx-navy)), so every
   colour here is set against that ground rather than inherited from Tabler's light defaults.
   44px min-height keeps it a real touch target, per the standing design bar. */
/* modified: FLX-762 round 3 -- the outline moved OFF the toggle and ONTO this wrapper, so one
   border encloses the whole control: just the toggle while closed, toggle plus the full module
   list while open. That works without any open/closed special-casing because this menu is not a
   floating layer -- see the round-3 note on .sidebar-module-menu below -- so the wrapper's own box
   already grows to contain the list. overflow:hidden clips the square-cornered rows to the
   wrapper's radius. */
#sidebar .sidebar-module-switch {
  border: 1px solid rgb(255 255 255 / 25%);
  border-radius: 0.5em;
  overflow: hidden;
}
#sidebar .sidebar-module-switch:hover,
#sidebar .sidebar-module-switch:focus-within {
  border-color: rgb(255 255 255 / 40%);
}
.sidebar-module-btn {
  min-height: 44px;
  padding: 0.5rem 0.75rem;
  color: var(--flx-stone);
  background-color: rgb(255 255 255 / 6%);
  /* border/radius deliberately removed -- the wrapper above owns the outline now */
  border: 0;
  border-radius: 0;
  font-size: 0.8rem;
  text-align: left;
}
.sidebar-module-btn:hover,
.sidebar-module-btn:focus-visible {
  color: #fff;
  background-color: rgb(255 255 255 / 12%);
}
.sidebar-module-btn .fa {
  flex-shrink: 0;
}
/* added: FLX-762 round 3 -- user-reported: the sub-item icons sat well right of the toggle's own
   icon. Same fixed-width centred column as the rows below and as #sidebar .nav-link, applied to
   the toggle too, so the toggle icon and every module icon share one left edge AND one centre
   line -- a narrow glyph (fa-wrench) and a wide one (fa-car) otherwise start their labels on
   different pixels even at identical padding. */
#sidebar .sidebar-module-btn > span > i:first-child {
  display: inline-block;
  width: 1.25rem;
  text-align: center;
  flex-shrink: 0;
}
/* fixed: FLX-762 -- the open state was never declared, so opening the menu erased the control.
   This toggle is a bare .btn with no colour variant, and Tabler defines --tblr-btn-active-color/
   -active-bg/-active-border-color ONLY on variant classes (.btn-primary, .btn-azure, ...) -- never
   on base .btn, never at :root. Bootstrap adds .show while the menu is open, firing Tabler's
   `.btn.show{color:var(--tblr-btn-active-color);background-color:var(--tblr-btn-active-bg);
   border-color:var(--tblr-btn-active-border-color)}` with all three tokens undefined. Undefined
   custom properties make each declaration invalid at computed-value time, so colour fell back to
   `inherit` (.navbar's --tblr-navbar-color, i.e. --tblr-secondary, a muted grey on this navy),
   background to `transparent` and border to `currentcolor` -- the user-reported "heading
   disappears". Scoped with #sidebar deliberately: `.sidebar-module-btn.show` alone only TIES
   `.btn.show` at (0,2,0) and would win on load order alone, which a future asset reorder would
   silently break. */
#sidebar .sidebar-module-btn.show {
  color: #fff;
  background-color: rgb(255 255 255 / 12%);
  /* modified: FLX-762 round 3 -- the outline lives on the wrapper now, so the toggle no longer
     manages a seam. All it needs is one divider separating it from the list it opened. */
  border-bottom: 1px solid rgb(255 255 255 / 18%);
}
/* the chevron is the one affordance that says "open" without relying on the panel being visible */
.sidebar-module-btn .fa-angle-down {
  transition: transform 0.15s ease;
}
.sidebar-module-btn.show .fa-angle-down {
  transform: rotate(180deg);
}
/* modified: FLX-745 -- the open menu was invisible against the sidebar. It inherited Tabler's
   dark-theme dropdown background, which is within a few percent of the sidebar's own
   var(--flx-navy) #081220, so the panel had no edge and read as part of the nav. A dropdown is a
   layer ABOVE the thing that opened it and has to say so: lifted surface, real border, shadow. */
/* modified: FLX-762 rounds 1-2, CORRECTED in round 3. Rounds 1 and 2 treated this as a floating
   dropdown and spent effort welding it to the toggle -- a -1px pull, square top corners, matching
   border colours, and data-bs-offset="0,0" to cancel Popper's default [0, 2] offset. **None of
   that applied.** This menu never floats: Bootstrap's dropdown checks
   `this._inNavbar || "static" === this._config.display` and skips Popper entirely for any toggle
   inside a .navbar. #sidebar IS `.navbar.navbar-vertical.navbar-expand-lg`, so the list renders
   statically, in normal flow, pushing the nav below it down. That is also why the offset attribute
   was inert and has been removed from nav_side.php.
   Two Tabler rules do the real damage here, both out of reach of a plain class selector:
     .navbar-expand-lg .navbar-collapse .dropdown-menu               (0,3,0)
       -> padding:0; background:0 0; border:none; box-shadow:none; min-width:0; margin:0
     .navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item (0,4,0)
       -> min-width:0; display:flex; width:auto;
          padding-left:calc(--tblr-page-padding + 1.75rem)  == 2.75rem
   The first is why the panel had no background of its own despite FLX-745 setting one -- (0,1,0)
   never stood a chance. The second is the ~44px indent that put the module icons far right of the
   toggle's, and the `width:auto` behind round 2's ragged row highlights.
   Everything that must survive them is therefore #sidebar-scoped at (1,x,0). The border, radius
   and shadow are gone from here entirely: the wrapper owns the outline now. */
.sidebar-module-menu {
  font-size: 0.85rem;
}
#sidebar .sidebar-module-menu {
  background-color: #152036;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding: 0;
  margin: 0;
  min-width: 0;
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
}
/* fixed: FLX-745 -- every row now has identical geometry. The hover state was Tabler's default
   light-surface link-hover, which on this dark panel read as a differently-sized block from the
   .active row's full-bleed --flx-blue.
   modified: FLX-762 -- rows are full-bleed inside the now-unpadded panel, so the per-row radius
   FLX-745 added is gone (the panel's own overflow clips the bottom row instead) and padding-left
   is 0.75rem to match Tabler's .nav default, which is what #sidebar .nav-link below resolves to --
   the switcher and the nav it sits above now share one left edge for icons and one for labels. */
.sidebar-module-menu .dropdown-item {
  position: relative;
  display: flex;
  width: 100%;
  min-height: 40px;
  padding: 0.4rem 0.75rem;
  border-radius: 0;
  color: var(--flx-stone);
  /* backstop: "All modules" puts its label in a bare text node with no span to truncate */
  white-space: nowrap;
  overflow: hidden;
}
/* fixed: FLX-762 round 2 -- user-reported: hover and active highlights were different widths on
   every row, each stopping at the end of its own label. Tabler ships TWO `.dropdown-item` rules and
   the second overrides its own base: `.dropdown-item{min-width:0;display:flex;width:auto;
   padding-left:calc(... + 1.75rem);color:inherit}`. `width:auto` on a flex item shrink-wraps the
   row to its content, so the highlight tracked the text -- and that 1.75rem-plus padding-left is
   the original indent that read as the "blank" gutter this ticket started from.
   Every element in the chain is pinned to 100% here (panel -> li -> row), #sidebar-scoped at
   (1,2,0) so nothing in Tabler can contest it on load order, and border-box so the row's own
   padding cannot push it back past the panel edge. */
#sidebar .sidebar-module-menu > li {
  display: block;
  width: 100%;
}
#sidebar .sidebar-module-menu .dropdown-item {
  display: flex;
  width: 100%;
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
  /* fixed: FLX-762 round 3 -- padding and colour MUST live at this specificity, not on the plain
     .sidebar-module-menu .dropdown-item (0,2,0) rule above. Tabler's
     `.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item` is (0,4,0) and was winning
     both: its padding-left of --tblr-page-padding + 1.75rem (2.75rem, ~44px) is exactly the indent
     that pushed the module icons right of the toggle's, and its `color:inherit` was overriding the
     row colour. 0.75rem matches .sidebar-module-btn's own padding-x, so the toggle icon and every
     module icon now start on the same pixel. */
  padding: 0.4rem 0.75rem;
  color: var(--flx-stone);
}
/* same fixed-width centred icon column as #sidebar .nav-link > i:first-child, so a narrow glyph
   (fa-wrench) and a wide one (fa-car) still start their labels on the same pixel */
.sidebar-module-menu .dropdown-item > i:first-child {
  flex-shrink: 0;
  width: 1.25rem;
  text-align: center;
}
/* fixed: FLX-762 -- "Website Management" and "Booking & Calendar" ran past the panel's right edge.
   The label span is .flex-grow-1, and a flex item's default min-width:auto refuses to shrink below
   its content, so nowrap text overflowed rather than truncating. */
.sidebar-module-menu .dropdown-item > span {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* added: FLX-762 -- the accent rail. Full-bleed rows lost the inset that used to imply "these
   belong to the control above", so the rail carries that job: it lands exactly where Tabler's own
   --tblr-navbar-active-border-color indicator sits on the nav below, making the switcher read as
   part of the same navigation rather than as a floating menu. Absolutely positioned so turning it
   on never reflows the row. */
.sidebar-module-menu .dropdown-item::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 3px;
  background-color: transparent;
}
#sidebar .sidebar-module-menu .dropdown-item:hover,
#sidebar .sidebar-module-menu .dropdown-item:focus-visible {
  background-color: rgb(255 255 255 / 10%);
  color: #fff;
}
.sidebar-module-menu .dropdown-item:hover::before,
.sidebar-module-menu .dropdown-item:focus-visible::before {
  background-color: rgb(255 255 255 / 35%);
}
/* The active row is marked with a check as well as with colour -- colour alone is not an
   accessible way to say "you are here". Its rail is white, not --flx-blue: the row's background is
   already --flx-blue, and a blue rail on a blue row is no rail at all. */
/* fixed: FLX-762 round 3 -- this was losing outright. Tabler's
   `.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active` is (0,5,0) and sets
   `background: var(--tblr-navbar-active-bg)` (rgba(0,0,0,.2)), so the selected module rendered as a
   dark grey band rather than the brand blue this rule has specified since FLX-745. #sidebar takes
   it to (1,3,0), which beats it. */
#sidebar .sidebar-module-menu .dropdown-item.active,
#sidebar .sidebar-module-menu .dropdown-item.active:hover {
  background-color: var(--flx-blue);
  color: #fff;
}
.sidebar-module-menu .dropdown-item.active::before {
  background-color: #fff;
}
.sidebar-module-menu .dropdown-divider {
  border-top-color: rgb(255 255 255 / 12%);
  margin: 0.25rem 0;
}

body.sidebar-collapsed .sidebar-module-switch {
  display: none !important;
}

.rating-btn { min-width: 44px; min-height: 44px; font-size: 1rem; font-weight: 700;
              border: 1.5px solid #adb5bd; border-radius: 6px;
              box-shadow: 0 2px 4px rgba(0,0,0,0.12); }
/* modified: FLX-781 -- the four scale colours were hardcoded literals, so nothing
   else on the screen could match them by construction. That is exactly how the
   FLX-780 tally ended up on Tabler's #2fb344/#d63939 while the scale sat on
   Bootstrap's #198754/#dc3545 -- two greens and two reds inches apart. The
   pass/fail ends are now tokens both components share; the middle two stay
   literal because nothing else references them. | was: 4 hardcoded hex values */
.rating-btn.active-1,.rating-legend-badge.active-1 { background: var(--flx-eval-fail); color: #fff; border-color: var(--flx-eval-fail); }
.rating-btn.active-2,.rating-legend-badge.active-2 { background: #fd7e14; color: #fff; border-color: #fd7e14; }
.rating-btn.active-3,.rating-legend-badge.active-3 { background: #ffc107; color: #000; border-color: #ffc107; }
.rating-btn.active-4,.rating-legend-badge.active-4 { background: var(--flx-eval-pass); color: #fff; border-color: var(--flx-eval-pass); }

.rating-legend-badge { font-size: .8rem; font-weight: 600; padding: .35em .7em; white-space: nowrap; }

.session-timer-display { font-size: 1.35rem; font-weight: 700; color: #ffc107; letter-spacing: .03em; line-height: 1; }
.topic-row { border-bottom: 1px solid #dee2e6; }
.topic-row:last-child { border-bottom: none; }
.topic-name { font-size: .85rem; font-weight: 500; min-width: 160px; }

/* modified: FLX-1192 -- topic category band uses the palette navy (--flx-navy) instead of a stray near-black | was: #111 */
.category-header { background: #081220; color: #fff; font-size: .72rem; font-weight: 700;
                   text-transform: uppercase; letter-spacing: .06em; }

.topic-row:not(.category-header):not(.comment-row):hover { background: #f0f0f0; cursor: pointer; }

.comment-row:hover { background: #f5f5f5; cursor: default; }

.comment-meta > .text-muted { font-size: .72rem !important; color: #adb5bd !important; font-style: italic; }

.comments-table>:not(caption)>*>*{padding:.6rem .75rem;vertical-align:top;}
.comments-table thead th{white-space:nowrap;}
/* modified: FLX-1192 -- PROD session-view: comment text painted over the Edit/Delete icons with a
   horizontal scrollbar. The FLX-679 global rule (.table-responsive .table td { white-space: nowrap },
   further down) has the same specificity as this selector and is declared later, so it won once
   FLX-369 put the table inside .table-responsive; nowrap + max-width:0 then overflowed the cell onto
   the actions column. Selector now includes the wrapper so it outranks the global rule, and
   white-space:normal is declared explicitly. CSS-only so both PHP-rendered and JS-inserted rows wrap.
   | was: .comments-table td.comment-cell with no white-space declaration */
.table-responsive .comments-table td.comment-cell,
.comments-table td.comment-cell{font-size:.75rem;line-height:1.6;white-space:normal;overflow-wrap:anywhere;word-break:break-word;max-width:0;}
.comments-table .topic-badge{font-size:.68rem;font-weight:600;}
#signaturePad { border: 2px dashed #dee2e6; border-radius: 8px; cursor: crosshair; touch-action: none; }
.save-indicator { font-size: .75rem; }
.save-indicator-error { color: #ffcad4 !important; font-weight: 700; }

.gender-icon-male { color: var(--flx-blue); }
.gender-icon-female { color: #d63384; }
.gender-icon-x { color: #000; }

.student-name-expiring { color: var(--flx-amber); }
.student-name-expired { color: #a40606; }

.flx-cal-invite-icon { font-size: .75rem; cursor: default; color: var(--bs-primary); display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; }

.fin-col-lbl { width: 140px; }

:root {
  --fc-button-bg-color:            var(--flx-navy);
  --fc-button-border-color:        var(--flx-navy);
  --fc-button-hover-bg-color:      var(--flx-navy);
  --fc-button-hover-border-color:  var(--flx-navy);
  --fc-button-active-bg-color:     var(--flx-slate);
  --fc-button-active-border-color: var(--flx-slate);
  --fc-today-bg-color:             rgba(8, 18, 32, 0.06);
  --fc-now-indicator-color:        #dc3545;
  --fc-highlight-color:            rgba(8, 18, 32, 0.10);
  --fc-event-bg-color:             var(--flx-navy);
  --fc-event-border-color:         var(--flx-navy);
}

.fc .fc-button-primary:not(:disabled).fc-button-active,
.fc .fc-button-primary:not(:disabled):active {
  background-color: var(--flx-slate);
  border-color: var(--flx-slate);
  box-shadow: none;
}
.fc .fc-button-primary:focus {
  box-shadow: 0 0 0 0.2rem rgba(8, 18, 32, 0.35);
}

.fc .fc-button {
  font-size: 0.8rem;
  padding: 0.3rem 0.55rem;
}

@media (max-width: 767.98px) {
  .fc .fc-toolbar {
    flex-wrap: wrap;
    gap: 0.35rem;
  }
  .fc .fc-toolbar-chunk {
    flex-shrink: 0;
  }
  .fc .fc-toolbar-title {
    font-size: 0.95rem;
  }

  .fc .fc-button {
    font-size: 0.7rem;
    padding: 0.18rem 0.4rem;
    min-width: unset;
  }
}

/* added: FLX-397 -- majority of admins use this page on tablet/iPad. pointer:coarse targets
   touch input specifically (unlike a max-width breakpoint, it still applies on a large iPad Pro
   landscape screen), bumping tap target size on the calendar toolbar and instructor filter pills
   without shrinking anything for mouse/desktop users. */
@media (pointer: coarse) {
  .fc .fc-button {
    padding: 0.5rem 0.85rem;
    font-size: 0.85rem;
  }
  .fc .fc-daygrid-day-number,
  .fc .fc-col-header-cell-cushion {
    padding: 0.4rem;
  }
  .instr-filter-pill {
    padding: 9px 16px;
    font-size: 0.85rem;
  }
  /* added: FLX-729 -- row actions are .btn-icon.btn-sm so a table row stays compact on a mouse
     screen, which is what was asked for. That is below the 44x44 touch minimum, so on a coarse
     pointer only, they grow back to a real touch target. Both requirements are satisfied
     without either one compromising the other. */
  .btn-icon.btn-sm,
  .btn-list .btn-sm {
    min-width: 44px;
    min-height: 44px;
  }
  /* Segmented filter tabs get the height but not the width floor -- a 44px-wide "All" in a
     five-item btn-group would push the group past a phone viewport. */
  .btn-group-sm > .btn {
    min-height: 44px;
  }
}

thead > tr { border-color: inherit; border-style: solid; border-width: 0; margin: 0; padding: 0 !important; line-height: 1; font-size: small; }

div.dataTables_wrapper div.dataTables_length label {
  margin-bottom: 0;
  font-size: .8125rem;
}
div.dataTables_wrapper div.dataTables_length select {
  font-size: .8125rem;

}

div.dataTables_wrapper div.dataTables_info {
  font-size: .8rem;
}

div.dataTables_wrapper div.dataTables_paginate .pagination {
  margin-bottom: 0;
  --bs-pagination-font-size: .8rem;
}

/* fixed: FLX-697 -- DataTables chrome escaping its card. Tabler's .card-table is deliberately
   edge-to-edge and only insets the first/last CELL padding by 1.25rem; DataTables' injected
   length/info/paging divs are not cells, so on any card that has no .card-body they rendered
   flush against the raw card edge while the table header/body stayed inset -- the misalignment
   reported on FleetFix Customers. Those 7 tables are fixed properly at the source, via
   FlxUI.cardTableDom (assets/js/flx-ui.js), which routes the chrome into a real .card-footer.
   THIS rule is the structural backstop so the same mistake cannot render badly again: it only
   matches a wrapper that is a DIRECT child of .card -- i.e. one with no .card-body supplying
   padding -- so the ~30 card-body-wrapped tables elsewhere in the app are untouched, as is any
   table already using the shared dom string (its chrome sits inside .card-footer, not here). */
.card > .dataTables_wrapper > .dataTables_length,
.card > .dataTables_wrapper > .dataTables_filter,
.card > .dataTables_wrapper > .dataTables_info,
.card > .dataTables_wrapper > .dataTables_paginate {
  padding: .5rem var(--tblr-card-cap-padding-x, 1.25rem);
}

/* added: user directive (2026-07-30) -- table density standard flipped. The previous rule
   ("no table-sm, cramped/no breathing room") was reversed after the user held up the Tenant
   Memberships table (creator/memberships.php, which already uses table-sm) as the actual
   benchmark for "compact and clean" and confirmed compact-everywhere over the old standard.
   Rather than adding `table-sm` markup to every table across the app (a large, unbounded sweep),
   this reuses Tabler's own real `.table-sm` cell padding value (.25rem, confirmed in the
   vendored tabler.min.css) as the new baseline for EVERY `.table`, site-wide, in one place --
   existing `table-sm` usages (Tenant Memberships, Permissions) are unaffected since they already
   matched this value; every other table now matches them too with zero per-file markup changes. */
.table > :not(caption) > * > * {
  padding: .25rem .25rem;
}

table.dataTable thead th { position: relative; }
table.dataTable thead .sorting,
table.dataTable thead .sorting_asc,
table.dataTable thead .sorting_desc { padding-right: 1.5rem; cursor: pointer; }
table.dataTable thead .sorting::after,
table.dataTable thead .sorting_asc::after,
table.dataTable thead .sorting_desc::after {
  font-family: FontAwesome;
  position: absolute;
  right: .45rem;
  top: 50%;
  transform: translateY(-50%);
  font-size: .7rem;
}
table.dataTable thead .sorting::after      { content: '\f0dc'; opacity: .35; }
table.dataTable thead .sorting_asc::after  { content: '\f0de'; opacity: .85; }
table.dataTable thead .sorting_desc::after { content: '\f0dd'; opacity: .85; }
table.dataTable thead .sorting::before,
table.dataTable thead .sorting_asc::before,
table.dataTable thead .sorting_desc::before { display: none; }
table.dataTable thead .sorting_disabled { padding-right: .75rem; cursor: default; }
table.dataTable thead .sorting_disabled::after { display: none; }

.ds-search-card {
  max-width: 480px;
  margin: 3rem auto 0;
}

#studentResults {
  margin-top: .625rem;
}
#studentResults .list-group-item {
  background-color: #f5f5f5;
}

.pwd-strength-track {
  height: 6px;
}

.tenant-logo-preview {
  max-height: 120px;
  max-width: 100%;
  object-fit: contain;
}
.tenant-color-swatch {
  width: 110px;
  height: 44px;
  border-radius: 6px;
}

@media print {
  #sidebar,
  header.navbar,
  .modal,
  .d-flex.justify-content-end.align-items-center.gap-2.mb-3,
  .alert { display: none !important; }

  .page { padding-left: 0 !important; }

  main { padding: 1rem !important; }
}

.eval-card-header {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: .5rem;
}
.eval-card-title {
  flex-shrink: 0;
  font-weight: 600;
}
.eval-legend {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem;
}

.eval-legend-badge {
  font-size: .7rem;
  white-space: nowrap;
}
.eval-legend-num  { font-weight: 700; }
.eval-legend-label { font-weight: 400; opacity: .9; }

.eval-topic-name {
  font-size: .82rem;
  flex: 1 1 0;
  min-width: 0;
}
.eval-rating-badge {
  flex-shrink: 0;
  font-size: .7rem;
  white-space: nowrap;

}

.topic-row { border-bottom: 1px solid #b8bfc7; }
.topic-row:last-child { border-bottom: none; }

@media (max-width: 575.98px) {
  .eval-card-header {
    flex-direction: column;
    gap: .4rem;
  }
  .eval-legend {
    gap: .25rem;
  }
  .eval-legend-badge {
    font-size: .65rem;
  }

  .eval-topic-row {
    align-items: flex-start !important;
    gap: .5rem;
  }
  .eval-topic-name {
    font-size: .8rem;
  }
  .eval-rating-badge {
    font-size: .65rem;
  }
}

.screen-lock-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.88);z-index:9999;display:flex;align-items:center;justify-content:center;text-align:center;color:#fff;flex-direction:column;cursor:pointer;}
.screen-lock-overlay .lock-icon{font-size:3rem;margin-bottom:1rem;opacity:.8;}
.screen-lock-overlay h4{font-size:1.4rem;margin-bottom:.5rem;}
.screen-lock-overlay p{opacity:.65;font-size:.9rem;}

:root {
  --flx-navy:  #081220;
  --flx-slate: #1F2937;
  --flx-steel: #374151;
  --flx-stone: #E5E7EB;
  --flx-paper: #F7F8FA;
  --flx-blue:  #0353A4;
  --flx-amber: #F59E0B;

  /* added: FLX-781 -- pass/fail semantics for the evaluation screens. Deliberately
     NOT part of the 7-colour brand palette above: green and red are not brand
     colours here, they are the examiner vocabulary the roadside evaluation grid
     has used since it shipped. Declared as tokens so the rating scale and the
     compliance tally cannot drift apart again -- the values are the ones the
     rating scale already used, kept exactly so nothing changes visually there. */
  --flx-eval-pass: #198754;
  --flx-eval-fail: #dc3545;
  --bs-body-font-size: 0.85rem;
  --tblr-body-font-size: 0.85rem;
  /* added: FLX-678 -- Tabler ships its own --tblr-* color namespace, entirely separate from the
     --bs-primary override above; .btn-primary/focus-rings/links were rendering in Tabler's own
     stock blue (#066fd1) instead of the brand palette until these were added. --tblr-primary-darken
     etc. are derived from --tblr-primary via Tabler's own color-mix() formulas, so overriding the
     root value alone is enough to cascade correctly in modern browsers. */
  --tblr-primary: var(--flx-navy);
  --tblr-primary-fg: #fff;
  --tblr-link-color: var(--flx-blue);
}

/* fixed: FLX-1232 -- text selection was invisible on the sidebar and barely visible anywhere.
   Tabler paints it with `::selection{background-color:color-mix(in srgb,var(--tblr-primary) 10%,
   transparent)}`, and the --tblr-primary override just above resolves that to a 10% navy tint:
   navy-on-navy inside #sidebar (reported as "cannot select the Tenant ID / IP"), a faint grey on
   white pages. Solid brand blue with white text is the pairing the active nav pill already uses
   on the same navy ground (#sidebar .nav-link.active), so one unscoped rule reads on both the
   light page body and the dark sidebar. Same (0,0,1) specificity as Tabler's rule; this sheet
   loads after tabler.min.css, so order decides. */
::selection {
  background-color: var(--flx-blue);
  color: #fff;
}

/* added: user directive -- match the locked artifact's tone/smoothness exactly. Tabler's own
   default .card uses --tblr-border-color-translucent (a semi-transparent border) plus a real
   drop shadow (--tblr-shadow-card) -- the artifact's cards are flatter and crisper: a solid
   1px --flx-stone border, no shadow at all, slightly larger 12px radius. Global, since cards are
   used on every page (dashboard widgets, DataTable wrappers, forms) -- the single highest-
   leverage place to close this gap app-wide rather than touching each page individually. */
.card {
  border-radius: 12px;
  border-color: var(--flx-stone);
  box-shadow: none;
}

/* added: user directive -- login page's form fields ran edge-to-edge on mobile with almost no
   side inset. User found width:90% fixed it, but that's Tabler's own global
   .container/.container-tight rule (--tblr-gutter-x-driven padding) -- overriding it there would
   shift every container's spacing app-wide. Scoped to the login page's own container instead via
   a dedicated class, and only below the breakpoint where container-tight's own max-width takes
   over (so the desktop auth-card layout, already correctly sized, is untouched). */
@media (max-width: 767.98px) {
  .flx-auth-container {
    width: 90%;
  }
}

/* added: FLX-678 -- an <i class="fa ..."></i> immediately followed by a text label, with no
   margin utility class, relied on Bootstrap's own default line-height/icon-baseline rendering to
   look adequately spaced ("+ New Booking" instead of "+New Booking"). Tabler's CSS reset doesn't
   render that bare-space convention the same way, so ~30 buttons/nav-links/headings across the
   app (that never needed an explicit me-1/me-2 before) started showing the icon jammed against
   its label the moment the CSS framework swapped. Rather than touching every one of those call
   sites individually (that's each page's own Step 2 content-redesign job), this is a global,
   additive safety net: only applies where no margin-utility class is already present, so it can
   never re-space the ~225 call sites that already carry an explicit me-1/me-2/etc. */
.btn i.fa:first-child:not([class*="me-"]),
.nav-link i.fa:first-child:not([class*="me-"]),
.dropdown-item i.fa:first-child:not([class*="me-"]),
.dropdown-toggle i.fa:first-child:not([class*="me-"]),
.card-title i.fa:first-child:not([class*="me-"]),
.card-header i.fa:first-child:not([class*="me-"]),
.modal-title i.fa:first-child:not([class*="me-"]),
label i.fa:first-child:not([class*="me-"]),
h1 i.fa:first-child:not([class*="me-"]),
h2 i.fa:first-child:not([class*="me-"]),
h3 i.fa:first-child:not([class*="me-"]),
h4 i.fa:first-child:not([class*="me-"]),
h5 i.fa:first-child:not([class*="me-"]),
h6 i.fa:first-child:not([class*="me-"]) {
  margin-right: .375rem;
}
/* fixed: FLX-729 -- the safety net above assumes an icon is followed by a text label. A .btn-icon
   has no label, and it matches two of those selectors at once (`.btn i.fa` and, in a card header,
   `.card-header i.fa`), so every icon-only action button rendered with .375rem of dead space on
   its right and a visibly off-centre glyph. Icon-only buttons opt back out here.

   fixed: FLX-848 -- the opt-out above was written as `.btn-icon i.fa:first-child`, which is ONE
   selector lighter than the safety net it exists to cancel: the net is (0,4,1) because of its
   trailing :not([class*="me-"]), the opt-out was (0,3,1). It therefore never applied a single
   time, and every icon-only button in the app has rendered off-centre since FLX-729 shipped
   believing this was fixed. Restated with the same :not() the net carries so the two tie at
   (0,4,1) and this one wins on source order.

   Deliberately NOT solved by deleting the margin from the safety net (the user's first attempt):
   that centres the icon-only buttons by taking the gap away from every icon+label button in the
   app at the same time -- "+Add Rate" on FleetFix Tax Settings loses its space. There is no CSS
   selector for "this icon is followed by a bare text node", which is why the opt-out is
   class-based rather than structural.

   KNOWN GAP, not addressed here: 27 further icon-only buttons across ~20 files carry no
   .btn-icon class at all (drivingschool/session-view.php comment actions, modules/website/
   media.php card actions, and others), so no class-based opt-out can reach them. Raised on
   FLX-848 for its own triage rather than mass-editing markup inside a CSS fix. */
.btn-icon i.fa:first-child:not([class*="me-"]) {
  margin-right: 0;
}

/* fixed: FLX-678 -- Tabler's .alert is display:flex;flex-direction:row with no wrap (built for a
   specific icon + single-wrapped-content-div structure). This app's ~155 existing .alert call
   sites instead write a bare icon followed by loose text and inline <code>/<strong> tags directly
   as children -- under flex, every one of those becomes its own unwrapped flex item in one row,
   forcing severe horizontal overflow (confirmed live on creator/error-log.php's info banner).
   Reverting .alert to normal block flow restores the plain HTML inline/block text wrapping this
   markup was actually written for, matching pre-Tabler behavior -- only the flex layout changes,
   the --tblr-alert-* color/padding/border tokens are untouched. */
.alert { display: block; }

/* fixed: FLX-678 -- Tabler's .badge sets its own fixed default text color (muted gray, via
   --tblr-badge-color) independent of whatever bg-* class is applied -- bg-success/bg-primary/etc.
   only ever set the background (each with !important), never text color, so "badge bg-success"
   rendered as barely-readable gray text on a green pill (found live: system-settings.php's "Active
   -- uploads go to R2" badge and its "SYS" badge). Bootstrap 5.3+/Tabler's own correct idiom for a
   solid-background badge is the paired text-bg-* utility, which sets both together -- but ~161
   existing badge call sites across the app use the old bg-only pattern, far too many to convert
   one by one. This mirrors Tabler's own text-bg-* color choices (verified against its compiled
   CSS) as a global rule instead, so every existing "badge bg-*" call site gets readable text
   without any markup changes. The -subtle background variants are deliberately unaffected (already
   pale, dark-text-appropriate, a different pattern). */
.badge.bg-light, .badge.bg-white { color: var(--flx-slate) !important; }
/* fixed: user-reported bug -- .badge.bg-success/.bg-danger/.bg-warning used to be solid/dark
   (needing forced white text, same reasoning as bg-secondary/bg-primary/bg-info/bg-dark below),
   but a later change made those three pale/subtle pills with a small dot (see the
   .badge.bg-success block above) -- this rule, same specificity and !important, was landing
   after that one in the cascade and winning, silently overwriting the intended dark emphasis
   text with white-on-pale (unreadable). Removed from this list for the same reason the
   comment below already carves out "-subtle" variants: pale background, needs dark text. */
.badge.bg-secondary, .badge.bg-primary, .badge.bg-info, .badge.bg-dark {
  color: #F9FAFB !important;
}

/* added: user directive -- Students' DataTable (drivingschool/students.php, scrollX:true +
   explicit per-column pixel widths) never wraps cell text, staying single-line and scrolling
   horizontally on narrow screens; other tables (e.g. creator/tenants.php's TenantsTable, no
   scrollX/widths) don't share that, so cell text wraps within whatever width the browser
   happens to give each column -- tall, crowded rows on mobile. Fixed globally instead of
   touching every DataTable init individually (~34+ pages per the documented DataTable
   convention): any table inside the app's own established .table-responsive wrapper gets
   nowrap cells, so text never breaks mid-column -- .table-responsive's own overflow-x:auto
   (already relied on everywhere) provides the horizontal scroll instead, the same end result as
   Students' scrollX approach with no JS changes needed anywhere. Escape hatch: add .text-wrap to
   a specific <td>/<th> if a genuine free-text column needs real wrapping. */
.table-responsive .table td,
.table-responsive .table th {
  white-space: nowrap;
}
.table-responsive .table td.text-wrap,
.table-responsive .table th.text-wrap {
  white-space: normal;
}
/* added: FLX-1226 - the wrapper scrolls on one axis only. Tabler's .table-responsive sets
   overflow-x:auto and nothing for overflow-y, so overflow-y computes to auto as well; every
   wrapper in the app is auto-height (no max-height anywhere, no DataTables scrollY), so nothing
   can legitimately overflow it vertically - the only thing a vertical scrollbar there can ever
   be is a rounding artefact of the scroll container, which is what the user saw on the FleetFix
   work-order Jobs card: a scrollbar down the right edge of the lines table with the Total /
   Actions columns reading as bleeding out of the card. Hidden on the vertical axis, the wrapper
   still scrolls horizontally when a table is genuinely wider than its card. A dropdown menu
   inside the wrapper was already clipped by the auto value; it needs Popper's fixed strategy,
   data-bs-popper-config='{"strategy":"fixed"}' on the toggle (helpers.php, website/pages.php,
   work-order.php, linx/drivers.php, linx/vehicles.php, events-manage.php, tenant/manage-users.php
   as of this ticket; the data-bs-strategy="fixed" three of them carried was not a Bootstrap
   option and did nothing) - that is the fix for a clipped menu, not the wrapper's overflow. */
.table-responsive {
  overflow-y: hidden;
}

/* added: FLX-678 -- topbar polish pass. Standardizes the theme-toggle/notification-bell icon
   buttons to a real 44x44px touch target with an invisible-at-rest ghost style -- ~32px via bare
   p-2 was under the accessible minimum, and a first attempt at fixing that left Tabler's default
   .btn border showing (border-0 got dropped when the class list changed), which combined with the
   44px footprint read as a heavy, over-designed box rather than a clean icon affordance. No
   visible border/background at rest; feedback appears only on hover/focus, matching how
   Stripe/Linear/Vercel treat icon-only toolbar buttons -- the tap target is preserved, it just
   doesn't need to be drawn to exist. */
.flx-icon-btn {
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  border: none;
  background: transparent !important;
  /* fixed: user directive -- Tabler's own .btn base sets --tblr-btn-bg to a real surface color
     (not transparent, despite the base .btn rule's own default -- a later .btn rule in the same
     compiled stylesheet overrides it) plus a default box-shadow, so a plain .btn.btn-icon with
     no other override renders as a visible filled/bordered box at rest -- exactly what read as
     "disoriented"/"disproportionate" next to the borderless avatar button beside it in dark
     mode. Both explicitly killed here rather than relying on background alone. */
  box-shadow: none !important;
  color: var(--tblr-secondary);
  /* matches Tabler's own base .nav-link/.btn transition timing exactly (.15s ease-in-out per
     the vendored tabler.min.css), not an approximated ease -- verified directly, not assumed. */
  transition: background-color .15s ease-in-out, color .15s ease-in-out;
}
.flx-icon-btn:hover, .flx-icon-btn:focus-visible {
  background-color: var(--tblr-bg-surface-secondary);
  color: var(--tblr-body-color);
}
.flx-icon-btn:focus-visible {
  outline: 2px solid var(--flx-blue);
  outline-offset: 1px;
}
/* added: FLX-1133 -- the count badge on the bell. Pinned to the corner of the 14px glyph inside
   the 44px button (top/right 8px), not to the button's own corner, and a fixed 16px disc so a
   one- or two-digit count renders the same size. [hidden] keeps Bootstrap's display:none. */
.flx-bell-badge {
  position: absolute;
  top: 8px;
  right: 8px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  font-size: 10px;
  line-height: 16px;
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Tabler's input-icon-addon is pointer-events:none by design (decorative icon, Enter submits) and
   is min-width:2.5rem -- the input needs matching left padding so typed text never sits under the
   icon. form-control-sm's own padding-left is smaller than the addon's min-width, so this is an
   explicit, deliberate override, not a default. */
.flx-topbar-search { min-width: 200px; }
/* modified: FLX-989 -- the padding-left/right declarations that used to live here moved to the
   input-icon repair block at the end of this file. At 0,2,0 they lost outright to FLX-870's
   `.page .form-control:not(...):not(...)` shorthand (0,4,0), so they had stopped doing anything. */
.flx-topbar-search .input-icon-addon { color: var(--tblr-secondary); }

/* added: FLX-679 (epic FLX-676) -- Command Center topbar. .flx-topbar replaces the old
   <header class="navbar navbar-expand-md"> row (dropped in layout_top.php -- it only ever
   rendered a permanently-hidden brand plus this same content). Sits inside .page-wrapper, above
   .page-header, matching the locked artifact's topbar position. */
.flx-topbar {
  height: 60px;
  display: flex;
  align-items: center;
  background: var(--tblr-bg-surface);
  border-bottom: 1px solid var(--tblr-border-color);
  flex-shrink: 0;
}
.flx-topbar .container-fluid {
  display: flex;
  align-items: center;
  height: 100%;
}
/* command-palette sizing: wider + capped, per the locked Concept A artifact, vs. the old
   200px-min right-aligned search box. */
.flx-topbar-search {
  max-width: 420px;
  flex: 1 1 0;
  min-width: 0;
  /* fixed: user directive -- flex:1 lets the search box grow to fill all available space up to
     max-width, leaving no natural gap before the icon group (ms-auto alone isn't a gap, just an
     alignment push) -- read as the icons "sticking to the search bar". */
  margin-right: 1rem;
}
/* fixed: FLX-679 -- the icon-group (theme toggle/bell/avatar dropdown) had no flex-shrink
   protection, so on narrow viewports the search box's reserved padding-right (below, only
   actually needed at md+ where the kbd hint is visible) left too little room and pushed the
   avatar dropdown partially past the right edge of the screen -- "half chopped" in mobile QA
   review. Icon group now never shrinks; the search box (which can legitimately shrink) absorbs
   the squeeze instead. */
.flx-topbar-actions {
  flex-shrink: 0;
}
/* modified: FLX-989 -- the sub-md padding-right release moved to the input-icon repair block at
   the end of this file, alongside the reserve it releases. */
.flx-cmdk-kbd {
  position: absolute;
  right: .6rem;
  top: 50%;
  transform: translateY(-50%);
  font-size: 11px;
  font-family: inherit;
  color: var(--tblr-secondary);
  background: var(--tblr-bg-surface-secondary);
  border: 1px solid var(--tblr-border-color);
  border-radius: 5px;
  padding: 1px 6px;
  pointer-events: none;
}

.table>thead { vertical-align: middle; }

html { scroll-behavior: smooth; }

body::before {
  content: "";
  position: fixed;
  top: 0; left: 0;
  height: 3px;
  width: 100%;
  z-index: 10000;

  background: linear-gradient(90deg, var(--flx-navy) 0%, var(--flx-slate) 55%, var(--flx-amber) 100%);
  transform-origin: left center;
  transform: scaleX(0);
  animation: flx-page-load 1.5s cubic-bezier(0.4,0,0.2,1) forwards;
  pointer-events: none;
}
@keyframes flx-page-load {
  0%   { transform: scaleX(0);    opacity: 1; }
  65%  { transform: scaleX(0.78); opacity: 1; }
  90%  { transform: scaleX(0.96); opacity: 1; }
  100% { transform: scaleX(1);    opacity: 0; }
}

[data-bs-theme="dark"]  .flx-icon-light { display: none !important; }
[data-bs-theme="dark"]  .flx-icon-dark  { display: inline !important; }
[data-bs-theme="light"] .flx-icon-dark  { display: none !important; }
[data-bs-theme="light"] .flx-icon-light { display: inline !important; }

[data-bs-theme="dark"] {

  --bs-body-bg:           var(--flx-navy);
  --bs-body-bg-rgb:       8, 18, 32;
  --bs-body-color:        var(--flx-stone);
  --bs-body-color-rgb:    230, 234, 240;
  --bs-secondary-bg:      var(--flx-navy);
  --bs-secondary-bg-rgb:  17, 24, 39;
  --bs-tertiary-bg:       var(--flx-slate);
  --bs-tertiary-bg-rgb:   16, 38, 54;
  --bs-tertiary-color:    #94a3b8;

  --bs-emphasis-color:    var(--flx-paper);
  --bs-secondary-color:   #94a3b8;
  --bs-heading-color:     var(--flx-paper);

  --bs-link-color:        #60a5fa;
  --bs-link-color-rgb:    96, 165, 250;
  --bs-link-hover-color:  var(--flx-amber);

  /* fixed: FLX-855 -- user, dark mode, FleetFix PM Plans: "the PM 1 Service with link
     is not visible." It was never that page. EVERY link in the app was affected.

     This whole block predates FLX-678 and overrides --bs-* tokens only -- it does not
     set a single --tblr-* token. The three --bs-link-* values directly above are
     correct and were being computed and then ignored, because FLX-678 pointed the
     app's links at Tabler's namespace instead: :root declares
     --tblr-link-color: var(--flx-blue), and nothing brought it down here. So links
     kept painting #0353A4 -- the LIGHT theme's brand blue -- on a near-black surface,
     at roughly 1.9:1 against the card. The AA floor is 4.5:1.

     #60a5fa is not a new colour and does not touch the 7-colour brand palette: it is
     what this block already chose for --bs-link-color, and what .btn-outline-primary
     already paints in dark mode further down. Mirroring it here makes the two
     namespaces agree rather than introducing a third value. Lands at ~6.5:1.

     A token, not a rule on `a`, so it flows to everything Tabler derives from it. */
  --tblr-link-color:       #60a5fa;
  --tblr-link-color-rgb:   96, 165, 250;
  --tblr-link-hover-color: var(--flx-amber);

  --bs-border-color:                rgba(255,255,255,0.10);
  --bs-border-color-translucent:    rgba(255,255,255,0.08);

  --bs-card-bg:               var(--flx-navy);
  --bs-card-cap-bg:           rgba(16,38,54,0.6);
  --bs-card-border-color:     rgba(255,255,255,0.08);
  --bs-card-color:            var(--flx-stone);

  --bs-table-color:           var(--flx-stone);
  --bs-table-border-color:    rgba(255,255,255,0.08);
  --bs-table-striped-bg:      rgba(255,255,255,0.03);
  --bs-table-hover-bg:        rgba(37,99,235,0.07);

  --bs-dropdown-bg:               var(--flx-navy);
  --bs-dropdown-border-color:     rgba(255,255,255,0.10);
  --bs-dropdown-link-color:       var(--flx-stone);
  --bs-dropdown-link-hover-color: var(--flx-paper);
  --bs-dropdown-link-hover-bg:    var(--flx-slate);
  --bs-dropdown-link-active-color:#fff;
  --bs-dropdown-link-active-bg:   var(--flx-navy);
  --bs-dropdown-divider-bg:       rgba(255,255,255,0.08);
  --bs-dropdown-header-color:     #94a3b8;

  --bs-modal-bg:              var(--flx-navy);
  --bs-modal-content-bg:      var(--flx-navy);
  --bs-modal-border-color:    rgba(255,255,255,0.10);

  --bs-offcanvas-bg:          var(--flx-navy);
  --bs-offcanvas-border-color:rgba(255,255,255,0.08);

  --bs-input-bg:              var(--flx-slate);
  --bs-input-border-color:    rgba(255,255,255,0.15);
  --bs-input-color:           var(--flx-stone);
  --bs-input-placeholder-color: #64748b;

  --bs-nav-tabs-border-color:              rgba(255,255,255,0.10);
  --bs-nav-tabs-link-hover-border-color:   rgba(255,255,255,0.08);
  --bs-nav-tabs-link-active-color:         var(--flx-paper);
  --bs-nav-tabs-link-active-bg:            var(--flx-steel);
  --bs-nav-tabs-link-active-border-color:  rgba(255,255,255,0.10);

  --bs-pagination-color:           var(--flx-stone);
  --bs-pagination-bg:              var(--flx-navy);
  --bs-pagination-border-color:    rgba(255,255,255,0.10);
  --bs-pagination-hover-color:     var(--flx-paper);
  --bs-pagination-hover-bg:        var(--flx-slate);
  --bs-pagination-hover-border-color: rgba(255,255,255,0.15);
  --bs-pagination-active-color:    #fff;
  --bs-pagination-active-bg:       var(--flx-navy);
  --bs-pagination-active-border-color: var(--flx-navy);
  --bs-pagination-disabled-color:  #64748b;
  --bs-pagination-disabled-bg:     var(--flx-navy);

  --bs-list-group-bg:             var(--flx-navy);
  --bs-list-group-border-color:   rgba(255,255,255,0.08);
  --bs-list-group-color:          var(--flx-stone);
  --bs-list-group-hover-bg:       var(--flx-slate);
  --bs-list-group-active-color:   #fff;
  --bs-list-group-active-bg:      var(--flx-navy);
  --bs-list-group-action-color:   var(--flx-stone);

  --bs-progress-bg:       var(--flx-steel);

  --bs-tooltip-bg:        var(--flx-stone);
  --bs-tooltip-color:     var(--flx-navy);
  --bs-popover-bg:        var(--flx-navy);
  --bs-popover-border-color: rgba(255,255,255,0.10);
  --bs-popover-header-bg: var(--flx-slate);
  --bs-popover-body-color:var(--flx-stone);

  scrollbar-color: var(--flx-steel) var(--flx-navy);
}

[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select,
[data-bs-theme="dark"] .input-group-text {
  background-color: var(--flx-slate);
  border-color: rgba(255,255,255,0.15);
  color: var(--flx-stone);
}
[data-bs-theme="dark"] .form-control::placeholder { color: #64748b; }

[data-bs-theme="dark"] .form-control:focus,
[data-bs-theme="dark"] .form-select:focus {
  background-color: var(--flx-slate);
  border-color: var(--flx-steel);
  color: var(--flx-stone);
  box-shadow: 0 0 0 0.2rem rgba(8,18,32,0.40);
}
[data-bs-theme="dark"] .form-check-input {
  background-color: var(--flx-steel);
  border-color: rgba(255,255,255,0.2);
}
[data-bs-theme="dark"] .form-check-input:checked {
  background-color: var(--flx-navy);
  border-color: var(--flx-navy);
}
[data-bs-theme="dark"] .table thead th {
  background-color: #0a192f;
  color: #94a3b8;
  border-color: rgba(255,255,255,0.08);
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
[data-bs-theme="dark"] .table td,
[data-bs-theme="dark"] .table th { border-color: rgba(255,255,255,0.06); }
[data-bs-theme="dark"] .table-hover > tbody > tr:hover > * { background-color: rgba(37,99,235,0.07); }
/* added: FLX-713 -- card headers are a coloured band with white text, so a card's title reads as
   a header rather than as more body content.
   modified: FLX-729 -- band is now --flx-card-header-bg (#215399), user-specified 2026-08-02
   | was: #111827. White on #215399 is 7.6:1, so titles/actions still clear WCAG AA comfortably.

   Everything below the first rule exists because the surface is coloured and Tabler colours a
   card-header's CONTENTS for a light surface. Without these, the change lands as "title looks
   great, and the button next to it disappeared" -- .btn-outline-secondary in particular is a
   dark border with dark text, which on the band is very nearly invisible. Cards carry titles,
   subtitles, muted counts, links, filter labels and action buttons, so each of those needs a
   deliberate value here, not a hope that inherit does the right thing. */
.card-header {
  padding: 0.5em 1em;
  /* fixed: FLX-1217 -- Tabler 1.4 sizes .card-header-tabs (Media Library, Navigation) from these
     two variables: the strip carries negative margins of -cap-y / -cap-x on every side so it fills
     the header edge to edge. Tabler's values are 1rem / 1.25rem while this header pads 0.5em / 1em,
     so the strip overflowed the header box and the sibling action button (Upload files, Add Nav
     Item) was pushed outside it. The variables now state the real padding, and the strip fits.
     Set on the header alone: .card-footer, which Tabler also sizes from them, keeps Tabler's values. */
  --tblr-card-cap-padding-y: 0.5em;
  --tblr-card-cap-padding-x: 1em;
  margin-bottom: 0;
  color: #fff;
  background-color: var(--flx-card-header-bg);
  border-bottom: var(--tblr-card-border-width) solid var(--tblr-card-border-color);
}
.card-header .card-title,
.card-header h1, .card-header h2, .card-header h3,
.card-header h4, .card-header h5, .card-header h6,
.card-header .form-label,
.card-header label {
  color: #fff;
}
/* Muted text keeps its "secondary information" meaning but re-based for a coloured ground.
   modified: FLX-729 -- lightened to --flx-card-header-muted (#CBD5E1) | was: #94A3B8, which
   measured 6.9:1 on the old near-black band but only 2.97:1 on #215399 -- a WCAG AA failure the
   colour change would otherwise have introduced silently on every card subtitle in the app. */
/* modified: FLX-1163 -- a .badge is a self-coloured component and must keep its own text colour:
   a badge that also carried `small` (Security Audit's "All endpoints OK") was matched here, and
   this rule sits later in the cascade at equal specificity, so it beat the badge's dark-green
   emphasis text with light grey on a pale green pill. Badges, and anything inside one, are
   excluded. | was: no :not(.badge) guard */
.card-header .text-muted:not(.badge):not(.alert),
.card-header .text-secondary:not(.badge):not(.alert),
.card-header .card-subtitle:not(.badge):not(.alert),
.card-header small:not(.badge):not(.alert),
.card-header .small:not(.badge):not(.alert) {
  color: var(--flx-card-header-muted) !important;
}
.card-header .badge .text-muted,
.card-header .badge small,
.card-header .badge .small,
.card-header .alert .text-muted,
.card-header .alert small,
.card-header .alert .small {
  color: inherit !important;
}
/* added: FLX-1163 -- the rest of what an app-wide scan of card-header contents found still
   inheriting a light-surface colour on the band. (1) .text-primary is brand blue on the blue
   band -- the SOC2 / Endpoint Health / Recent Tenants title icons were all but invisible; on the
   band it is white. A decorative icon in a title takes the title's colour whatever hue it was
   given (a green "Recent Users" icon on blue is no better); a muted hint icon stays muted.
   (2) Tabler's .card-header-tabs inside the band (Media, Navigation): an inactive tab is
   body-colour text on the band; re-based to the band's muted tone, white on hover, and the
   active tab keeps Tabler's own card-coloured face. (3) An .alert inside a header keeps its own
   text colour (the students tier-limit notice carried `small` and was being muted). */
.card-header:not(.card-header-light) .text-primary:not(.btn):not(.badge) { color: #fff !important; }
.card-header:not(.card-header-light) .card-title i[class*="text-"]:not(.text-muted) { color: inherit !important; }
/* fixed: FLX-1217 -- (2) assumed the tabs sat on the blue band. Tabler 1.4 paints the strip
   --tblr-bg-surface-tertiary (a light grey in light mode), so the band-muted tone was light on
   light and the inactive tabs all but disappeared. The strip is a light surface: inactive tabs
   take Tabler's own muted text colour, hover/focus the body colour, and the active tab -- an <a>
   on the Navigation page, which the `.card-header a:not(.btn)` rule below would paint stone --
   states the body colour explicitly. All three tokens follow dark mode.
   | was: inactive var(--flx-card-header-muted), hover #fff, active unstated */
/* modified: FLX-1221 -- the product owner reviewed FLX-1220's light variant against Services (blue
   band, white title, light button) and asked for one header look everywhere: Navigation and
   Media Library are back on the band. Tabler's strip masks a light header, so ON THE BAND the
   strip is made transparent and the band shows through it; inactive tabs are the band-muted
   tone with white on hover/focus (the FLX-1163 values), and the active tab keeps Tabler's white
   face with the body colour stated explicitly (on Navigation it is an <a>, which
   `.card-header a:not(.btn)` below would otherwise paint stone). A header that does carry
   .card-header-light keeps Tabler's own strip and the light-surface colours (FLX-1220's rules,
   now scoped to it).
   | was (FLX-1220): unscoped rules, inactive var(--tblr-secondary), hover var(--tblr-body-color) */
.card-header:not(.card-header-light) .card-header-tabs { background: transparent; }
.card-header:not(.card-header-light) .card-header-tabs .nav-link:not(.active) { color: var(--flx-card-header-muted); }
.card-header:not(.card-header-light) .card-header-tabs .nav-link:not(.active):hover,
.card-header:not(.card-header-light) .card-header-tabs .nav-link:not(.active):focus-visible { color: #fff; }
.card-header:not(.card-header-light) .card-header-tabs .nav-link.active { color: var(--tblr-body-color); }
.card-header.card-header-light .card-header-tabs .nav-link:not(.active) { color: var(--tblr-secondary); }
.card-header.card-header-light .card-header-tabs .nav-link:not(.active):hover,
.card-header.card-header-light .card-header-tabs .nav-link:not(.active):focus-visible { color: var(--tblr-body-color); }
.card-header.card-header-light .card-header-tabs .nav-link.active { color: var(--tblr-body-color); }
.card-header a:not(.btn) {
  color: var(--flx-stone);
}
.card-header a:not(.btn):hover {
  color: #fff;
}

/* fixed: FLX-821 -- a .dropdown-menu is a white floating popover, but in the DOM it sits INSIDE
   .card-header, so it inherited color:#fff from the band and, worse, the rule directly above
   painted every .dropdown-item #E5E7EB because a dropdown item is an <a> that is not a .btn.
   White and light-grey text on a white menu: the Manage Users actions were completely unreadable.

   Scoped to .dropdown-menu rather than fixed on one page: the menu is visually outside the band
   even though it is inside it structurally, so NONE of the header's colour rules should reach in.
   Every card-header dropdown in the app was affected, not only the one that was reported. */
.card-header .dropdown-menu {
  color: var(--tblr-body-color, #182433);
}
.card-header .dropdown-menu a:not(.btn),
.card-header .dropdown-item {
  color: var(--tblr-body-color, #182433);
}
.card-header .dropdown-menu a:not(.btn):hover,
.card-header .dropdown-item:hover,
.card-header .dropdown-item:focus {
  color: var(--tblr-body-color, #182433);
  background-color: var(--tblr-dropdown-link-hover-bg, #f1f5f9);
}
/* A disabled item must still be READABLE while looking unavailable -- it carries the tier-limit
   explanation in its title attribute, so a user has to be able to find it to hover it. */
.card-header .dropdown-item.disabled,
.card-header .dropdown-item:disabled,
.card-header .dropdown-menu .disabled {
  color: #94A3B8;
}
/* The muted/small override further up is !important and would otherwise repaint menu text in the
   band's light grey. Headers and helper text inside the menu belong to the menu, not the band. */
.card-header .dropdown-menu .text-muted,
.card-header .dropdown-menu .text-secondary,
.card-header .dropdown-menu small,
.card-header .dropdown-menu .small,
.card-header .dropdown-header {
  color: #64748B !important;
}

/* Buttons inside a dark header -- user directive 2026-08-01: "wherever the dark header is
   applied, ensure if there are any buttons in the header, make them lighter."
   Outline variants are the urgent case (dark-on-dark). Solid variants keep their semantic hue --
   a danger button must still read as danger -- but are guaranteed white text and a lightened
   face so they sit forward of the band rather than sinking into it. */
.card-header:not(.card-header-light) .btn-outline-secondary,
.card-header:not(.card-header-light) .btn-outline-primary,
.card-header:not(.card-header-light) .btn-outline-dark,
.card-header:not(.card-header-light) .btn-outline-light {
  --tblr-btn-color: var(--flx-stone);
  --tblr-btn-border-color: rgba(255,255,255,0.28);
  --tblr-btn-bg: transparent;
  --tblr-btn-hover-color: var(--flx-card-header-bg);
  --tblr-btn-hover-bg: var(--flx-stone);
  --tblr-btn-hover-border-color: var(--flx-stone);
  --tblr-btn-active-color: var(--flx-card-header-bg);
  --tblr-btn-active-bg: var(--flx-stone);
  --tblr-btn-active-border-color: var(--flx-stone);
  --tblr-btn-focus-shadow-rgb: 229,231,235;
}
/* modified: FLX-869 -- .btn-primary joins this group | was: grouped with the semantic colours
   below and "lightened" with filter: brightness(1.15).
   That works for info/success/warning/danger, which are mid-tone hues a 15% lift visibly
   brightens. It cannot work for primary: --tblr-primary is repointed to --flx-navy (#081220),
   and 1.15x near-black is still near-black, so a primary button in a header band rendered dark
   on blue. It was never a semantic colour -- it is the darkest value in the palette, and the one
   variant brightness() can do nothing for.
   Landing it here makes it render exactly like .btn-secondary in a band, which is precisely what
   FLX-735/736 has been asking people to type by hand on each new header. Now they need not. */
.card-header:not(.card-header-light) .btn-primary,
.card-header:not(.card-header-light) .btn-secondary,
.card-header:not(.card-header-light) .btn-dark,
.card-header:not(.card-header-light) .btn-light,
.card-header:not(.card-header-light) .btn-white {
  --tblr-btn-color: var(--flx-card-header-bg);
  --tblr-btn-bg: var(--flx-stone);
  --tblr-btn-border-color: var(--flx-stone);
  --tblr-btn-hover-color: var(--flx-card-header-bg);
  --tblr-btn-hover-bg: #fff;
  --tblr-btn-hover-border-color: #fff;
  --tblr-btn-active-color: var(--flx-card-header-bg);
  --tblr-btn-active-bg: #fff;
  --tblr-btn-active-border-color: #fff;
}
/* modified: FLX-869 -- .btn-primary removed from both groups below, having moved to the
   light-face group above. Left here it would keep its navy background and merely gain white
   text, which is the dark-on-blue button itself. The remaining four are genuine semantic hues
   that must keep their own colour -- a danger button has to still read as danger -- and are
   mid-tone enough for a brightness lift to actually do something. */
.card-header:not(.card-header-light) .btn-info,
.card-header:not(.card-header-light) .btn-success,
.card-header:not(.card-header-light) .btn-warning,
.card-header:not(.card-header-light) .btn-danger {
  --tblr-btn-color: #fff;
  --tblr-btn-hover-color: #fff;
  --tblr-btn-active-color: #fff;
  filter: brightness(1.15);
}
.card-header:not(.card-header-light) .btn-info:hover,
.card-header:not(.card-header-light) .btn-success:hover,
.card-header:not(.card-header-light) .btn-warning:hover,
.card-header:not(.card-header-light) .btn-danger:hover {
  filter: brightness(1.28);
}
/* Form controls that live in a card-header (FleetFix list pages keep their search and filters
   there, per FLX-697) stay on their own light face -- a transparent input on this band would be
   an invisible text field. Only the surrounding chrome is re-based. */
.card-header .form-control::placeholder {
  color: #6B7280;
}
.card-header .input-group-text {
  color: var(--flx-stone);
  background-color: rgba(255,255,255,0.08);
  border-color: rgba(255,255,255,0.18);
}

/* added: FLX-713 -- user-specified small muted paragraph. NOTE: `text-small` has zero call sites
   in the app today (the existing convention is `.small` / `.text-muted`), so this rule is
   currently dormant and takes effect wherever the class is first used. */
p.text-small.mb-0 {
  font-size: 12px;
  color: #94A3B8;
}

[data-bs-theme="dark"] .card {
  border-color: rgba(255,255,255,0.08);
  box-shadow: 0 1px 4px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04);
}
/* modified: FLX-713 -- was background-color: rgba(16,38,54,0.6), which in dark theme would have
   overridden the band and left card headers inconsistent between the two themes.
   Pinned to the same token so a header looks like a header in both (FLX-729). */
[data-bs-theme="dark"] .card-header {
  background-color: var(--flx-card-header-bg);
  border-bottom-color: rgba(255,255,255,0.06);
}
[data-bs-theme="dark"] .card-footer {
  border-top-color: rgba(255,255,255,0.06);
}
[data-bs-theme="dark"] .modal-content {
  background-color: var(--flx-navy);
  border-color: rgba(255,255,255,0.10);
  box-shadow: 0 8px 32px rgba(0,0,0,0.7);
}
[data-bs-theme="dark"] .modal-header,
[data-bs-theme="dark"] .modal-footer { border-color: rgba(255,255,255,0.08); }
[data-bs-theme="dark"] .btn-close { filter: invert(1) grayscale(100%) brightness(200%); }

/* added: FLX-725 -- until this block .btn-close was the ONLY button rule in the whole dark
   theme, so every btn-outline-* fell through to the Bootstrap default: a dark border and dark
   text, sitting on the #111827 card header FLX-713 introduced. Dark on dark, effectively
   invisible. Worst offenders were the FleetFix work-order status filter group and the
   Back / Edit Details footers.
   Colour choices are the light-theme hues lifted for a dark surface rather than new colours --
   the palette's own blue is too dark to read at #111827, so primary borrows the #60a5fa already
   used by the `code` rule below. No !important anywhere: [data-bs-theme] adds a level of
   specificity, so these win on their own. */
[data-bs-theme="dark"] .btn-outline-secondary {
  color: var(--flx-stone);
  border-color: rgba(255,255,255,0.18);
  background-color: transparent;
}
[data-bs-theme="dark"] .btn-outline-secondary:hover,
[data-bs-theme="dark"] .btn-outline-secondary:focus {
  color: #fff;
  border-color: rgba(255,255,255,0.30);
  background-color: rgba(255,255,255,0.10);
}
/* The btn-group filter tabs read as plain links unless the selected one is genuinely filled --
   a border-only "active" is indistinguishable from hover on a dark surface. */
[data-bs-theme="dark"] .btn-outline-secondary.active,
[data-bs-theme="dark"] .btn-check:checked + .btn-outline-secondary {
  color: #fff;
  border-color: rgba(255,255,255,0.34);
  background-color: rgba(255,255,255,0.16);
}

[data-bs-theme="dark"] .btn-outline-primary { color: #60a5fa; border-color: rgba(96,165,250,0.45); }
[data-bs-theme="dark"] .btn-outline-primary:hover,
[data-bs-theme="dark"] .btn-outline-primary:focus {
  color: #fff; border-color: #60a5fa; background-color: rgba(96,165,250,0.20);
}

[data-bs-theme="dark"] .btn-outline-danger { color: #f87171; border-color: rgba(248,113,113,0.45); }
[data-bs-theme="dark"] .btn-outline-danger:hover,
[data-bs-theme="dark"] .btn-outline-danger:focus {
  color: #fff; border-color: #f87171; background-color: rgba(248,113,113,0.20);
}

[data-bs-theme="dark"] .btn-outline-warning { color: #fbbf24; border-color: rgba(251,191,36,0.45); }
[data-bs-theme="dark"] .btn-outline-warning:hover,
[data-bs-theme="dark"] .btn-outline-warning:focus {
  color: var(--flx-navy); border-color: #fbbf24; background-color: #fbbf24;
}

[data-bs-theme="dark"] .btn-outline-success { color: #4ade80; border-color: rgba(74,222,128,0.45); }
[data-bs-theme="dark"] .btn-outline-success:hover,
[data-bs-theme="dark"] .btn-outline-success:focus {
  color: #062e13; border-color: #4ade80; background-color: #4ade80;
}

[data-bs-theme="dark"] .btn-outline-secondary:disabled,
[data-bs-theme="dark"] .btn-outline-primary:disabled,
[data-bs-theme="dark"] .btn-outline-danger:disabled,
[data-bs-theme="dark"] .btn-outline-warning:disabled,
[data-bs-theme="dark"] .btn-outline-success:disabled { opacity: 0.42; }

/* Modal footers use .btn-link.link-secondary for Cancel; Tabler's link-secondary resolves to a
   mid-grey that disappears against the #111827 modal surface. */
[data-bs-theme="dark"] .btn-link.link-secondary { color: #9AA6B8; }
[data-bs-theme="dark"] .btn-link.link-secondary:hover { color: var(--flx-stone); }
/* added: user directive -- the default no-photo-uploaded avatar placeholder
   (/assets/img/user-pic.png, StorageService::url()'s own fallback) is a dark silhouette,
   effectively invisible against the dark topbar/dropdown in dark mode. Scoped to that exact
   filename via attribute selector so a REAL uploaded profile photo (a completely different src,
   R2/local upload path) is never touched -- inverting an actual photo would look broken. */
[data-bs-theme="dark"] img[src$="user-pic.png"] { filter: invert(1) brightness(1.8); }
[data-bs-theme="dark"] hr { border-color: rgba(255,255,255,0.10); opacity: 1; }
[data-bs-theme="dark"] .badge.bg-light { background-color: rgba(230,234,240,0.14) !important; color: var(--flx-stone) !important; }
[data-bs-theme="dark"] .badge.bg-secondary { background-color: var(--flx-steel) !important; }
[data-bs-theme="dark"] code {
  color: #60a5fa;
  background: rgba(37,99,235,0.12);
  padding: 0.1em 0.38em;
  border-radius: 3px;
  font-family: var(--bs-font-monospace);
}
[data-bs-theme="dark"] .nav-tabs .nav-link { color: #94a3b8; border-color: transparent; }
[data-bs-theme="dark"] .nav-tabs .nav-link.active {
  color: var(--flx-paper);
  background-color: var(--flx-steel);
  border-color: rgba(255,255,255,0.10) rgba(255,255,255,0.10) var(--flx-steel);
}
[data-bs-theme="dark"] .nav-tabs .nav-link:hover { color: var(--flx-stone); }
[data-bs-theme="dark"] .breadcrumb-item.active    { color: #94a3b8; }
[data-bs-theme="dark"] .breadcrumb-item + .breadcrumb-item::before { color: #64748b; }
[data-bs-theme="dark"] .text-muted { color: #94a3b8 !important; }
[data-bs-theme="dark"] .text-secondary { color: #94a3b8 !important; }
[data-bs-theme="dark"] .border            { border-color: rgba(255,255,255,0.10) !important; }
[data-bs-theme="dark"] .border-bottom     { border-bottom-color: rgba(255,255,255,0.10) !important; }
[data-bs-theme="dark"] .border-top        { border-top-color:    rgba(255,255,255,0.10) !important; }
[data-bs-theme="dark"] .border-start      { border-left-color:   rgba(255,255,255,0.10) !important; }
[data-bs-theme="dark"] .border-end        { border-right-color:  rgba(255,255,255,0.10) !important; }

[data-bs-theme="dark"] .alert-success { background:#064e3b; border-color:#065f46; color:#d1fae5; }
[data-bs-theme="dark"] .alert-error,
[data-bs-theme="dark"] .alert-danger  { background:#7f1d1d; border-color:#991b1b; color:#fecaca; }
[data-bs-theme="dark"] .alert-warning { background:#78350f; border-color:#92400e; color:#fde68a; }
[data-bs-theme="dark"] .alert-info    { background:#1e3a5f; border-color:#1d4ed8; color:#bfdbfe; }
[data-bs-theme="dark"] .bg-body-tertiary  { background-color: var(--flx-slate) !important; }
[data-bs-theme="dark"] .bg-body-secondary { background-color: var(--flx-navy) !important; }
[data-bs-theme="dark"] .offcanvas,
[data-bs-theme="dark"] .offcanvas-md       { background-color: var(--flx-navy); border-color: rgba(255,255,255,0.08); }

/* modified: FLX-677 -- #sidebar (the Tabler navbar-vertical aside) now always carries its own
   data-bs-theme="dark" regardless of the page's own light/dark toggle (brand palette's
   Slate/Steel tokens were already documented as "sidebar (dark)" -- this makes that real), so
   these rules must be unconditional, not gated on an ancestor [data-bs-theme] match -- the old
   [data-bs-theme="light"] #sidebar .nav-link variant (further below) is now dead/wrong and has
   been removed rather than left to fight this one on cascade order. */
/* added: FLX-679 -- Tabler's own dark navbar-vertical resolves its background through
   --tblr-bg-surface -> --tblr-gray-800 (#1F2937, coincidentally == --flx-slate), not
   --flx-navy (#081220) -- the locked Command Center artifact's sidebar uses navy specifically,
   a visibly richer/darker tone. Never explicitly set before now, so the sidebar had quietly been
   rendering lighter than designed this entire migration. */
#sidebar {
  background-color: var(--flx-navy) !important;
  /* added: user directive -- Tabler's own compiled default (`.navbar-vertical.navbar-expand-lg
     {width:15rem}`, 240px) left a mismatched gap against this rule's own width/padding intent.
     max-width caps the real rendered width (border-box, so the 1em side padding is included in
     it, not added on top) -- .page's own padding-left below is kept in sync with this value. */
  max-width: 270px;
  padding: 0em 1em;
  width: 100%;
  /* fixed: user directive -- wire up Tabler's own native "expanded group contains the current
     page" indicator (`.nav-item.active::after`, already compiled into tabler.min.css, see
     body_end_plugins.php's markActivePage() which now adds the real `active` class to the
     ancestor <li> instead of a custom class) rather than a hand-rolled accent bar. Tabler's
     default pulls this border's color from `--tblr-primary`, which this app repoints to
     `--flx-navy` app-wide (unrelated original reason, see the Quick Create button fix above) --
     on this sidebar's own navy background that would render navy-on-navy and vanish, so it's
     recolored to the brand blue here, scoped to #sidebar only. */
  --tblr-navbar-active-border-color: var(--flx-blue);
}
/* the collapsed icon rail deliberately has no separate group-indicator line (round 2/3 decision,
   compared live against the Tabler reference -- the icon rail there has none either, only the
   centered .active pill on the current icon itself), so Tabler's native accent line (now firing
   because of the real `.active` class on the ancestor <li>, see above) is suppressed there. */
body.sidebar-collapsed #sidebar-menu .nav-item.active::after {
  display: none !important;
}
#sidebar .nav-link {
  color: var(--flx-stone) !important;
}
#sidebar .nav-link:hover {
  color: var(--flx-paper) !important;
  background-color: rgba(255,255,255,0.06);
  border-radius: 6px;
}
/* modified: FLX-679 -- was a faint translucent-white background, indistinguishable from
   :hover at a glance. Nothing ever added this class in the first place (see body_end_plugins.php
   for the new active-page-detection script that now does) -- the locked Command Center artifact
   uses a solid brand-blue pill for the current page, a much stronger signal. */
#sidebar .nav-link.active {
  color: #fff !important;
  background-color: var(--flx-blue);
  border-radius: 8px;
}
/* removed: round 3 -- the custom `.nav-parent-active` class/rule (a hand-rolled white-text +
   blue-icon treatment for "this group contains the active page") is gone. Tabler already has a
   real pattern for exactly this (`<li class="nav-item active dropdown">` in its own
   layout-fluid-vertical.html reference, compiled as `.nav-item.active .nav-link{color:var(
   --tblr-navbar-active-color)}` plus the `::after` accent border in `tabler.min.css`) --
   body_end_plugins.php's markActivePage() now adds the real `active` class to the ancestor <li>
   itself instead of a custom class on the inner link, so Tabler's own compiled rule does this
   for free; no bespoke CSS needed here at all. */
#sidebar .nav-link.active:hover {
  background-color: var(--flx-blue);
  color: #fff !important;
}

[data-bs-theme="dark"] .breadcrumb-item a {
  color: #94a3b8;
}
[data-bs-theme="dark"] .breadcrumb-item a:hover {
  color: var(--flx-stone);
}

[data-bs-theme="dark"] .dataTables_wrapper .dataTables_info,
[data-bs-theme="dark"] .dataTables_wrapper .dataTables_length label { color: #94a3b8; }
[data-bs-theme="dark"] .dataTables_wrapper .dataTables_filter input {
  background-color: var(--flx-slate);
  border-color: rgba(255,255,255,0.15);
  color: var(--flx-stone);
}

[data-bs-theme="dark"] .page-link { background-color: var(--flx-navy); border-color: rgba(255,255,255,0.10); color: var(--flx-stone); }
[data-bs-theme="dark"] .page-link:hover { background-color: var(--flx-slate); color: var(--flx-paper); }

[data-bs-theme="dark"] .page-item.active .page-link   { background-color: var(--flx-navy); border-color: var(--flx-navy); }
[data-bs-theme="dark"] .page-item.disabled .page-link { background-color: var(--flx-navy); color: #64748b; }

[data-bs-theme="dark"] #studentResults .list-group-item {
  background-color: var(--flx-slate);
  color: var(--flx-stone);
  border-color: rgba(255,255,255,0.08);
}

[data-bs-theme="dark"] .media-item { border-color: rgba(255,255,255,0.10); background: #0d1e36; }
[data-bs-theme="dark"] .media-file-placeholder { background: var(--flx-slate); color: #94a3b8; }
[data-bs-theme="dark"] .media-footer { color: var(--flx-stone); }

[data-bs-theme="dark"] .fc .fc-daygrid-day-number,
[data-bs-theme="dark"] .fc .fc-col-header-cell-cushion { color: var(--flx-stone); }
[data-bs-theme="dark"] .fc-theme-standard td,
[data-bs-theme="dark"] .fc-theme-standard th     { border-color: rgba(255,255,255,0.08); }
[data-bs-theme="dark"] .fc-theme-standard .fc-scrollgrid { border-color: rgba(255,255,255,0.08); }

[data-bs-theme="dark"] .fc .fc-button-primary:focus {
  box-shadow: 0 0 0 0.2rem rgba(8,18,32,0.50);
}
[data-bs-theme="dark"] .fc .fc-button-primary:not(:disabled).fc-button-active,
[data-bs-theme="dark"] .fc .fc-button-primary:not(:disabled):active {
  background-color: var(--flx-slate);
  border-color: var(--flx-slate);
}

[data-bs-theme="dark"] ::-webkit-scrollbar            { width: 6px; height: 6px; }
[data-bs-theme="dark"] ::-webkit-scrollbar-track      { background: var(--flx-navy); }
[data-bs-theme="dark"] ::-webkit-scrollbar-thumb      { background: var(--flx-steel); border-radius: 3px; }
[data-bs-theme="dark"] ::-webkit-scrollbar-thumb:hover{ background: var(--flx-slate); }

[data-bs-theme="dark"] .stat-card:hover { background-color: var(--flx-slate) !important; }

[data-bs-theme="light"] {

  --bs-body-bg:           var(--flx-paper);
  --bs-body-bg-rgb:       247, 248, 250;
  --bs-body-color:        var(--flx-navy);
  --bs-body-color-rgb:    8, 18, 32;
  --bs-secondary-bg:      var(--flx-stone);
  --bs-secondary-bg-rgb:  230, 234, 240;
  --bs-tertiary-bg:       var(--flx-stone);
  --bs-tertiary-bg-rgb:   230, 234, 240;
  --bs-tertiary-color:    var(--flx-steel);

  --bs-emphasis-color:    var(--flx-navy);
  --bs-secondary-color:   var(--flx-steel);
  --bs-heading-color:     var(--flx-navy);

  --bs-link-color:        var(--flx-slate);
  --bs-link-color-rgb:    37, 99, 235;
  --bs-link-hover-color:  var(--flx-amber);

  --bs-border-color:               var(--flx-stone);
  --bs-border-color-translucent:   rgba(8,18,32,0.10);

  --bs-card-bg:               #FFFFFF;
  --bs-card-cap-bg:           rgba(230,234,240,0.5);
  --bs-card-border-color:     var(--flx-stone);

  --bs-table-color:           var(--flx-navy);
  --bs-table-border-color:    var(--flx-stone);
  --bs-table-striped-bg:      rgba(8,18,32,0.025);
  --bs-table-hover-bg:        rgba(37,99,235,0.05);

  --bs-dropdown-bg:             #FFFFFF;
  --bs-dropdown-border-color:   var(--flx-stone);
  --bs-dropdown-link-color:     var(--flx-navy);
  --bs-dropdown-link-hover-bg:  var(--flx-stone);
  --bs-dropdown-link-active-bg: var(--flx-navy);

  --bs-modal-bg:          #FFFFFF;
  --bs-modal-content-bg:  #FFFFFF;

  --bs-pagination-active-bg:           var(--flx-navy);
  --bs-pagination-active-border-color: var(--flx-navy);

  --bs-nav-tabs-link-active-color: var(--flx-navy);

  --bs-offcanvas-bg: #FFFFFF;

  --bs-input-border-color:      #bdc5d0;
  --bs-input-placeholder-color: #adb8c4;
}

[data-bs-theme="light"] .table thead th {
  background-color: var(--flx-paper);
  color: var(--flx-steel);
  border-color: var(--flx-stone);
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
[data-bs-theme="light"] .card {
  box-shadow: 0 1px 3px rgba(8,18,32,0.08);
}

.form-label { font-weight: 400 !important; }

[data-bs-theme="light"] .form-control,
[data-bs-theme="light"] .form-select {
  border-color: #bdc5d0;
}

[data-bs-theme="light"] .form-control::placeholder,
[data-bs-theme="light"] .form-select::placeholder {
  color: #adb8c4;
}

[data-bs-theme="light"] .form-control:focus,
[data-bs-theme="light"] .form-select:focus {
  border-color: var(--flx-navy);
  box-shadow: 0 0 0 0.2rem rgba(8,18,32,0.15);
}
[data-bs-theme="light"] .bg-body-tertiary  { background-color: var(--flx-stone) !important; }
[data-bs-theme="light"] .bg-body-secondary { background-color: var(--flx-stone) !important; }
[data-bs-theme="light"] ::-webkit-scrollbar            { width: 6px; height: 6px; }
[data-bs-theme="light"] ::-webkit-scrollbar-track      { background: var(--flx-paper); }
[data-bs-theme="light"] ::-webkit-scrollbar-thumb      { background: var(--flx-stone); border-radius: 3px; }
[data-bs-theme="light"] ::-webkit-scrollbar-thumb:hover{ background: #94a3b8; }

[data-bs-theme="light"] .breadcrumb-item a {
  color: var(--flx-steel) !important;
}
[data-bs-theme="light"] .breadcrumb-item a:hover {
  color: var(--flx-navy) !important;
}

[data-bs-theme="light"] .page-link {
  color: var(--flx-steel) !important;
  border-color: #dee2e6;
}
[data-bs-theme="light"] .page-link:hover {
  color: var(--flx-navy) !important;
  background-color: var(--flx-paper);
  border-color: var(--flx-stone);
}
[data-bs-theme="light"] .page-item.active .page-link {
  background-color: var(--flx-navy) !important;
  border-color: var(--flx-navy) !important;
  color: #fff !important;
}
[data-bs-theme="light"] .page-item.disabled .page-link {
  color: #9ca3af !important;
  background-color: #fff;
  border-color: #dee2e6;
}

.expires-label { font-size: 8pt; line-height: 1.3; }

[data-bs-theme="light"] .nav-tabs .nav-link {
  color: var(--flx-steel);
}
[data-bs-theme="light"] .nav-tabs .nav-link:hover {
  color: var(--flx-navy);
}
[data-bs-theme="light"] .nav-tabs .nav-link.active {
  color: var(--flx-navy);
  font-weight: 600;
}

.dt-utc-hint{font-size:.72em;color:#6c757d;}

.ack-section .form-check-label a{color:#dc3545;}
.ack-section .form-check-label a:hover{color:#a71d2a;}

input[readonly],select[readonly],textarea[readonly]{background-color:#FCE5E3!important;}
[data-bs-theme="dark"] input[readonly],[data-bs-theme="dark"] select[readonly],[data-bs-theme="dark"] textarea[readonly]{background-color:#4a1f1d!important;}

.cal-color-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:10px;margin-top:6px;}
.cal-color-card{appearance:none;font:inherit;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:64px;border-radius:12px;border:2px solid rgba(0,0,0,.08);padding:4px 4px 6px;margin:0;cursor:pointer;transition:transform .12s,box-shadow .12s,border-color .12s;}
.cal-color-card:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.14);}
.cal-color-card:focus-visible{outline:2px solid var(--flx-blue);outline-offset:2px;}
.cal-color-name{font-size:.65rem;font-weight:600;color:#3a3a3a;background:rgba(255,255,255,.7);border-radius:6px;padding:1px 6px;line-height:1.4;white-space:nowrap;user-select:none;}
.cal-color-check{position:absolute;top:5px;right:5px;width:18px;height:18px;border-radius:50%;background:var(--flx-navy);color:#fff;font-size:.6rem;display:none;align-items:center;justify-content:center;box-shadow:0 0 0 2px #fff;}
.cal-color-card--selected{border-color:var(--flx-navy);box-shadow:0 0 0 2px #fff,0 0 0 4px var(--flx-navy);}
.cal-color-card--selected .cal-color-check{display:flex;}
.cal-color-lock{position:absolute;top:5px;left:5px;width:18px;height:18px;border-radius:50%;background:rgba(255,255,255,.85);color:#6c757d;font-size:.62rem;display:none;align-items:center;justify-content:center;}
.cal-color-card--taken{cursor:not-allowed;opacity:.55;filter:grayscale(40%);}
.cal-color-card--taken .cal-color-lock{display:flex;}
[data-bs-theme="dark"] .cal-color-card--selected{border-color:var(--flx-amber);box-shadow:0 0 0 2px var(--flx-slate),0 0 0 4px var(--flx-amber);}
[data-bs-theme="dark"] .cal-color-card--selected .cal-color-check{background:var(--flx-amber);}
[data-bs-theme="dark"] .cal-color-name{color:#1a1a1a;}
.cal-color-summary{font-size:.8rem;font-weight:600;margin-bottom:6px;display:flex;align-items:center;gap:6px;}

.license-alert-item.license-alert-expiring{background-color:#f0cf65;color:#000;}
.license-alert-item.license-alert-expired{background-color:#ffcad4;color:#a40606;}
.license-alert-badge.license-alert-expiring{background-color:#f0cf65;color:#000;}
.license-alert-badge.license-alert-expired{background-color:#ffcad4;color:#a40606;}

.dashboard-widget-link{display:block;color:inherit;text-decoration:none;}
.dashboard-widget-link .card{transition:box-shadow .15s,border-color .15s,transform .1s;cursor:pointer;}
.dashboard-widget-link:hover .card,.dashboard-widget-link:focus-visible .card{box-shadow:0 .25rem .75rem rgba(8,18,32,.1);border-color:rgba(13,110,253,.35);transform:translateY(-1px);}

/* Bootstrap only rounds a .list-group's bottom corners when it is a direct child of .card; nested one level inside card-body.p-0 it stays square-cornered against the card's rounded edge. */
.card-body.p-0>.list-group.list-group-flush:last-child>.list-group-item:last-child,
.card-body.p-0>.card-footer:last-child,
.card-body.p-0>.topic-row:last-child{
    border-bottom-left-radius:var(--bs-card-inner-border-radius);
    border-bottom-right-radius:var(--bs-card-inner-border-radius);
}
.list-scroll-panel{
    max-height:280px;
    overflow-y:auto;
    overflow-x:hidden;
    border-bottom-left-radius:var(--bs-card-inner-border-radius);
    border-bottom-right-radius:var(--bs-card-inner-border-radius);
}
.dashboard-clickable-card{cursor:pointer;transition:box-shadow .15s ease-in-out,border-color .15s ease-in-out;}
/* fixed: user directive -- match the locked artifact's tone/colors exactly. Was Bootstrap's
   generic default blue (#0d6efd/rgb(13,110,253)), not this app's own --flx-blue brand token --
   a real color-consistency gap on every dashboard widget's hover/focus state. */
.dashboard-clickable-card:hover,.dashboard-clickable-card:focus-visible{box-shadow:0 0 0 2px rgba(3,83,164,.25);border-color:rgba(3,83,164,.4);}
.dashboard-clickable-card .h4{font-variant-numeric:tabular-nums;letter-spacing:-0.01em;}

.wsel-container{width:100%;max-width:480px;}
.wsel-card{border:1px solid var(--flx-stone);border-radius:16px;box-shadow:0 8px 24px rgba(8,18,32,.08);}
.wsel-title{display:flex;align-items:center;gap:.5rem;}
.wsel-search-wrap{position:relative;}
.wsel-search-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:#6c757d;pointer-events:none;}
.wsel-search{padding-left:36px!important;border-radius:10px;border-color:var(--flx-stone);}
.wsel-search:focus{border-color:var(--flx-blue);box-shadow:0 0 0 .2rem rgba(3,83,164,.15);}
.wsel-list{display:flex;flex-direction:column;gap:8px;}
.wsel-item{appearance:none;font:inherit;cursor:pointer;margin:0;display:flex;align-items:center;gap:12px;width:100%;padding:12px 14px;border-radius:12px;border:1px solid var(--flx-stone);background:#fff;text-align:left;transition:transform .12s,box-shadow .12s,border-color .12s,background-color .12s;}
.wsel-item:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(8,18,32,.08);border-color:#c7ccd4;}
.wsel-item:focus-visible{outline:none;box-shadow:0 0 0 .2rem rgba(3,83,164,.25);}
.wsel-item.is-active{border-color:var(--flx-blue);background:rgba(3,83,164,.06);}
.wsel-avatar{flex-shrink:0;width:42px;height:42px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:1rem;color:#fff;}
.wsel-avatar--0{background:var(--flx-blue);}
.wsel-avatar--1{background:var(--flx-amber);}
.wsel-avatar--2{background:var(--flx-steel);}
.wsel-avatar--3{background:var(--flx-slate);}
.wsel-info{min-width:0;flex:1 1 auto;}
.wsel-name{font-weight:600;color:var(--flx-navy);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.wsel-role{font-size:.78rem;color:#6c757d;}
.wsel-active-pill{flex-shrink:0;display:inline-flex;align-items:center;gap:4px;font-size:.72rem;font-weight:600;color:var(--flx-blue);background:rgba(3,83,164,.12);border-radius:999px;padding:3px 10px;}
.wsel-chevron{flex-shrink:0;width:16px;height:16px;color:#adb5bd;opacity:0;transform:translateX(-4px);transition:opacity .12s,transform .12s;}
.wsel-item:hover .wsel-chevron,.wsel-item:focus-visible .wsel-chevron{opacity:1;transform:translateX(0);}
.wsel-empty{display:flex;flex-direction:column;align-items:center;gap:6px;padding:24px 8px;color:#6c757d;}
.wsel-empty svg{width:28px;height:28px;opacity:.5;}
.wsel-signout{color:#6c757d;text-decoration:none;}
.wsel-signout:hover{color:var(--flx-steel);text-decoration:underline;}
.cal-color-summary-dot{width:12px;height:12px;border-radius:50%;display:inline-block;flex-shrink:0;}

/* FLX-367 -- instructor filter toggle pills on the Bookings calendar, merged into the instructor color legend */
.instr-filter-pill{--pill-color:#6c757d;display:inline-flex;align-items:center;gap:6px;font-size:.8rem;font-weight:600;line-height:1;padding:6px 12px;border-radius:999px;border:1px solid var(--flx-stone);background:#fff;color:var(--flx-steel);cursor:pointer;transition:border-color .12s,background-color .12s,box-shadow .12s,color .12s;}
.instr-filter-pill:hover{border-color:var(--pill-color);}
.instr-filter-pill:focus-visible{outline:none;box-shadow:0 0 0 .2rem rgba(37,99,235,.25);}
.instr-filter-pill.active{border-color:var(--pill-color);background:var(--pill-color);color:#fff;box-shadow:0 1px 3px rgba(8,18,32,.12);}
.instr-filter-pill--all{--pill-color:var(--flx-blue);}
.instr-filter-dot{width:8px;height:8px;border-radius:50%;background:var(--pill-color);display:inline-block;flex-shrink:0;}
.instr-filter-pill.active .instr-filter-dot{background:#fff;}

/* FLX-376 -- myaccount.php Online Now widget, moved out of a page-level <style> block. Colors
   use var(--bs-success)/var(--bs-primary) instead of hardcoded hex so they follow the same
   tokens as every other status dot/link in the app instead of a private one-off value. */
.online-pulse-dot{display:inline-block;width:8px;height:8px;background:var(--bs-success);border-radius:50%;position:relative;flex-shrink:0;}
.online-pulse-dot::after{content:'';position:absolute;top:-3px;left:-3px;width:14px;height:14px;border-radius:50%;background:rgba(var(--bs-success-rgb),.35);animation:oc-pulse 1.8s ease-out infinite;}
@keyframes oc-pulse{0%{transform:scale(.6);opacity:1;}100%{transform:scale(1.6);opacity:0;}}
.online-now-card:hover{box-shadow:0 0 0 2px rgba(var(--bs-success-rgb),.4)!important;border-color:rgba(var(--bs-success-rgb),.6)!important;}
.oc-sort.active-asc .oc-sort-icon,
.oc-sort.active-desc .oc-sort-icon{color:var(--bs-primary)!important;}
.oc-avatar{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;font-size:.6rem;font-weight:700;flex-shrink:0;color:#fff;}
#ocRefreshIcon.spinning{animation:oc-spin .7s linear infinite;}
@keyframes oc-spin{to{transform:rotate(360deg);}}
#onlineNowOffcanvas{width:min(700px,96vw);}
@media (min-width:1200px){#onlineNowOffcanvas{width:50vw;min-width:560px;}}
.oc-browser-chip{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:3px;font-size:.55rem;font-weight:700;color:#fff;flex-shrink:0;cursor:default;}
#ocTable thead th{font-size:.72rem;}
#onlineNowOffcanvas .form-control-sm,
#onlineNowOffcanvas .form-select-sm{font-size:.75rem;padding:.2rem .4rem;}
#ocRefreshCountdown{font-size:.72rem;}

/* FLX-377 -- thin progress bar for the Students roster's Package Hours column */
.students-pkg-progress{height:6px;}

/* FLX-378 -- generic fixed-width utilities, replacing scattered inline style="max-width:Npx"/
   style="min-width:Npx" attributes (Bookings page Pending Requests table, instructor search box,
   bulk/coverage reassign selects, reschedule duration input, legend divider) */
.mw-160{max-width:160px;}
.mw-180{max-width:180px;}
.mw-220{max-width:220px;}
.mw-240{max-width:240px;}
.minw-210{min-width:210px;}
.w-90{width:90px;}
.h-18{height:18px;}
.legend-dot{width:10px;height:10px;border-radius:50%;display:inline-block;flex-shrink:0;}
.legend-dot--unassigned{background:#6c757d;}
.legend-dot--completed{background:#198754;}
.legend-dot--noshow{background:#dc3545;}
.legend-dot--pending{width:10px;height:10px;border-radius:2px;border:2px solid #fd7e14;background:rgba(253,126,20,.15);display:inline-block;flex-shrink:0;}

/* FLX-380 -- applications.php inline style cleanup */
.badge-xxs{font-size:.6rem;vertical-align:middle;}
.badge-xs{font-size:.7rem;}
.ls-tight{letter-spacing:.04em;}
.avatar-circle-44{width:44px;height:44px;font-size:.85rem;}
.sig-preview{max-width:320px;}
.timeline-th-w{width:45%;}

/* FLX-384 -- tenant admin pages inline style cleanup */
.progress-h14{height:14px;}
.avatar-circle-46{width:46px;height:46px;font-size:1.1rem;}

/* FLX-385 -- creator/roles-edit.php inline style cleanup */
.fs-065{font-size:.65rem;}

/* FLX-407 -- myaccount.php dashboard drill-down widgets. .dashboard-clickable-card already
   covers the card-level click affordance; this covers the smaller "in progress" badge trigger
   and the drill-down modal's table so its DataTable controls don't feel cramped against the
   modal-body edge. */
.badge[data-widget]{cursor:pointer;}
.badge[data-widget]:hover,.badge[data-widget]:focus-visible{box-shadow:0 0 0 2px rgba(13,110,253,.35);}
#dashboardDrilldownModal .modal-body{padding-top:1rem;}
#dashboardDrilldownModal .table-responsive{margin-top:.5rem;}

/* FLX-436 -- shimmering skeleton placeholder, used by the dashboard drill-down modal's loading state */
.flx-skeleton{display:inline-block;height:.9em;min-width:60px;border-radius:4px;background:linear-gradient(90deg,var(--flx-stone) 25%,#f3f4f6 37%,var(--flx-stone) 63%);background-size:400% 100%;animation:flx-skeleton-shimmer 1.4s ease infinite;vertical-align:middle;}
@keyframes flx-skeleton-shimmer{0%{background-position:100% 50%}100%{background-position:0 50%}}
[data-bs-theme="dark"] .flx-skeleton{background:linear-gradient(90deg,var(--flx-steel) 25%,#4b5563 37%,var(--flx-steel) 63%);background-size:400% 100%;}

/* FLX-616 -- Media Library UX overhaul (modules/website/media.php). WordPress-style dense grid:
   image fills the tile, filename/size/type + action buttons only appear on hover/focus instead
   of always-on card chrome. */
/* fixed: FLX-867 -- the grid once rendered ONE full-width tile per row instead of ~9 at 150px:
   a single ~1500px square filling the card. NOT ROOT-CAUSED at the time. The mechanism the symptom
   fit was repeat(auto-fill, minmax(150px,1fr)) collapsing to a single track against an INDEFINITE
   container width, after which 1fr resolves to max-content and the track grows to the image's
   intrinsic width. What made an ancestor shrink-to-fit was never identified, so width:100% was
   applied to make the width definite and a max-width:240px guard was put on the tile.

   modified: FLX-1092 -- that failure mode is now structurally impossible rather than guarded
   against, which is why the guard is gone. Two changes do it: the track count is EXPLICIT per
   breakpoint, so there is no auto-fill computation left to collapse; and every track is
   minmax(0,1fr) rather than 1fr, whose zero minimum is precisely what stops a track resolving to
   its content's intrinsic width. width:100% and min-width:0 are kept -- they cost nothing and the
   original cause was never actually found.

   fixed: FLX-1217 -- the cause is found: the #mediaGrid ID rule near the top of this file (the
   events-edit Asset Library modal, same element id) outranked this class and turned the grid into
   a flex-wrap box. That rule is now scoped to its modal; nothing here needed to change.

   Eight per row on desktop, per user direction. */
.flx-media-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.5rem;width:100%;min-width:0;}
@media (min-width:576px){.flx-media-grid{grid-template-columns:repeat(3,minmax(0,1fr));}}
@media (min-width:768px){.flx-media-grid{grid-template-columns:repeat(4,minmax(0,1fr));}}
@media (min-width:992px){.flx-media-grid{grid-template-columns:repeat(6,minmax(0,1fr));}}
@media (min-width:1200px){.flx-media-grid{grid-template-columns:repeat(8,minmax(0,1fr));}}
/* The tile itself no longer clips: the "more actions" dropdown lives inside the hover overlay and
   would be cut off by overflow:hidden. The frame below does the clipping instead, and an open menu
   lifts its tile above its neighbours. */
.flx-media-tile{position:relative;min-width:0;}
.flx-media-tile:focus-within{z-index:20;}
.flx-media-tile-frame{position:relative;aspect-ratio:1/1;border-radius:.5rem;overflow:hidden;background:var(--flx-paper);box-shadow:var(--flx-shadow-xs);}
.flx-media-tile img{width:100%;height:100%;object-fit:contain;display:block;padding:.5rem;}
.flx-media-tile-icon{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:var(--flx-stone);}
.flx-media-tile-badge{position:absolute;top:.35rem;left:.35rem;width:.5rem;height:.5rem;border-radius:50%;box-shadow:0 0 0 2px #fff;z-index:2;}
.flx-media-tile-badge.is-private{background:#6c757d;}
.flx-media-tile-badge.is-public{background:#198754;}
.flx-media-tile-overlay{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:space-between;padding:.35rem;background:rgba(8,18,32,.72);opacity:0;transition:opacity .15s var(--flx-ease);}
.flx-media-tile:hover .flx-media-tile-overlay,.flx-media-tile:focus-within .flx-media-tile-overlay{opacity:1;}
.flx-media-tile-actions{display:flex;gap:.2rem;justify-content:flex-end;}
/* Type scale unchanged from FLX-616 -- the tiles got smaller, the labels did not. */
.flx-media-tile-actions .btn{width:1.75rem;height:1.75rem;padding:0;line-height:1;font-size:.72rem;background:#fff;border-color:transparent;}
.flx-media-tile-actions .btn:hover{background:#fff;border-color:transparent;filter:brightness(.92);}
.flx-media-tile-info{color:#fff;min-width:0;}
.flx-media-tile-name{font-size:.74rem;font-weight:600;}
.flx-media-tile-meta{font-size:.66rem;opacity:.85;}
.flx-media-tile .dropdown-menu{font-size:.78rem;}

/* FLX-616 -- tag-chip filter bar above the grid; chips just type into the existing Search box */
.flx-tag-bar{display:flex;flex-wrap:wrap;gap:.375rem;}
.flx-tag-chip{border:1px solid var(--flx-stone);background:#fff;border-radius:999px;padding:.2rem .65rem;font-size:.72rem;color:#495057;cursor:pointer;display:inline-flex;align-items:center;gap:.3rem;}
.flx-tag-chip:hover{border-color:#adb5bd;}
.flx-tag-chip.active{background:var(--bs-primary);border-color:var(--bs-primary);color:#fff;}
.flx-tag-chip-count{opacity:.65;font-size:.65rem;}
.flx-tag-chip.active .flx-tag-chip-count{opacity:.85;}

/* FLX-1092 -- storage read-out in the Media Library card footer, opens #mediaStorageModal.
   Replaces .flx-storage-pill, the bordered button that sat in the card header: as a bordered
   control among real controls it read as an action, which it is not. Here it is a quiet figure on
   the same line as the item count, and it has no border because nothing is being offered. */
.flx-storage-link{display:inline-flex;flex-direction:column;align-items:flex-start;gap:3px;min-width:10rem;border:0;}
.flx-storage-link .progress{width:100%;}

/* FLX-660 -- (i) tooltip icon replacing a form-text hint line under a field label */
.flx-hint-icon{cursor:help;font-size:.85em;}

/* fixed: FLX-734 -- a card-header toolbar's search box always dropped onto a second row beneath
   the filter pills. Not a width problem: .form-control is width:100%, and a flex item's default
   flex-basis:auto resolves to that declared width, so the input asks for the entire row and
   flex-wrap has no choice but to give it one. The inline max-width:240px call sites capped how
   wide it *rendered* but never what it *asked for*, which is why the bug survived every attempt
   to fix it by making the box smaller. Toolbar controls now size from content instead. */
.card-header .form-control,
.card-header .form-select,
.card-header .input-icon {
  flex: 0 1 auto;
  width: auto;
}
/* .input-icon is a positioning wrapper -- the input inside still fills it, and the wrapper is
   what carries .flx-toolbar-search's cap. */
.card-header .input-icon > .form-control {
  width: 100%;
}
/* Bootstrap's own input-group sizing, restored: the rule above would otherwise break every
   grouped control in a header (they must stay flex:1 1 auto / width:1% to share the group). */
.card-header .input-group > .form-control,
.card-header .input-group > .form-select {
  flex: 1 1 auto;
  width: 1%;
}
/* A real basis rather than max-width alone, so the search box holds one consistent width across
   every list page instead of collapsing to its 20-character intrinsic size. */
/* modified: FLX-740 -- was `flex: 0 1 15rem`, i.e. a flex-basis, which still leaves the box
   shrinkable and still leaves its line-breaking decision to intrinsic sizing. A toolbar search is
   a fixed-size control: give it a real width and take it out of the flex algorithm entirely, so
   it can only ever move to a second row when the row is genuinely full. */
.card-header .flx-toolbar-search {
  flex: 0 0 auto;
  width: 15rem;
  max-width: 100%;
}

/* fixed: FLX-743 -- Tabler's .card-actions pulls itself -.5rem to the right, sized against
   Tabler's own 1.25rem cap padding (leaving 0.75rem of real gap). This app's card-header padding
   is 1em, so the same -.5rem left an action button roughly 5px from the card border -- visibly
   touching it on Tax Settings. Keep the vertical pull (it is what keeps a button from growing the
   header's height) and drop the horizontal one. */
.card-header .card-actions {
  margin-right: 0;
}

/* fixed: FLX-742 -- Tabler's `-lt` badge variants are a pale tint sized for a white card body.
   On the saturated header band they render as an opaque light block that reads like a rendering
   artefact rather than a label (reported on Tax Settings' "Tenant default"). Re-based to a
   translucent white chip. Deliberately scoped to the `-lt` tints only: a semantic badge in a
   header (bg-danger low-stock count on Parts, bg-success) must keep its hue, because there the
   colour IS the message. Both declarations need !important -- Tabler sets these with !important. */
.card-header .badge[class*="-lt"] {
  background-color: rgba(255,255,255,.16) !important;
  color: #fff !important;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.26);
}

/* fixed: FLX-742 -- FLX-729 re-based four outline variants for the coloured band and stopped
   there. btn-outline-danger/success/warning/info were left with their dark hue as both text and
   border, which on #215399 is a dark-on-dark near-miss -- the Delete profile control on Tax
   Settings was effectively unreadable. Same treatment as the others: keep the hue so the button
   still reads as destructive/positive, lift it to a tint that clears AA on the band (#FECACA on
   #215399 measures 5.25:1), and invert to a solid face on hover. */
.card-header:not(.card-header-light) .btn-outline-danger {
  --tblr-btn-color: #FECACA;
  --tblr-btn-border-color: rgba(254,202,202,.55);
  --tblr-btn-bg: transparent;
  --tblr-btn-hover-color: #7F1D1D;
  --tblr-btn-hover-bg: #FECACA;
  --tblr-btn-hover-border-color: #FECACA;
  --tblr-btn-active-color: #7F1D1D;
  --tblr-btn-active-bg: #FECACA;
  --tblr-btn-active-border-color: #FECACA;
  --tblr-btn-focus-shadow-rgb: 254,202,202;
}
.card-header:not(.card-header-light) .btn-outline-success {
  --tblr-btn-color: #BBF7D0;
  --tblr-btn-border-color: rgba(187,247,208,.55);
  --tblr-btn-bg: transparent;
  --tblr-btn-hover-color: #14532D;
  --tblr-btn-hover-bg: #BBF7D0;
  --tblr-btn-hover-border-color: #BBF7D0;
  --tblr-btn-active-color: #14532D;
  --tblr-btn-active-bg: #BBF7D0;
  --tblr-btn-active-border-color: #BBF7D0;
  --tblr-btn-focus-shadow-rgb: 187,247,208;
}
.card-header:not(.card-header-light) .btn-outline-warning {
  --tblr-btn-color: #FDE68A;
  --tblr-btn-border-color: rgba(253,230,138,.55);
  --tblr-btn-bg: transparent;
  --tblr-btn-hover-color: #713F12;
  --tblr-btn-hover-bg: #FDE68A;
  --tblr-btn-hover-border-color: #FDE68A;
  --tblr-btn-active-color: #713F12;
  --tblr-btn-active-bg: #FDE68A;
  --tblr-btn-active-border-color: #FDE68A;
  --tblr-btn-focus-shadow-rgb: 253,230,138;
}
.card-header:not(.card-header-light) .btn-outline-info {
  --tblr-btn-color: #BAE6FD;
  --tblr-btn-border-color: rgba(186,230,253,.55);
  --tblr-btn-bg: transparent;
  --tblr-btn-hover-color: #0C4A6E;
  --tblr-btn-hover-bg: #BAE6FD;
  --tblr-btn-hover-border-color: #BAE6FD;
  --tblr-btn-active-color: #0C4A6E;
  --tblr-btn-active-bg: #BAE6FD;
  --tblr-btn-active-border-color: #BAE6FD;
  --tblr-btn-focus-shadow-rgb: 186,230,253;
}

/* added: FLX-742 -- a card nested inside another card repeated the identical saturated band, so
   Tax Settings rendered as blue-band-inside-blue-band with no visual hierarchy between "the
   section" and "one profile in it" -- the main reason the page read as unfinished. This is
   Tabler's own .card-header-light on the inner header: a quiet tertiary surface that subordinates
   the child card to its parent. Every contents rule the band needs (white text, re-based buttons,
   translucent badges) has to be handed back, since those all assume a dark ground. */
.card-header.card-header-light {
  background-color: var(--tblr-bg-surface-tertiary);
  color: var(--tblr-body-color);
  border-bottom: var(--tblr-card-border-width) solid var(--tblr-border-color);
}
.card-header.card-header-light .card-title,
.card-header.card-header-light h1, .card-header.card-header-light h2,
.card-header.card-header-light h3, .card-header.card-header-light h4,
.card-header.card-header-light h5, .card-header.card-header-light h6,
.card-header.card-header-light .form-label,
.card-header.card-header-light label {
  color: var(--tblr-body-color);
}
.card-header.card-header-light .text-muted:not(.badge):not(.alert),
.card-header.card-header-light .text-secondary:not(.badge):not(.alert),
.card-header.card-header-light .card-subtitle:not(.badge):not(.alert),
.card-header.card-header-light small:not(.badge):not(.alert),
.card-header.card-header-light .small:not(.badge):not(.alert) {
  color: var(--tblr-secondary) !important; /* modified: FLX-1163 -- same badge / alert carve-out as the band */
}
.card-header.card-header-light a:not(.btn) {
  color: var(--bs-link-color);
}
/* fixed: FLX-804 -- was `--tblr-btn-*: initial` on nine properties plus
   `background-color: revert !important` and `color: revert !important`. `initial` on a CUSTOM
   property is the guaranteed-invalid value, so every var(--tblr-btn-*) downstream became invalid
   at computed-value time -- the identical failure mode as FLX-762's .btn.show. `revert !important`
   then handed the control to the USER AGENT stylesheet, which is not "stock light-surface
   rendering" but no rendering at all, and it differs per browser: on iPadOS the UA paints a
   system-blue face, which is exactly what was photographed on Tax Settings and reported as "the
   blue looks ugly".
   The dark-band button rules are now scoped :not(.card-header-light), so there is nothing left to
   undo -- a button in a light header simply falls through to its own variant colours, which since
   FLX-811 finally exist as --tblr-btn-* instead of the dead --bs-btn-*. Only the badge's flattening
   is still wanted here. */
.card-header.card-header-light .badge[class*="-lt"] {
  box-shadow: none;
  filter: none;
}

/* added: FLX-741 -- breadcrumbs moved below the title/subtitle (see _include/top_nav_breadcrumbs.php),
   so they no longer inherit Tabler's .page-pretitle type. Same uppercase wayfinding treatment,
   one step down in size per user direction, and muted so the H1 above stays the loudest thing in
   the header. */
.page-breadcrumbs .breadcrumb {
  font-size: .6875rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .04em;
  line-height: 1rem;
}
.page-breadcrumbs .breadcrumb-item,
.page-breadcrumbs .breadcrumb-item a {
  color: var(--tblr-secondary);
}
.page-breadcrumbs .breadcrumb-item a:hover {
  color: var(--bs-primary);
}

/* added: FLX-737 -- user directive 2026-08-07: every table in the app aligns its text left.
   Bootstrap's .text-end/.text-center utilities are !important, so a plain rule cannot reach the
   ~190 currency/count/action cells that carry them; this matches !important with a higher
   specificity (0,1,3 vs the utility's 0,1,0) and wins on the cascade's own terms. Scoped to
   cells inside .table so utility classes keep working everywhere else in the app, and written
   in CSS rather than stripped from ~27 files so JS-rendered cells and future markup inherit it
   too. NOTE: this deliberately overrides the accounting convention of right-aligned figures. */
.table > thead > tr > th,
.table > thead > tr > td,
.table > tbody > tr > th,
.table > tbody > tr > td,
.table > tfoot > tr > th,
.table > tfoot > tr > td {
  text-align: left !important;
}

/* fixed: FLX-738 -- .btn-link.link-secondary is this app's "quiet standalone button" idiom: the
   two card-footer Back links and ~45 modal Cancel buttons. .btn-link has no face and no border,
   so on a .card-footer / .modal-footer (both #F9FAFB) it rendered as bare grey text with no
   affordance that it was clickable at all. Given a real white bordered face here, per user
   screenshot 2026-08-07. Inline .btn-link.p-0 text links are untouched -- they are genuinely
   links inside body copy and a border would be wrong on every one of them. */
.btn-link.link-secondary {
  --tblr-btn-color: #495057;
  --tblr-btn-bg: #fff;
  --tblr-btn-border-color: var(--flx-stone);
  --tblr-btn-hover-color: var(--flx-slate);
  --tblr-btn-hover-bg: #F8F9FA;
  --tblr-btn-hover-border-color: #ADB5BD;
  --tblr-btn-active-color: var(--flx-slate);
  --tblr-btn-active-bg: #F1F3F5;
  --tblr-btn-active-border-color: #ADB5BD;
  text-decoration: none;
}
[data-bs-theme="dark"] .btn-link.link-secondary {
  --tblr-btn-color: #CBD5E1;
  --tblr-btn-bg: transparent;
  --tblr-btn-border-color: rgba(255,255,255,.18);
  --tblr-btn-hover-color: #fff;
  --tblr-btn-hover-bg: rgba(255,255,255,.08);
  --tblr-btn-hover-border-color: rgba(255,255,255,.32);
  --tblr-btn-active-color: #fff;
  --tblr-btn-active-bg: rgba(255,255,255,.12);
  --tblr-btn-active-border-color: rgba(255,255,255,.32);
}

/* added: FLX-780 -- roadside evaluation compliance tally.
   Replaces four loose objects per row (button, badge, button, badge) with one
   segmented control per counter. Previously this component had NO css at all:
   the markup claimed a 44x44 touch floor in a code comment while rendering
   btn-sm, which is well under it. That floor is real here -- an examiner taps
   these one-handed, on a tablet, in a moving car -- so it is enforced rather
   than asserted.

   The two segments are deliberately asymmetric. Incrementing is the in-car
   action and takes the whole button face; decrementing corrects a mistap and
   gets a narrower target, which also makes an accidental undo less likely. */
.flx-tally-group .btn {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  padding: 0.25rem 0.5rem;
}
.flx-tally-add {
  min-width: 64px;
  font-variant-numeric: tabular-nums;
}
/* Narrower than the add segment, still at the touch floor. */
.flx-tally-sub {
  min-width: 44px;
  padding-inline: 0.25rem;
}
.flx-tally-sub .fa {
  opacity: 0.75;
}
/* The count sits INSIDE its own button, which is the whole point -- a number
   floating beside a button never reads as that button's value. Tabular figures
   stop the control resizing as the tally crosses 9. */
.flx-tally-count {
  font-weight: 600;
  font-size: 0.9375rem;
  line-height: 1;
  min-width: 1ch;
}
@media (max-width: 575.98px) {
  .flx-tally { gap: 0.375rem !important; }
  .flx-tally-add { min-width: 56px; }
}

/* added: FLX-781 -- point the tally at the same pass/fail tokens the rating
   scale uses. Recoloured through Tabler's own --tblr-btn-* variables rather
   than overriding colour/border with !important, so hover, focus-visible and
   active states all follow without being restated. */
.flx-tally-success .btn {
  --tblr-btn-color: var(--flx-eval-pass);
  --tblr-btn-border-color: var(--flx-eval-pass);
  --tblr-btn-hover-bg: var(--flx-eval-pass);
  --tblr-btn-hover-border-color: var(--flx-eval-pass);
  --tblr-btn-active-bg: var(--flx-eval-pass);
  --tblr-btn-active-border-color: var(--flx-eval-pass);
}
.flx-tally-danger .btn {
  --tblr-btn-color: var(--flx-eval-fail);
  --tblr-btn-border-color: var(--flx-eval-fail);
  --tblr-btn-hover-bg: var(--flx-eval-fail);
  --tblr-btn-hover-border-color: var(--flx-eval-fail);
  --tblr-btn-active-bg: var(--flx-eval-fail);
  --tblr-btn-active-border-color: var(--flx-eval-fail);
}

/* added: FLX-782 -- compact tier for card-header toolbars on phones.
   Every toolbar in the app was built at desktop scale and simply inherited that
   scale onto a 390px viewport, so a status filter group, a search field and an
   action button each claimed a full row.

   The rule this block follows: shrink TYPE and PADDING, never the touch target.
   Controls stay at the 44px floor because that is what a thumb needs; what comes
   down is the visual weight around the label. Reducing height instead would look
   tidier in a screenshot and be worse to actually use. */
@media (max-width: 575.98px) {

  /* Type and horizontal padding come down; min-height holds the touch floor. */
  .card-header .btn,
  .card-header .form-control,
  .card-header .form-select {
    font-size: 0.8125rem;
    padding-inline: 0.5rem;
    min-height: 40px;
  }

  /* A segmented filter group is the worst offender: at desktop scale five
     segments span the viewport and wrap unpredictably. Let it scroll sideways
     as one unit instead of breaking apart -- the group stays readable as a
     group, which is the entire point of segmenting it. */
  .card-header .btn-group {
    max-width: 100%;
    overflow-x: auto;
    flex-wrap: nowrap;
    scrollbar-width: none;
  }
  .card-header .btn-group::-webkit-scrollbar { display: none; }
  .card-header .btn-group > .btn { flex: 0 0 auto; }

  /* Search fields were fixed-width and pushed their siblings off-screen. */
  .card-header .form-control[type="search"],
  .card-header input[type="search"],
  .card-header .flx-toolbar-search {
    min-width: 0;
    flex: 1 1 8rem;
  }

  /* Any inline form inside a header wraps rather than overflowing. This is the
     Students-page defect specifically: an inner d-flex with no flex-wrap. */
  .card-header form.d-flex { flex-wrap: wrap; }

  /* added: FLX-850 -- opt-in toolbar layout for a header whose search field is the
     control that keeps getting clipped. User, from an iPhone 16 Pro Max capture of
     modules/fleetfix/parts.php: the search box shares row one with the status filters
     and is cut off mid-placeholder, while Add Part sits alone on a row of its own.

     Reordering is done with flex `order` and not by moving the markup, because the
     desktop and tablet layout must stay exactly as it is -- the DOM is byte-identical
     at every other width, and there is no second copy of the button to keep in sync.

     The search is the only control given a 100% basis, so it takes a full row of its
     own and everything else closes up on the row above it. Scoped to [type="search"]
     because .flx-toolbar-search is also on the branch <select>, which stays with the
     filters. */
  .card-header.flx-toolbar-stack-search .flx-toolbar-search[type="search"] {
    order: 99;
    flex: 1 0 100%;
    width: 100%;
    max-width: 100%;
  }
  /* Ordered between the filters (0) and the search (99), so it lands in the space the
     search vacated. ms-auto is Bootstrap's margin-left:auto !important, so cancelling
     it needs the same weight -- without this the button keeps being pushed to the far
     right of its row instead of sitting beside Low stock. */
  .card-header.flx-toolbar-stack-search .card-actions {
    order: 50;
    margin-left: 0 !important;
  }
}

/* added: FLX-782 -- row-level action buttons (training packages, and anything
   else pairing a small edit with a small destructive action).
   Replaces per-button inline styles that set font-size .7rem with py-0, which
   rendered a ~18px target -- unusable on a phone and a violation of the
   all-CSS-in-styles.min.css rule. Square, aligned, and at the touch floor. */
.flx-row-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  line-height: 1;
  flex: 0 0 auto;
}
@media (max-width: 575.98px) {
  .flx-row-action { width: 40px; height: 40px; }
}

/* ============================================================================
   FLX-802 / FLX-803 (Epic FLX-801) -- card-header toolbar and icon-action sizing.
   User, from QA screenshots on Invoices / Online Inquiries / Customers: "Buttons are too spaced.
   It needs to match the height of all other elements in the header." And on Products & Services /
   Tax Settings: "Inconsistency in spacing around icons and it looks too small. Increase icon size
   and properly space it all sides."

   Cause: a card-header toolbar mixes three control families -- .btn-group-sm > .btn-sm (filter
   pills), .form-control-sm (the search box) and .btn-sm (actions) -- and each derives its height
   from its own padding + line-height + border, so no two agree. Nothing was ever wrong per-control;
   they were simply never measured against each other.

   Fixed once here rather than in markup: 53 card-header toolbars across the app share this shape,
   so one token drives all of them and any future toolbar inherits it for free. Height, not
   min-height, because a mixed inline-flex/inline-block row only truly aligns when every box is
   the same size -- min-height alone still lets the input grow. */
:root {
  /* modified: FLX-821 -- user, on the Manage Users header: "The buttons and search looks too tall,
     keep it compact and maybe a bit small?" One token still drives all 53 toolbars.
     The phone tier below deliberately does NOT follow this down -- it holds a 40px floor, because
     the rule there is shrink type and padding, never the touch target. A mouse does not need
     44px; a thumb does. | was: 2.25rem
     modified: FLX-844 -- this is now the value for controls OUTSIDE a card header only, chiefly
     the row-level .btn-icon in a table cell. .card-header re-declares the token at 1.5rem so a
     header control matches a .card-title line box; see the FLX-844 note further down. */
  --flx-ctl-h: 2rem;
}
.card-header .btn,
.card-header .form-control,
.card-header .form-select,
.card-header .input-group > .form-control,
.card-header .input-group > .form-select {
  height: var(--flx-ctl-h);
  min-height: var(--flx-ctl-h);
  padding-top: 0;
  padding-bottom: 0;
  /* added: FLX-821 -- the second half of "maybe a bit small". Height alone leaves full-size type
     in a shorter box, which reads cramped rather than compact. This is the same 0.8125rem the
     phone tier already uses, so the two scales now agree instead of diverging at 576px. */
  font-size: 0.8125rem;
  display: inline-flex;
  align-items: center;
  line-height: 1;
}
/* An input cannot centre its own text with align-items, so give it real symmetric padding back. */
.card-header .form-control,
.card-header .form-select {
  padding-top: 0;
  padding-bottom: 0;
}
/* .btn-group children must not double their borders once heights are pinned. */
.card-header:not(.card-header-light) .btn-group > .btn {
  height: var(--flx-ctl-h);
}
/* added: FLX-821 -- a count chip inside a segmented filter button. In a .btn-group the segments
   sit flush, so the chip needs to stay visually attached to its own label rather than drifting
   toward the divider. */
.card-header .btn-group > .btn > .badge {
  font-size: 0.6875rem;
  padding-inline: 0.35rem;
}

/* Icon-only actions: a square box on the same scale as every other control, with the glyph
   optically centred rather than sitting on a text baseline. FLX-729 already removed the phantom
   right margin these inherited from the icon-plus-label safety net; this gives them a real size. */
.btn-icon {
  width: var(--flx-ctl-h);
  min-width: var(--flx-ctl-h);
  height: var(--flx-ctl-h);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.btn-icon i.fa {
  font-size: 0.95rem;
  line-height: 1;
}
/* One gap between stacked actions, in a table cell or a card header alike -- the "inconsistent
   spacing around icons" in the report was .btn-list defaulting differently by context. */
.btn-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
td .btn-list,
.card-actions.btn-list {
  gap: 0.5rem;
}

/* ============================================================================
   FLX-834 (Epic FLX-1) -- the (i) that now carries a page's blurb.

   .flx-hint-icon's .85em is sized against a .form-label. Against .page-title the
   same em would render a ~17px glyph competing with the heading it annotates, so
   the page-header instance is pinned to the body scale instead. The offset is a
   margin-left declaration rather than a me-* utility because the icon-plus-label
   safety net further up only ever adds margin-RIGHT, on the assumption an icon
   leads its text -- this one follows it. */
.flx-page-title-hint {
  margin-left: 0.375rem;
  font-size: 0.85rem;
  vertical-align: middle;
}
/* A tooltip is rendered at body level, so the only way to reach it is the custom
   class the trigger declares. Bootstrap's stock 200px inner is sized for a short
   field hint; the page blurbs moved here run to ~180 characters (Website Imports,
   Security Audit), which at that width becomes a narrow eight-line column.

   modified: FLX-847 -- .flx-hint-tooltip joins the selector rather than getting a
   duplicate block. The card blurbs are the same kind of content at the same lengths
   (creator/data-fixes.php's run past 300 characters), so they want the same inner. */
.flx-page-title-tooltip .tooltip-inner,
.flx-hint-tooltip .tooltip-inner {
  max-width: 22rem;
  text-align: left;
}
/* added: FLX-847 -- one card blurb (Website Import) is two paragraphs that were two
   separate <p> tags before the move. Joined with a blank line they need the newline
   honoured or they run together into one wall of text; pre-line honours the break
   and still collapses the incidental indentation the PHP concatenation introduces.
   Scoped to the card-blurb class alone so FLX-834's page-title tooltips, none of
   which contain a newline, keep byte-identical rendering. */
.flx-hint-tooltip .tooltip-inner {
  white-space: pre-line;
}

/* added: FLX-847 -- the card-header equivalent of .flx-page-title-hint above.
   em rather than the page hint's absolute rem: a .card-title is 1rem in a section
   header but is also used at h5 scale inside nested cards (FleetFix Tax Settings
   renders one card title inside another), so the glyph has to track whatever
   heading it was dropped into instead of pinning to one size the way a page title,
   which only ever exists once and at one scale, safely can.

   margin-LEFT, because the icon-plus-label safety net further up only ever adds
   margin-right on the assumption an icon leads its text. This one follows it. */
.flx-card-title-hint {
  margin-left: 0.375rem;
  font-size: 0.85em;
  vertical-align: middle;
}
/* The header band paints .card-title #fff and remaps .text-muted to a lightened
   --flx-card-header-muted for contrast on the coloured ground (FLX-729). The hint
   icon carries .text-muted, so it already inherits the AA-checked value there and
   needs no colour of its own -- this rule deliberately sets none. */

/* ============================================================================
   FLX-835 (Epic FLX-1) -- one band height for every card header in the app.

   User, from a QA screenshot of modules/fleetfix/part.php: Part Details and
   Movement Ledger sit side by side in one row and their coloured header bands do
   not line up.

   Cause: FLX-713 gave .card-header a padding and nothing else, so a band's height
   has always been whatever its contents happened to measure. FLX-802/821 then
   pinned every control inside a header to --flx-ctl-h, which aligned the controls
   against each other but never the band against a header that has none: a header
   holding a search box is --flx-ctl-h tall (32px), a header holding only an
   h3.card-title is one card-title line-height tall (24px), and the two differ by
   exactly the 8px in the screenshot. 100 of the app's 259 card headers carry a
   control and ~155 carry only a title, so this shows up in every row mixing them.

   FLX-844 finishes it. FLX-835's floor converged the two cases on the TALLER of
   them -- 47px -- but the shorter one is what the app is mostly made of, so a
   header carrying a button still read 8px taller than the title-only header beside
   it (Priority jobs vs Shop load on modules/fleetfix/dashboard.php). Converging
   downward instead: a header control is held to 1.5rem, which is not a round number
   picked by eye but the exact line-height Tabler gives .card-title. A control now
   occupies the same line box a title does, so both cases resolve to one band height
   by construction rather than by a floor propping the short one up.

   The floor stays, derived from the same token, so it now lands exactly on that
   height rather than above it -- inert for the two common cases and still catching
   a header whose content is shorter than one title line. A floor rather than a cap
   because the four two-line headers (.card-title over .card-subtitle, on FleetFix
   Tax Settings) and any toolbar that wraps at a narrow width must still be free to
   grow.

   Scoped to .card-header, not :root: a row-level .btn-icon in a table cell keeps
   the 2rem FLX-821 set, because it is measured against table rows rather than a
   card title and FLX-803 sized those up on an explicit report that they were too
   small. Shrinking them here would walk that back silently.

   The 1em is the header's own padding-top plus padding-bottom -- it declares
   0.5em each -- resolved against the same font-size, so the two cannot drift.
   One rule covers all 259 blocks across 80 files; no markup changed. */
.card-header {
  /* added: FLX-844 -- one declaration drives the whole band: custom properties
     inherit, so the .card-header .btn / .form-control / .form-select rules above
     and any .btn-icon nested in the header all pick this up without repeating it. */
  --flx-ctl-h: 1.5rem;
  min-height: calc(var(--flx-ctl-h) + 1em + var(--tblr-card-border-width, 1px));
  /* The floor's companion. Tabler's own align-items:center handles the common
     single-line header, but two headers opt out of it deliberately -- the wrapping
     rating legend on training/session-view.php (.eval-card-header sets
     align-items:flex-start) and the stacked Pending Booking Requests header. Once
     a floor exists, "start" in a header shorter than the floor means the title
     pinned to the top with dead band underneath it. align-content centres the
     wrapped line(s) in the leftover cross space without overriding either
     header's deliberate align-items, and is inert when content already fills
     the box. */
  align-content: center;
}
@media (max-width: 575.98px) {
  /* modified: FLX-854 -- 30px, and the token is now declared on :root as well so it
     reaches the row-level .btn-icon outside a header, not only the band. User, after
     trying the 30px value locally: "This is looking great and not appealing too
     large. so change it across the entire app where buttons exists."
     | was: 40px on .card-header alone (FLX-844)

     This is a deliberate, user-approved departure from the rule stated at the top of
     this block -- "shrink TYPE and PADDING, never the touch target". What makes it
     safe is that only the PAINT comes down; the ::after below hands the target back.
     The choice was put to the user explicitly rather than assumed, because on its own
     this would reverse FLX-729. */
  :root,
  .card-header {
    --flx-ctl-h: 30px;
  }
}

/* FLX-854 -- 30px of paint, a thumb's worth of hit area.

   A transparent absolutely-positioned ::after, expanding each small control's pointer
   target beyond its painted box. It draws nothing: no background, no border, no
   content beyond the empty string that makes the box exist at all.

   THE INSET IS ASYMMETRIC ON PURPOSE. A flat -7px would give a tidy 44x44, but
   .btn-list spaces row actions 8px apart, so neighbouring targets would overlap by
   6px -- and since the later element wins an overlap, the right edge of Edit would
   fire Delete. On a destructive action that is worse than the small target it set out
   to fix. So: the full -7px vertically, where the 14px was actually lost (30 -> 44),
   and -4px horizontally, exactly half the gap, so adjacent targets meet but never
   cross. Net 38x44 -- clear of WCAG 2.5.8 AA (24x24), short of 2.5.5 AAA on width
   only. Stated precisely rather than rounded up to "44x44".

   :not(.dropdown-toggle) throughout: Bootstrap draws the caret with ::after, and
   without the exclusion this silently erases it on every dropdown button.

   The @media (pointer: coarse) block near the top of this file is deliberately NOT
   edited. A tablet is a coarse pointer and is NOT below 576px, so it must keep real
   44px controls; only the phone tier trades paint for a synthetic target. These
   selectors match that block's at equal specificity and sit later in the file, so
   they win here and nowhere else. */
@media (max-width: 575.98px) {
  .btn-sm:not(.dropdown-toggle),
  .btn-icon:not(.dropdown-toggle),
  .btn-list .btn:not(.dropdown-toggle),
  .btn-group-sm > .btn:not(.dropdown-toggle) {
    position: relative;
  }
  .btn-sm:not(.dropdown-toggle)::after,
  .btn-icon:not(.dropdown-toggle)::after,
  .btn-list .btn:not(.dropdown-toggle)::after,
  .btn-group-sm > .btn:not(.dropdown-toggle)::after {
    content: "";
    position: absolute;
    inset-block: -7px;
    inset-inline: -4px;
  }
  /* A .btn-group's segments are FLUSH -- zero gap, shared borders -- so the -4px above
     would have each one reaching 4px into both neighbours and make the boundary between
     Active / Inactive / All ambiguous to a thumb. They give up the horizontal expansion
     entirely: a segment is already several times wider than it is tall, so height was
     the only dimension it was ever short of. */
  .btn-group > .btn:not(.dropdown-toggle)::after {
    inset-inline: 0;
  }
  /* The paint comes down to the token. Same selectors the coarse-pointer block uses,
     so these override it on a phone and leave it intact on a tablet. */
  .btn-icon.btn-sm,
  .btn-list .btn-sm {
    min-width: var(--flx-ctl-h);
    min-height: var(--flx-ctl-h);
  }
  .btn-group-sm > .btn {
    min-height: var(--flx-ctl-h);
  }
}

/* FLX-843 -- part and asset photo galleries (_include/fleetfix_gallery_card.php).

   Tabler already ships the carousel itself (.carousel-indicators-thumb, .carousel-control-*,
   .carousel-caption and the rest are all in tabler.min.css). Everything below is only the part
   Tabler has no opinion about: the empty state, the per-slide manage controls, and holding the
   slide box to one size. Nothing here restyles a Tabler carousel class. */
.flx-gallery-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 2rem 1rem;
  border: 2px dashed var(--tblr-border-color);
  border-radius: var(--tblr-border-radius);
  background-color: var(--tblr-bg-surface-secondary);
  transition: border-color .15s ease, background-color .15s ease;
}
.flx-gallery-empty-icon {
  font-size: 2.5rem;
  line-height: 1;
  color: var(--tblr-secondary);
  opacity: .5;
  margin-bottom: .75rem;
}
.flx-gallery-empty-text {
  color: var(--tblr-secondary);
  max-width: 34ch;
  margin-bottom: 1rem;
}
.flx-gallery-empty-hint {
  font-size: .75rem;
  color: var(--tblr-secondary);
  margin-top: .75rem;
  margin-bottom: 0;
}
/* Drop feedback is a class rather than :hover so it responds to a dragged file, which is the
   only time the affordance is actually live. Applies to the empty state and the carousel alike. */
.flx-gallery-dragover {
  border-color: var(--tblr-primary);
  background-color: var(--tblr-primary-lt);
}
.flx-gallery-carousel {
  overflow: hidden;
  border-bottom-left-radius: inherit;
  border-bottom-right-radius: inherit;
}
/* One box for every slide regardless of what was uploaded. A shop's photos arrive in portrait
   from a phone and landscape from a tablet; without a fixed ratio the card resizes on each slide
   and the controls move under the cursor mid-click. object-fit keeps the subject centred rather
   than distorting it. */
/* modified: FLX-851 -- shorter ratio plus a hard height cap | was: aspect-ratio 4/3 with no
   max-height, which made the slide height a pure function of the column width and gave it no
   ceiling at all. In a col-lg-4 that is ~278px on a laptop but 410-450px on a wide monitor,
   where the column itself is 550-600px -- taller than the whole Part Details card beside it,
   which is what "dominating the page" was.

   BOTH declarations do work, at different viewports: the cap governs from roughly 356px of
   column width upward (the common case, and the one that was broken), and below that the ratio
   takes over so a narrow phone still gets a proportionate box instead of a 200px letterbox.

   The ratio is still here for the reason FLX-843 introduced it -- shop photos arrive portrait
   from a phone and landscape from a tablet, and without a fixed box the card resizes on every
   slide and the controls move under the cursor mid-click.

   ACCEPTED TRADE-OFF: object-fit:cover against a shorter box crops more off a portrait photo.
   Taken deliberately -- the gallery is a supporting card rather than the page's subject since
   FLX-846, the cover thumbnail only has to be recognisable, and the uncropped original is
   always intact in the Media Library. If detail turns out to matter more than compactness,
   raise max-height rather than reaching for object-fit:contain, which letterboxes a portrait
   photo into grey bars and looks worse at exactly this size. */
/* modified: FLX-1042 -- contain, not cover, and padded off the edges | was: object-fit: cover
   with no padding.
   cover fills the 16/9 box by cropping whatever will not fit, and on a truck or trailer the ends
   are the first thing lost -- which is routinely the point of the photograph: the damage, the
   decal, the coupling, the plate. contain letterboxes instead, so the whole unit is legible in the
   thumbnail and one unit can be told from another without opening it. The same argument is
   stronger still for parts, which share this class: a cropped photo of a filter or a fitting is
   close to useless for identifying it. */
.flx-gallery-img {
  width: 100%;
  aspect-ratio: 16 / 9;
  max-height: 200px;
  object-fit: contain;
  background-color: var(--tblr-bg-surface-secondary);
  padding: 1em;
}
.flx-gallery-actions {
  position: absolute;
  top: .5rem;
  right: .5rem;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: .25rem;
}
/* The controls sit on the photo, so they carry their own surface -- a bare icon button is
   unreadable against an arbitrary image, and contrast against user content cannot be assumed. */
.flx-gallery-actions .btn {
  --tblr-btn-bg: rgba(255, 255, 255, .92);
  --tblr-btn-border-color: rgba(255, 255, 255, .92);
  --tblr-btn-color: var(--tblr-body-color);
  --tblr-btn-hover-bg: #fff;
  --tblr-btn-hover-border-color: #fff;
  --tblr-btn-hover-color: var(--tblr-danger);
  width: 2.25rem;
  height: 2.25rem;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
}
.flx-gallery-actions .js-flx-gallery-cover:hover {
  --tblr-btn-hover-color: var(--tblr-primary);
}
.flx-gallery-cover-badge {
  box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
}
/* Touch devices get the full 44px target. Scoped to coarse pointers so a mouse keeps the
   compact control -- the same reasoning as the phone tier on .card-header. */
@media (pointer: coarse) {
  .flx-gallery-actions .btn {
    width: 2.75rem;
    height: 2.75rem;
  }
}
.flx-gallery-status {
  padding: .5rem 1rem;
  font-size: .875rem;
  color: var(--tblr-secondary);
  border-top: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
}
.flx-gallery-status-error {
  color: var(--tblr-danger);
}
/* An upload batch is sequential and can run for several seconds. The card dims and stops taking
   pointer events so a second click cannot start a competing batch; the buttons are disabled in
   JS as well, and this covers the areas that are not buttons. */
.flx-gallery-busy .carousel,
.flx-gallery-busy .flx-gallery-empty {
  opacity: .6;
  pointer-events: none;
}

/* FLX-845 -- gallery carousel controls, against a photo whose background is white.

   Both problems below are the same problem: Tabler's carousel is skinned for a photo that fills
   its frame with colour, and a parts shelf is photographed against white more often than not.

   THE FILTER TRAP, WHICH IS THE WHOLE REASON THIS IS NOT TWO LINES. Tabler's arrows are SVGs with
   stroke="#ffffff", and the light theme paints them by applying
   `filter: invert(1) grayscale(100)` to the entire .carousel-control-* element. `filter` applies
   to an element's BACKGROUND as well as its content, so the obvious fix -- background:#000 on the
   control -- inverts to WHITE in light mode and produces precisely the opposite of the intent,
   while looking correct in dark mode. That is the FLX-804 family of bug (a declaration that reads
   as "make it black" and resolves to something else entirely).

   So the filter is neutralised on the control and the black chip goes on the icon span instead.
   The arrow then stays its authored white in BOTH themes and sits on a black circle in both --
   which is what "black bg" has to mean here, since the control floats over user content whose
   colour is unknown rather than over a themed surface. */
.flx-gallery-carousel .carousel-control-prev,
.flx-gallery-carousel .carousel-control-next {
  filter: none;
  opacity: 1;
  width: 20%;
}
.flx-gallery-carousel .carousel-control-prev-icon,
.flx-gallery-carousel .carousel-control-next-icon {
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  background-color: rgba(0, 0, 0, .55);
  /* The icon is a background-image at 100% 100%, so the chip needs padding rather than a larger
     box -- inset shrinks the arrow inside the circle instead of scaling the circle to the arrow. */
  background-size: 1.25rem 1.25rem;
  background-position: 50%;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
  transition: background-color .15s ease;
}
.flx-gallery-carousel .carousel-control-prev:hover .carousel-control-prev-icon,
.flx-gallery-carousel .carousel-control-next:hover .carousel-control-next-icon,
.flx-gallery-carousel .carousel-control-prev:focus-visible .carousel-control-prev-icon,
.flx-gallery-carousel .carousel-control-next:focus-visible .carousel-control-next-icon {
  background-color: rgba(0, 0, 0, .8);
}
/* The control is a 20%-wide full-height hit area but only the chip is visible, so a keyboard focus
   ring on the control itself would draw a rectangle across a fifth of the photo. Moved onto the
   chip, where it outlines the thing the user can actually see. */
.flx-gallery-carousel .carousel-control-prev:focus-visible,
.flx-gallery-carousel .carousel-control-next:focus-visible {
  outline: none;
}
.flx-gallery-carousel .carousel-control-prev:focus-visible .carousel-control-prev-icon,
.flx-gallery-carousel .carousel-control-next:focus-visible .carousel-control-next-icon {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* Thumbnails. Tabler ships border:0 and a shadow at .04 alpha, which against a white product shot
   means the thumbnail has no edge at all -- it reads as part of the photo rather than as a
   control. A white border plus a real shadow gives it one regardless of what the image contains,
   which is the point: the frame cannot depend on the picture. */
/* added: FLX-851 -- one thumb size, overriding both Tabler's 2rem base and the 4rem it switches
   to above 992px. A media query adds no specificity, so this single rule at one class higher
   beats both. 4rem thumbs over a 200px slide would occupy nearly a third of its height; the
   strip is a control, not a second gallery. The indicators also sit closer to the bottom edge,
   since Tabler's 1rem inset was proportioned for a much taller slide. */
.flx-gallery-carousel .carousel-indicators {
  margin-bottom: .5rem;
}
.flx-gallery-carousel .carousel-indicators-thumb [data-bs-target] {
  width: 2.5rem;
  border: 2px solid #fff;
  border-radius: var(--tblr-border-radius);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .35);
  opacity: .65;
  transition: opacity .15s ease, box-shadow .15s ease, transform .15s ease;
}
.flx-gallery-carousel .carousel-indicators-thumb [data-bs-target]:hover {
  opacity: .9;
}
/* The active thumb is the one piece of state in the strip, so it gets a ring in the brand colour
   rather than only Tabler's opacity:1 -- at these sizes an opacity step alone is easy to miss. */
.flx-gallery-carousel .carousel-indicators-thumb .active {
  opacity: 1;
  box-shadow: 0 0 0 2px var(--tblr-primary), 0 1px 6px rgba(0, 0, 0, .4);
  transform: translateY(-1px);
}
@media (prefers-reduced-motion: reduce) {
  .flx-gallery-carousel .carousel-indicators-thumb [data-bs-target] {
    transition: none;
  }
  .flx-gallery-carousel .carousel-indicators-thumb .active {
    transform: none;
  }
}

.flx-woatt-kind-select {
  width: auto;
  min-width: 7rem;
}
.flx-woatt-grid {
  border-radius: var(--tblr-border-radius);
  transition: background-color .15s ease;
}
.flx-woatt-grid.flx-gallery-dragover {
  background-color: var(--tblr-primary-lt);
  outline: 2px dashed var(--tblr-primary);
  outline-offset: 4px;
}
.flx-woatt-tile {
  overflow: hidden;
}
.flx-woatt-media {
  display: block;
  aspect-ratio: 4 / 3;
  background-color: var(--tblr-bg-surface-secondary);
  border-bottom: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);
}
.flx-woatt-thumb {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.flx-woatt-media-video .flx-woatt-thumb {
  object-fit: contain;
  background-color: #000;
}
.flx-woatt-media-doc {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  padding: .75rem;
  text-align: center;
  color: var(--tblr-body-color);
  text-decoration: none;
}
.flx-woatt-media-doc:hover {
  color: var(--tblr-primary);
}
.flx-woatt-doc-icon {
  font-size: 2.5rem;
  line-height: 1;
  color: var(--tblr-danger);
}
.flx-woatt-doc-name {
  font-size: .75rem;
  line-height: 1.25;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}
.flx-woatt-caption {
  color: var(--tblr-secondary);
  overflow-wrap: anywhere;
}
.flx-woatt-tile .btn-icon {
  min-width: 2.25rem;
  min-height: 2.25rem;
}
.flx-gallery-busy .flx-woatt-grid {
  opacity: .6;
  pointer-events: none;
}

/* FLX-865 -- click-to-edit values on a definition-list card (asset Specifications).

   Interaction modelled on Zoho CRM's record detail, supplied by the user as the reference: the
   value sits in a box that only reveals its border and a pencil on hover, and clicking swaps in
   a control with a tick and an x beside it.

   THE RESTING STATE MUST NOT LOOK LIKE A FORM. A card showing twenty bordered inputs reads as an
   edit screen the user is expected to fill in and submit, which is the opposite of a read page
   that happens to be editable. So the border and the pencil are painted on hover and focus only,
   and the cell keeps the exact metrics of the plain text it replaces -- a negative inline margin
   cancels its own padding so nothing shifts when the affordance appears. */
.flx-inline-edit {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  justify-content: space-between;
  min-width: 6rem;
  max-width: 100%;
  padding: .125rem .375rem;
  margin: -.125rem -.375rem;
  border: 1px solid transparent;
  border-radius: var(--tblr-border-radius);
  cursor: pointer;
  transition: border-color .12s ease, background-color .12s ease;
}
.flx-inline-edit:hover,
.flx-inline-edit:focus-visible {
  border-color: var(--tblr-border-color);
  background-color: var(--tblr-bg-surface-secondary);
  outline: none;
}
.flx-inline-edit:focus-visible {
  border-color: var(--tblr-primary);
}
.flx-inline-value {
  min-width: 0;
  overflow-wrap: anywhere;
}
/* The pencil is generated content rather than an <i> in the markup: it is decoration on a
   control the user is already told about by role="button", and twenty more DOM nodes on a card
   that renders twenty rows is a real cost for something purely visual. Font Awesome 4.7 is the
   app's icon font (see _include/head.php), and f040 is fa-pencil. */
.flx-inline-edit::after {
  content: "\f040";
  font-family: FontAwesome;
  font-size: .75rem;
  line-height: 1;
  color: var(--tblr-secondary);
  opacity: 0;
  flex: 0 0 auto;
  transition: opacity .12s ease;
}
.flx-inline-edit:hover::after,
.flx-inline-edit:focus-visible::after {
  opacity: .7;
}
/* Editing: the box stops behaving like text and becomes a real control row. The pencil goes --
   it advertises an action already taken. */
.flx-inline-edit.flx-inline-editing {
  display: block;
  width: 100%;
  padding: 0;
  margin: 0;
  border-color: transparent;
  background-color: transparent;
  cursor: default;
}
.flx-inline-edit.flx-inline-editing::after {
  content: none;
}
.flx-inline-editor {
  display: flex;
  align-items: center;
  gap: .25rem;
  /* modified: FLX-1132 -- fills the cell, so the control takes every pixel the value had and the
     tick / x land at the right edge. Without it the editor shrank to the control's intrinsic
     width -- a textarea's default 20 columns -- with the buttons jammed against it mid-row. */
  width: 100%;
  min-width: 0;
}
/* A multi-line control: buttons sit at its top edge rather than floating at half height. */
.flx-inline-editor:has(> textarea) { align-items: flex-start; }
.flx-inline-editor .form-control,
.flx-inline-editor .form-select {
  flex: 1 1 auto;
  min-width: 0;
}
/* Tick and x are circular icon buttons sized to the control beside them. The tick carries the
   brand colour and the x stays neutral, so the committing action is the one the eye lands on. */
.flx-inline-editor .btn.flx-inline-ok,
.flx-inline-editor .btn.flx-inline-cancel {
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  padding: 0;
}
.flx-inline-editor .btn.flx-inline-ok {
  --tblr-btn-bg: var(--tblr-primary);
  --tblr-btn-border-color: var(--tblr-primary);
  --tblr-btn-color: #fff;
  --tblr-btn-hover-bg: var(--tblr-primary);
  --tblr-btn-hover-border-color: var(--tblr-primary);
  --tblr-btn-hover-color: #fff;
}
.flx-inline-editor .btn.flx-inline-cancel {
  --tblr-btn-bg: transparent;
  --tblr-btn-border-color: var(--tblr-border-color);
  --tblr-btn-color: var(--tblr-secondary);
  --tblr-btn-hover-bg: var(--tblr-bg-surface-secondary);
  --tblr-btn-hover-border-color: var(--tblr-border-color);
  --tblr-btn-hover-color: var(--tblr-danger);
}
/* Touch devices get the 44px target the rest of the app holds to. */
@media (pointer: coarse) {
  .flx-inline-editor .btn.flx-inline-ok,
  .flx-inline-editor .btn.flx-inline-cancel {
    width: 2.5rem;
    height: 2.5rem;
  }
}
.flx-inline-saving {
  opacity: .55;
  pointer-events: none;
}
/* A brief confirmation, shown ONLY when the server reports the row actually changed. Saving a
   value identical to the stored one closes silently -- flashing "saved" at a write that never
   happened trains people to distrust the signal. */
.flx-inline-saved {
  animation: flxInlineSaved 1.4s ease;
}
@keyframes flxInlineSaved {
  0%   { background-color: var(--tblr-success); }
  100% { background-color: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .flx-inline-saved {
    animation: none;
  }
  .flx-inline-edit,
  .flx-inline-edit::after {
    transition: none;
  }
}
/* The editor stays open on a failed save, so the message sits under the control it belongs to
   rather than in a toast that outlives the field it describes. */
.flx-inline-error {
  margin-top: .25rem;
  font-size: .75rem;
  color: var(--tblr-danger);
}

/* FLX-868 -- the inline tick and x were ellipses, not circles.

   Nothing was wrong with border-radius. The BOX was never square: both buttons carry .btn-icon,
   which sets `min-width: var(--flx-ctl-h)` alongside its own width and height. FLX-865 asked for
   1.75rem in both dimensions, min-width (2rem) beat width, and the result was a 2rem x 1.75rem
   box -- a 1.14:1 ellipse once a 50% radius was applied.

   Fixed by adopting the token instead of fighting it: every dimension reads --flx-ctl-h, so the
   box is square BY CONSTRUCTION rather than by two literals someone has to keep equal, and it
   sits on the same control scale as every other button in the app. min-width and min-height are
   restated here because .btn-icon and the phone-tier `.btn-icon.btn-sm` rule both set them; this
   selector is one class heavier than either, so it wins in both tiers.

   .btn-icon is deliberately KEPT rather than dropped to escape one declaration. It also carries
   the flex centring, the FLX-848 margin opt-out that stops the icon-plus-label safety net pushing
   the glyph off centre, and the FLX-854 synthetic touch target that gives these a real thumb
   target on a phone while painting at 32px. Dropping the class would have silently lost all three. */
.flx-inline-editor .btn.flx-inline-ok,
.flx-inline-editor .btn.flx-inline-cancel {
  flex: 0 0 auto;
  width: var(--flx-ctl-h);
  min-width: var(--flx-ctl-h);
  height: var(--flx-ctl-h);
  min-height: var(--flx-ctl-h);
  padding: 0;
  border-radius: 50%;
}
@media (pointer: coarse) {
  /* Supersedes the FLX-865 rule further up, which set a literal 2.5rem square here. Same
     reasoning as above -- one token, both dimensions, square by construction. */
  .flx-inline-editor .btn.flx-inline-ok,
  .flx-inline-editor .btn.flx-inline-cancel {
    width: 2.25rem;
    min-width: 2.25rem;
    height: 2.25rem;
    min-height: 2.25rem;
  }
}

/* FLX-870 -- one control size for the admin UI, declared once.

   492 markup sites already carried .form-control-sm and 687 did not, which is the whole reason
   "make these smaller" keeps coming back as a request: the size was being applied per element,
   by hand, on whichever page someone happened to be looking at. A page written next week starts
   at the large default again. Declaring it once means it is answered for every form in the app,
   including the ones that do not exist yet, and the 492 explicit classes become redundant rather
   than wrong -- they resolve to identical metrics, so nothing has to be stripped out.

   Values are Tabler's own .form-control-sm / .form-select-sm, copied exactly rather than
   approximated, so an element with the class and one without are pixel-identical.

   SCOPED TO .page, WHICH IS THE ADMIN SHELL, AND THAT SCOPE IS LOAD-BEARING. apply.php -- the
   public student application form -- also loads this stylesheet and has no .page wrapper. It is
   a customer-facing form filled in on a phone, and 0.75rem is 12px: under the 16px threshold at
   which iOS zooms the viewport on focus. Shrinking it would have been a real usability
   regression on the one form in this app that strangers use.

   .form-control-lg and .form-control-plaintext are excluded: both are deliberate opt-outs, and a
   rule that silently overrode them would make them unusable. */
.page .form-control:not(.form-control-lg):not(.form-control-plaintext),
.page .form-select:not(.form-select-lg) {
  font-size: .75rem;
  border-radius: var(--tblr-border-radius-sm);
  /* added: FLX-1052 -- a value is data, and should read like it.
     Bold, in the brand blue, against the tinted ground the property rows use. With the muted
     labels this completes one hierarchy: the label says what the field is, the value is the
     thing being read, and on a tablet held in a yard that distinction has to survive glare.
     Contrast checked rather than assumed -- #0353a4 is 7.56:1 on white and 6.89:1 on the
     tinted field, both comfortably past WCAG AA's 4.5:1 for body text.
     Declared here, in FLX-870's shared rule, because that rule exists so a control question is
     answered once for every form in the app including the ones not written yet. Note the .page
     scope: apply.php, the public application form, is deliberately outside it -- FLX-870's
     exclusion and still load-bearing. */
  font-weight: 700;
  color: var(--flx-blue);
}
/* modified: FLX-1043 -- min-height removed | was:
   min-height: calc(1.25rem + .625rem + calc(var(--tblr-border-width) * 2));

   It was redundant, not merely removable. That calc resolves to 20 + 10 + 2 = 32px, and the box
   already computes to exactly 32px on its own: line-height 1.25rem, plus .3125rem of padding top
   and bottom, plus a border either side. The floor was set to precisely the height it was
   flooring, so a single-line control is pixel-identical with it and without it.

   The FLX-870 invariant above therefore still holds: Tabler's own .form-control-sm declares the
   same min-height and the same padding, so the 492 markup sites that carry the class and the 687
   that do not still land on the same 32px.

   It also removes an inconsistency rather than creating one -- Tabler's .form-select-sm sets no
   min-height at all, so selects have always been sized this way and inputs were the odd ones out.

   What this frees: a control whose intrinsic height is not line-height driven can now size to its
   own content instead of being held at a floor it never needed. */
.page .form-control:not(.form-control-lg):not(.form-control-plaintext) {
  padding: .3125rem .5rem;
}
.page .form-select:not(.form-select-lg) {
  /* Selects keep their right padding -- the dropdown arrow is painted into it. */
  padding-top: .3125rem;
  padding-bottom: .3125rem;
  padding-left: .5rem;
}

/* FLX-989 -- FLX-870 above sets padding with the SHORTHAND, which resets padding-left and
   padding-right as a side effect of setting the vertical padding. At 0,4,0 it outranks Tabler's
   own `.input-icon .form-control:not(:first-child)` (0,3,0), so every input inside an .input-icon
   in the admin shell lost the space reserved for its addon and the icon started rendering on top
   of the field's own text -- reported on the topbar search box, but it hit system-settings.php,
   tenant/manage-users.php and the FleetFix inquiries/products toolbars identically.

   Restored here rather than by unpicking FLX-870: that rule is deliberately one declaration for
   the whole admin UI, and .input-icon is the one Tabler component whose contract it breaks.
   Specificity is 0,4,0 -- equal to FLX-870, winning on source order, which is why this block has
   to stay below it. .input-icon-addon is min-width:2.5rem, so 2.5rem is the addon's own width,
   not a guess. */
.page .input-icon .form-control:not(:first-child),
.page .input-icon .form-select:not(:first-child) {
  padding-left: 2.5rem;
}
.page .input-icon .form-control:not(:last-child),
.page .input-icon .form-select:not(:last-child) {
  padding-right: 2.5rem;
}

/* The topbar search reserves more than a plain addon on the right: the Ctrl K hint sits in that
   space. Below md the hint is hidden (d-none d-md-inline in nav_top.php), so the reserve is
   released back to the standard field padding. .flx-topbar-search and .input-icon are the same
   element, so this reads 0,4,0 and wins on source order over the block above. */
.page .flx-topbar-search.input-icon .form-control {
  padding-right: 3.25rem;
}
@media (max-width: 767.98px) {
  .page .flx-topbar-search.input-icon .form-control {
    padding-right: .5rem;
  }
}
.page textarea.form-control:not(.form-control-lg) {
  min-height: calc(1.25rem + .625rem + calc(var(--tblr-border-width) * 2));
}
.page .form-control-color:not(.form-control-lg) {
  height: calc(1.25rem + .625rem + calc(var(--tblr-border-width) * 2));
}

/* FLX-879 -- the record's own identifier, highlighted in a page title.

   COLOUR CHOSEN ON MEASURED CONTRAST, NOT BY EYE. --flx-blue (#0353A4) is the palette's
   Secondary/Link value and reads as 7.12:1 against the page background, 6.1:1 against its own
   10% chip -- both clear WCAG AA comfortably. --flx-amber was the other candidate from the
   seven-colour palette and was rejected outright: 2.02:1 on the same background, which fails AA
   badly for text, and it carries a warning meaning an identifier should not.

   The chip is a tint of the text colour rather than a separate background value, so the pair can
   never drift apart and the whole treatment follows --flx-blue if the palette ever moves.

   Sized in em, not rem: it tracks whatever heading it lands in, so the same span works on the
   h2 page title and would work unchanged in a smaller heading elsewhere. */
.flx-title-accent {
  display: inline-block;
  margin-left: .5em;
  padding: 0 .35em;
  border-radius: var(--tblr-border-radius-sm);
  background-color: rgba(3, 83, 164, .10);
  color: var(--flx-blue);
  font-variant-numeric: tabular-nums;
  /* A record code is read character by character and is often compared against a printed work
     order or a plate, so the glyphs are held apart slightly rather than set tight. */
  letter-spacing: .01em;
}
/* Dark mode: the same hue would sit too close to the surface, so the tint carries more weight and
   the text lifts off the brand value toward the lighter end of the ramp. */
:root[data-bs-theme="dark"] .flx-title-accent {
  background-color: rgba(96, 165, 250, .16);
  color: #93C5FD;
}

/* ============================================================================================
   FLX-887 -- Edit User modal (tenant/manage-users.php #editUserModal)

   Written against Tabler's OWN modal rules, not Bootstrap's. Tabler ships three overrides that
   the previous hand-rolled header was silently fighting:

     .modal-header               { padding: 0 3.5rem 0 1.5rem }   <- no VERTICAL padding at all
     .modal-header               { border-radius: 0 }             <- square, over a rounded card
     .modal-header > .btn-close  { position: absolute; inset-block-start: 0; ... 3.5rem square }

   The old markup compensated with a pb-3 utility and nothing for the top, so a 46px avatar sat
   flush against the modal's top edge and the square header spilled past .modal-content's rounded
   corners. That is stated once here instead of being fought with utilities per element.
   ============================================================================================ */

.modal-header.flx-eu-header {
  /* Restores the vertical rhythm Tabler zeroes out. The right value keeps the 3.5rem Tabler
     reserves for its absolutely-positioned close button -- the markup now lets that button be a
     direct child again, so it lands where Tabler expects with no help from us. */
  padding: 1rem 3.5rem 1rem 1.25rem;
  /* Follow the card's own corner radius so the header can never overhang it. */
  border-top-left-radius: var(--tblr-border-radius-lg, .5rem);
  border-top-right-radius: var(--tblr-border-radius-lg, .5rem);
  /* Tabler's 3.5rem floor assumes a single-line title; this header carries two lines plus an
     avatar and should be sized by its content instead. */
  min-height: 0;
}

.flx-eu-identity {
  display: flex;
  align-items: center;
  gap: .75rem;
  /* min-width:0 on a flex child is what actually lets text-overflow work further in. */
  min-width: 0;
  width: 100%;
}

.flx-eu-avatar {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  /* 44px is the minimum comfortable touch target and reads correctly beside two lines of text. */
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background-color: var(--flx-blue);
  color: #fff;
  font-size: .95rem;
  font-weight: 600;
  letter-spacing: .02em;
}

.flx-eu-identity-text {
  min-width: 0;
  flex: 1 1 auto;
}

.modal-title.flx-eu-name {
  margin: 0;
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flx-eu-email {
  margin: 0;
  color: var(--tblr-secondary);
  font-size: .8125rem;
  line-height: 1.4;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flx-eu-status-chip {
  flex: 0 0 auto;
  align-self: center;
}

/* --------------------------------------------------------------------------------------------
   Meta strip. Replaces two stacked full-width rows of mixed tenant/membership facts separated by
   vertical rules. Auto-fit rather than a fixed column count: the same grid holds four cells or
   five (invite expiry appears only while an invite is pending) without a breakpoint per case.
   -------------------------------------------------------------------------------------------- */

.flx-eu-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: .25rem 1.5rem;
  padding: .875rem 1.25rem;
  border-bottom: var(--tblr-border-width) solid var(--tblr-border-color);
  background-color: var(--tblr-bg-surface-secondary);
}

.flx-eu-meta-item {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: .0625rem;
}

.flx-eu-meta-label {
  color: var(--tblr-secondary);
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.flx-eu-meta-value {
  color: var(--tblr-body-color);
  font-size: .8125rem;
  font-weight: 500;
  /* A timestamp and an id are both compared column-to-column, so the digits are held to one width. */
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The relative hint ("2 days ago") is the line an admin scanning for recency actually reads, so it
   stays legible rather than dropping to a decorative size -- one step down from the value, not two. */
.flx-eu-meta-hint {
  color: var(--tblr-secondary);
  font-size: .75rem;
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flx-eu-meta-item--id .flx-eu-meta-value {
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .78125rem;
}

/* --------------------------------------------------------------------------------------------
   Section labels and the role checklist.
   -------------------------------------------------------------------------------------------- */

.flx-eu-section-label {
  margin-bottom: .5rem;
  color: var(--tblr-secondary);
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
}

/* modified: FLX-1157 -- the role picker is a scroll box of module GROUPS, each a two-column grid of
   checkbox cards (name, description beneath). 18rem tall so a tenant with three modules' roles
   reads without a second scrollbar inside the modal. | was: an 11rem single-column list */
.flx-eu-rolelist {
  max-height: 18rem;
  overflow-y: auto;
  padding: .25rem .75rem .5rem;
  border: var(--tblr-border-width) solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius);
  background-color: var(--tblr-bg-surface);
}
.flx-eu-rolegroup { padding-top: .5rem; }
.flx-eu-rolegroup + .flx-eu-rolegroup { border-top: 1px solid var(--tblr-border-color-translucent); margin-top: .5rem; }
.flx-eu-rolegroup-title {
  margin-bottom: .25rem;
  color: var(--tblr-secondary);
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
}
.flx-eu-rolegrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 1rem; row-gap: .125rem; }
@media (max-width: 575.98px) { .flx-eu-rolegrid { grid-template-columns: minmax(0, 1fr); } }

.flx-eu-role {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  min-height: 1.75rem;
  margin-bottom: 0;
  padding: .25rem 0;
}
.flx-eu-role .form-check-input { margin-top: .2rem; flex-shrink: 0; }
.flx-eu-role .form-check-label {
  display: flex;
  flex-direction: column;
  gap: .0625rem;
  min-width: 0;
  font-size: .8125rem;
  line-height: 1.3;
}
.flx-eu-role-name { font-weight: 500; color: var(--tblr-body-color); }
.flx-eu-role-desc { font-size: .75rem; color: var(--tblr-secondary); }
.flx-eu-role .form-check-input:disabled ~ .form-check-label { opacity: .55; }
.flx-eu-role--orphan .flx-eu-role-name::after { content: " -- module not enabled"; font-weight: 400; color: #B45309; }
.flx-eu-platform-badge { font-size: .625rem; padding: .1rem .35rem; vertical-align: middle; flex-shrink: 0; }
.flx-eu-rolegroup[hidden] { display: none !important; }

/* A role that belongs to a module carries that module's name, so "Driving School Admin" is
   self-evidently a module role rather than something the tenant is simply missing context for. */
.flx-eu-role-module {
  padding: 0 .3em;
  border-radius: var(--tblr-border-radius-sm);
  background-color: rgba(3, 83, 164, .10);
  color: var(--flx-blue);
  font-size: .6875em;
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
}

/* A role the membership still holds after its module was disabled. Kept visible and checkable on
   purpose -- hiding it would drop it from the submitted set and strip it on the next save -- but
   marked so it does not read as a normal, grantable option. */
.flx-eu-role--orphan .flx-eu-role-module {
  background-color: rgba(217, 119, 6, .12);
  color: #B45309;
}

:root[data-bs-theme="dark"] .flx-eu-role-module {
  background-color: rgba(96, 165, 250, .16);
  color: #93C5FD;
}

:root[data-bs-theme="dark"] .flx-eu-role--orphan .flx-eu-role-module {
  background-color: rgba(251, 191, 36, .16);
  color: #FCD34D;
}

/* The [hidden] attribute is how the modal's JS hides the scheduling section, the invite-expiry
   cell, a non-applicable role row and the status chip. A bare `display` declaration on any of
   those elements outranks the user-agent's [hidden]{display:none}, and .badge brings its own
   display:inline-block along too -- so each one needs to say so explicitly or it stays visible.
   This is the classic reason an element "ignores" hidden. */
.flx-eu-meta-item[hidden],
.flx-eu-role[hidden],
.flx-eu-status-chip[hidden],
.flx-eu-status-chip.badge[hidden] {
  display: none !important;
}


/* ============================================================================
   FLX-888 (Epic FLX-9) -- Integrations catalog (Integrations tab, tenant/settings.php).
   Section rail on the left, tile grid on the right.

   Colours come only from the official 7-colour palette tokens (--flx-navy/slate/steel/
   stone/paper/blue/amber) plus the three agreed semantic status values (success #03810B,
   warning #F59E0B, danger #D61731). Tabler's bg-*-lt utilities are deliberately NOT used
   here -- they draw from Tabler's full rainbow, which is not this product's palette.
   ========================================================================= */

/* ---- section rail ---- */
.flx-int-nav {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px;
  border: 1px solid var(--flx-stone);
  border-radius: 12px;
  background: var(--flx-paper);
}
.flx-int-nav-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  min-height: 44px;
  padding: 10px 14px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--flx-slate);
  font-size: 14px;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
  transition: background-color .15s var(--flx-ease), color .15s var(--flx-ease);
}
.flx-int-nav-item:hover {
  background: var(--flx-stone);
  color: var(--flx-navy);
}
.flx-int-nav-item.is-active {
  background: var(--flx-blue);
  color: #fff;
}
.flx-int-nav-item:focus-visible {
  outline: 2px solid var(--flx-blue);
  outline-offset: 2px;
}
.flx-int-count {
  flex: 0 0 auto;
  min-width: 22px;
  padding: 1px 7px;
  border-radius: 999px;
  background: rgba(8,18,32,.08);
  color: inherit;
  font-size: 12px;
  font-weight: 600;
  text-align: center;
}
.flx-int-nav-item.is-active .flx-int-count {
  background: rgba(255,255,255,.22);
}

/* ---- tiles ---- */
/* modified: FLX-1036 -- the integration tile is a fixed 150x150 square holding only the logo,
   the name and a corner status badge. Everything it used to render inline (meta, note, action
   buttons) moved into a per-integration details modal opened by clicking it.
   | was: height:100% on a .card, which let a connected QuickBooks tile grow to roughly four
          times the height of an unconnected one and stopped the grid reading as a catalog.
   The element is a <button> when it opens a modal, so the font/color/background resets below
   are load-bearing, not defensive. */
/* modified: FLX-1212 -- the column now starts at the top and the status badge is its first
   in-flow item, so badge, plate and name sit at the SAME y in every tile of the row and share
   one gap. Centring the stack put the badge 12px from the top on a two-line name and 19px on a
   one-line one -- a row of badges that jitter is worse than a row that is not perfectly centred.
   | was: justify-content:center, gap:10px, padding 16px 10px 12px with the badge taken out of
          flow by .flx-int-chip-corner and pinned at top:6px / right:6px.
   Geometry at 150px: badge 14-33, plate 41-97, name 105-121 (one line) or 105-137.5 (two),
   bottom padding 12 -- the two-line case is what sets the 8px gap. */
.flx-int-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 8px;
  width: 150px;
  height: 150px;
  max-width: 150px;
  max-height: 150px;
  padding: 14px 10px 12px;
  overflow: hidden;
  text-align: center;
  font: inherit;
  color: inherit;
  background: #fff;
  border: 1px solid var(--flx-stone);
  border-radius: 12px;
  transition: box-shadow .15s var(--flx-ease), transform .1s var(--flx-ease),
              border-color .15s var(--flx-ease);
}
.flx-int-tile.is-clickable { cursor: pointer; }

/* added: FLX-1039 -- the identity rail on an integration's own management page. It occupies the
   column the Integrations tab gives its vertical nav, and carries what that nav cannot: which
   integration this is, whether it is working, and against which account. */
.flx-int-manage-rail { position: sticky; top: 84px; }

.flx-int-manage-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 96px;
  height: 96px;
  margin: 0 auto 14px;
  background: var(--flx-paper);
  border-radius: 18px;
  color: var(--flx-blue);
  font-size: 44px;
}

[data-bs-theme="dark"] .flx-int-manage-mark { background: rgba(255,255,255,.06); }

/* The rail is sticky on a wide screen only. Stuck to the top of a phone viewport it would eat
   most of the screen the settings themselves need. */
@media (max-width: 991.98px) {
  .flx-int-manage-rail { position: static; }
}
.flx-int-tile.is-clickable:focus-visible {
  outline: 2px solid var(--flx-blue);
  outline-offset: 2px;
}
.flx-int-tile:hover {
  box-shadow: var(--flx-shadow-md);
  transform: translateY(-2px);
}
/* added: FLX-1216 -- a tile with a manage page is an <a> (FLX-1039), and Tabler underlines links
   on hover, which underlined the badge and the name inside it. The tile is a card, never running
   text; no state of it is underlined. */
.flx-int-tile,
.flx-int-tile:hover,
.flx-int-tile:focus,
.flx-int-tile:focus-visible { text-decoration: none; }
/* added: FLX-1036 -- the "View details" affordance. Hidden until hover or keyboard focus, and
   pointer-events:none so it can never swallow the click it is advertising. */
/* modified: FLX-1216 -- solid brand navy, per the product owner's review of FLX-1212 on prod.
   | was: rgba(3,83,164,.92), the brand blue at 92%, which let the plate and name bleed through
          and made the badge (see the chip rule below) unreadable. */
.flx-int-hover {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 8px;
  font-size: 12px;
  font-weight: 600;
  color: #fff;
  background: var(--flx-navy);
  opacity: 0;
  pointer-events: none;
  transition: opacity .15s var(--flx-ease);
}
.flx-int-hover i { font-size: 20px; }
.flx-int-tile.is-clickable:hover .flx-int-hover,
.flx-int-tile.is-clickable:focus-visible .flx-int-hover { opacity: 1; }
/* added: FLX-1216 -- the badge stays above the overlay (FLX-1036), but its translucent pastel
   fill was built for a white tile: over the navy it vanished and its text lost contrast. While
   the overlay shows, each status becomes a solid pill in its own colour with white text (navy on
   amber, which is too light to carry white). None of these depend on a theme token that changes
   in dark mode, so they read the same on both, and at (0,4,0) they outrank the
   [data-bs-theme="dark"] chip colours (0,2,0) regardless of order. */
.flx-int-tile.is-clickable:hover .flx-int-chip-corner,
.flx-int-tile.is-clickable:focus-visible .flx-int-chip-corner { color: #fff; }
.flx-int-tile.is-clickable:hover .flx-int-chip--connected,
.flx-int-tile.is-clickable:focus-visible .flx-int-chip--connected { background: #03810B; }
.flx-int-tile.is-clickable:hover .flx-int-chip--available,
.flx-int-tile.is-clickable:focus-visible .flx-int-chip--available { background: var(--flx-blue); }
.flx-int-tile.is-clickable:hover .flx-int-chip--soon,
.flx-int-tile.is-clickable:focus-visible .flx-int-chip--soon { background: var(--flx-steel); }
.flx-int-tile.is-clickable:hover .flx-int-chip--setup,
.flx-int-tile.is-clickable:focus-visible .flx-int-chip--setup { background: var(--flx-amber); color: var(--flx-navy); }
/* modified: FLX-1212 -- the status badge is the first in-flow item of the tile's column, centred
   by the tile's align-items and spaced by its gap, instead of an absolutely-positioned corner
   pin. On a 150px square a corner-pinned "Setup Required" read as a strip jammed against the top
   border with no room before the plate. position:relative is kept so the z-index still lifts it
   above the hover overlay: it is the one fact the tile exists to carry, and hiding it behind the
   affordance would trade the answer for the invitation. The class name stays -- one consumer,
   and renaming it would touch PHP for no behaviour.
   | was: position:absolute; top:6px; right:6px; max-width:calc(100% - 12px) (FLX-1036) */
/* fixed: FLX-1216 -- selector is now .flx-int-chip.flx-int-chip-corner. As a single class this
   rule sat BEFORE .flx-int-chip in the file at equal specificity, so .flx-int-chip's
   margin-top:10px won and the badge rendered 10px lower than FLX-1212 designed; the name's flex
   box then absorbed the shortfall and two-line names clipped to a sliver. The extra class makes
   the corner rule win on specificity instead of on order. */
.flx-int-chip.flx-int-chip-corner {
  position: relative;
  flex: 0 0 auto;
  margin-top: 0;
  z-index: 2;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
  .flx-int-tile { transition: none; }
  .flx-int-tile:hover { transform: none; }
  .flx-int-hover { transition: none; }
}
.flx-int-tile.is-connected {
  border-color: rgba(3,129,11,.45);
}
/* The logo plate. Square-ish and quiet on purpose: it is a recognition anchor, not a
   decoration, so it carries one mark and no colour of its own beyond the brand blue. */
/* modified: FLX-1036 -- a centred 56px badge inside the square rather than a full-width 16:9
   banner with a divider under it.
   | was: aspect-ratio 16/9 + border-bottom, which only made sense when the tile had a body
          below it to be divided from. */
.flx-int-plate {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  flex: 0 0 auto;
  background: var(--flx-paper);
  border-radius: 10px;
  color: var(--flx-blue);
  font-size: 26px;
}
.flx-int-tile.is-dormant .flx-int-plate {
  color: var(--flx-steel);
  opacity: .55;
}
/* FLX-943 -- Creator-Admin-configured logo. Contained, never cropped: these are third-party
   brand marks of wildly different aspect ratios (a square Stripe glyph, a very wide QuickBooks
   lockup) and object-fit: cover would slice the wordmark off the ones that matter most. The
   padding keeps a full-bleed PNG off the plate edges. */
.flx-int-logo {
  max-width: 72%;
  max-height: 68%;
  object-fit: contain;
  padding: 4px;
}
/* Icon fallback for a logo URL that fails to load -- hidden until the img's onerror flips the
   plate, so a working logo never renders both. */
.flx-int-plate-fallback { display: none; }
.flx-int-plate.is-fallback .flx-int-plate-fallback { display: inline-block; }

/* ---- creator/integrations.php ----
   Two sizes of the same idea: a live preview beside the logo field, and a row thumbnail in the
   catalog table. Both contain rather than crop, matching .flx-int-logo on the tenant-facing tile,
   so what the Creator Admin sees here is what the tenant gets. */
.flx-int-cfg-preview,
.flx-int-cfg-thumb {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  border: 1px solid var(--flx-stone);
  border-radius: 8px;
  background: var(--flx-paper);
  color: var(--flx-blue);
  overflow: hidden;
}
.flx-int-cfg-preview {
  width: 104px;
  height: 72px;
  font-size: 26px;
}
.flx-int-cfg-thumb {
  width: 48px;
  height: 34px;
  font-size: 15px;
}
.flx-int-cfg-preview img,
.flx-int-cfg-thumb img {
  max-width: 86%;
  max-height: 82%;
  object-fit: contain;
}
.flx-int-body {
  padding: 14px;
}
/* modified: FLX-1036 -- wraps to two lines inside the square instead of truncating on one. At
   150px wide, "Google Business Profile" on a single nowrap line is almost entirely ellipsis.
   | was: white-space:nowrap with text-overflow:ellipsis */
.flx-int-name {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  max-width: 100%;
  flex-shrink: 0;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--flx-navy);
  overflow: hidden;
}
/* added: FLX-1216 -- flex-shrink:0 above. The name is a flex item in a fixed-height column; by
   default it shrank to whatever height the tile had left, and overflow:hidden then cut the second
   line mid-glyph rather than the line-clamp deciding where the text ends. The clamp owns
   truncation; the tile's own overflow:hidden is the backstop if geometry ever changes again. */
.flx-int-cat {
  margin-top: 2px;
  font-size: 12px;
  color: var(--flx-steel);
}
.flx-int-chip {
  display: inline-block;
  margin-top: 10px;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .01em;
}
.flx-int-chip--connected { background: rgba(3,129,11,.12);  color: #03810B; }
.flx-int-chip--available { background: rgba(3,83,164,.10);  color: var(--flx-blue); }
.flx-int-chip--setup     { background: rgba(245,158,11,.16); color: #8A5A00; }
.flx-int-chip--soon      { background: rgba(55,65,81,.10);  color: var(--flx-steel); }

.flx-int-meta {
  margin: 10px 0 0;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--flx-paper);
  font-size: 12px;
}
.flx-int-meta-row {
  display: flex;
  gap: 8px;
  align-items: baseline;
  justify-content: space-between;
}
.flx-int-meta-row + .flx-int-meta-row { margin-top: 4px; }
.flx-int-meta dt {
  font-weight: 500;
  color: var(--flx-steel);
  white-space: nowrap;
}
.flx-int-meta dd {
  margin: 0;
  text-align: right;
  color: var(--flx-slate);
  word-break: break-word;
}
.flx-int-note {
  margin: 10px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--flx-steel);
}
.flx-int-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
.flx-int-actions:empty { display: none; }
.flx-int-sync-result {
  margin-top: 8px;
  font-size: 12px;
}

/* ---- empty state ---- */
.flx-int-empty {
  padding: 40px 24px;
  border: 1px dashed var(--flx-stone);
  border-radius: 12px;
  background: var(--flx-paper);
  color: var(--flx-steel);
  text-align: center;
}
.flx-int-empty i {
  display: block;
  margin-bottom: 10px;
  font-size: 28px;
  opacity: .55;
}

/* ---- SMTP2GO modal ----
   The form inside keeps its original card markup so its field names and handler contract
   stay byte-identical; the card chrome is neutralised here rather than by editing that
   markup, since the modal already supplies a title and a border. */
#smtp2goModal .card {
  border: 0;
  background: transparent;
  box-shadow: none;
}
#smtp2goModal .card-header { display: none; }
#smtp2goModal .card-body { padding: 0; }

/* ---- dark theme ---- */
[data-bs-theme="dark"] .flx-int-nav {
  border-color: var(--flx-steel);
  background: var(--flx-slate);
}
[data-bs-theme="dark"] .flx-int-nav-item { color: var(--flx-stone); }
[data-bs-theme="dark"] .flx-int-nav-item:hover {
  background: var(--flx-steel);
  color: #fff;
}
[data-bs-theme="dark"] .flx-int-count { background: rgba(255,255,255,.10); }
[data-bs-theme="dark"] .flx-int-tile { border-color: var(--flx-steel); }
[data-bs-theme="dark"] .flx-int-plate {
  background: rgba(255,255,255,.06);
}
/* added: FLX-1036 -- the tile supplies its own surface now that it is no longer a .card. */
[data-bs-theme="dark"] .flx-int-tile { background: var(--flx-navy); }
[data-bs-theme="dark"] .flx-int-name { color: #fff; }
[data-bs-theme="dark"] .flx-int-cat,
[data-bs-theme="dark"] .flx-int-note { color: var(--flx-stone); }
[data-bs-theme="dark"] .flx-int-meta { background: var(--flx-navy); }
[data-bs-theme="dark"] .flx-int-meta dt { color: var(--flx-stone); }
[data-bs-theme="dark"] .flx-int-meta dd { color: #fff; }
[data-bs-theme="dark"] .flx-int-empty {
  border-color: var(--flx-steel);
  background: var(--flx-slate);
  color: var(--flx-stone);
}
[data-bs-theme="dark"] .flx-int-chip--available { background: rgba(3,83,164,.24); color: #9DC4F0; }
[data-bs-theme="dark"] .flx-int-chip--connected { background: rgba(3,129,11,.24); color: #7CD98A; }
[data-bs-theme="dark"] .flx-int-chip--setup     { background: rgba(245,158,11,.22); color: var(--flx-amber); }
[data-bs-theme="dark"] .flx-int-chip--soon      { background: rgba(255,255,255,.10); color: var(--flx-stone); }

/* On narrow screens the rail reads better as a horizontal strip than as a stack of
   full-width bars pushed above the grid. */
@media (max-width: 991.98px) {
  .flx-int-nav { flex-direction: row; }
  .flx-int-nav-item { justify-content: center; }
}

/* FLX-945: inline uploader inside the shared media picker (assets/js/flx-media-picker.js).
   The picker had no upload path of its own, so an empty library was a dead end -- its empty
   state pointed at the Media Library, which Creator Admin has no nav route to. */
.flx-media-dropzone {
  border: 1px dashed var(--tblr-border-color, #dadfe5);
  border-radius: var(--tblr-border-radius, .375rem);
  padding: .875rem 1rem;
  text-align: center;
  cursor: pointer;
  transition: border-color .15s, background-color .15s;
}
.flx-media-dropzone:hover,
.flx-media-dropzone:focus-visible {
  border-color: var(--flx-blue);
  background-color: rgba(3, 83, 164, .04);
}
.flx-media-dropzone.is-dragover {
  border-color: var(--flx-blue);
  background-color: rgba(3, 83, 164, .08);
}
.flx-media-dropzone.is-busy { pointer-events: none; opacity: .6; }
.flx-media-dropzone-icon { display: block; font-size: 1.25rem; opacity: .55; margin-bottom: .25rem; }

[data-bs-theme="dark"] .flx-media-dropzone:hover,
[data-bs-theme="dark"] .flx-media-dropzone:focus-visible,
[data-bs-theme="dark"] .flx-media-dropzone.is-dragover { background-color: rgba(157, 196, 240, .08); }

/* FLX-952: table cells whose content must not set the column width. Reported against the Error
   Log (creator/error-log.php), where Message text painted on top of File / Source and Tenant.
   The mechanism is the FLX-679 global rule above -- every cell inside .table-responsive is
   white-space:nowrap, and the wrapper's overflow-x:auto was supposed to provide the horizontal
   scroll instead. It cannot: Bootstrap/Tabler pin .table to width:100%, so the table can never
   grow past its container, and a row whose nowrap content needs more room than that gets its
   columns squeezed below their content width and simply overflows, painting over its neighbour.
   Any column carrying long unbreakable tokens (R2 object keys in a message, query-string URLs
   in a source path) can trigger it. The two <td> fixes that look obvious both do nothing: a
   max-width on a cell is ignored by the automatic table-layout algorithm, and .text-break
   (overflow-wrap: break-word) does not shrink a cell's min-content contribution. So the cap goes
   on an inner block, which re-declares white-space (an inherited value always loses to one
   declared on the element) and uses overflow-wrap:anywhere, which does shrink min-content --
   the column then absorbs a squeeze by wrapping instead of overflowing.
   NOTE: the width:100% conflict above is app-wide across ~34 DataTable pages, not specific to
   this one. Any other table showing overlapping columns has the same cause. */
.flx-cell-wrap {
  display: block;
  white-space: normal;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.flx-cell-clip {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.flx-log-message   { max-width: 26rem; }
.flx-log-source    { max-width: 15rem; }
.flx-log-stack     { white-space: pre-wrap; font-size: 12px; color: var(--tblr-body-color); } /* modified: FLX-1162 -- Tabler's base `pre { color: var(--tblr-light) }` assumes a dark panel; this one sits on bg-body-secondary, so the trace printed near-white on light grey. Body colour, and 12px. | was: 11px, colour inherited from Tabler's pre */
.flx-log-meta      { font-size: 11px; }
.flx-activity-text { max-width: 11rem; }

/* FLX-979 (Epic FLX-975) -- address autocomplete suggestion panel.
   Tabler tokens only, so the panel follows the theme rather than pinning its own colours. */
.flx-address-anchor {
  position: relative;
}
.flx-address-suggestions {
  position: absolute;
  z-index: 1056;
  top: 100%;
  left: 0;
  right: 0;
  margin: 2px 0 0;
  padding: 0;
  list-style: none;
  max-height: 17.5rem;
  overflow-y: auto;
  background: var(--tblr-bg-surface);
  border: var(--tblr-border-width) solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius);
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.flx-address-suggestions[hidden] {
  display: none;
}
.flx-address-suggestion {
  padding: 0.625rem 0.75rem;
  min-height: 2.75rem;
  display: flex;
  align-items: center;
  cursor: pointer;
  color: var(--tblr-body-color);
  border-bottom: var(--tblr-border-width) solid var(--tblr-border-color);
}
.flx-address-suggestion:last-child {
  border-bottom: 0;
}
.flx-address-suggestion:hover,
.flx-address-suggestion.is-active {
  background: var(--tblr-bg-surface-secondary);
}
.flx-address-suggestion.is-active {
  box-shadow: inset 2px 0 0 var(--tblr-primary);
}

/* FLX-982: address lookup busy state + empty result row. */
.flx-address-spinner {
  position: absolute;
  top: 50%;
  right: 0.625rem;
  width: 1rem;
  height: 1rem;
  margin-top: -0.5rem;
  border: 2px solid var(--tblr-border-color);
  border-top-color: var(--tblr-primary);
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms linear;
}
.flx-address-is-busy .flx-address-spinner {
  opacity: 1;
  animation: flx-address-spin 600ms linear infinite;
}
.flx-address-is-busy [data-flx-address-input] {
  padding-right: 2rem;
}
@keyframes flx-address-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .flx-address-is-busy .flx-address-spinner { animation-duration: 2s; }
}
.flx-address-empty {
  padding: 0.625rem 0.75rem;
  min-height: 2.75rem;
  display: flex;
  align-items: center;
  color: var(--tblr-secondary);
  font-size: 0.875rem;
}

/* FLX-993: two-line suggestions, primary street over muted locality. */
.flx-address-suggestion {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.0625rem;
}
.flx-address-primary {
  font-weight: 500;
  color: var(--tblr-body-color);
  line-height: 1.25;
}
.flx-address-secondary {
  font-size: 0.8125rem;
  color: var(--tblr-secondary);
  line-height: 1.25;
}

/* FLX-1040 / FLX-1041 -- Add/Edit Asset modal: section rail, inline fields, stable height.
   Reported after real-device use: unusable on a tablet, the dialog resized on every tab change,
   and the active pill was invisible in daylight. */

/* The pane container, not each pane, carries the height. Panes have different natural heights, so
   without this the dialog grew and shrank on every click -- and being modal-dialog-centered it
   jumped vertically too, which is what made it feel unstable rather than merely uneven. Sized to
   the tallest section so the frame never moves and only its contents change. */
.flx-asset-panes {
  min-height: 26rem;
  padding: 1.25rem 1.5rem;
}

.flx-section-rail {
  flex-direction: column;
  padding: 1rem;
  gap: 0.125rem;
  position: sticky;
  top: 0;
}

.flx-section-rail .nav-link {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  /* removed: FLX-1054 -- min-height: 44px | was: a touch-target floor added by FLX-1041.
     Without it the item falls back to Tabler's own nav-link metrics -- 0.5rem padding around a
     1.25rem line box, so 36px. That still clears WCAG 2.5.8 Target Size (Minimum, AA) at 24px
     comfortably; what is given up is 2.5.5 (Enhanced, AAA) at 44px. FLX-1041 justified the floor
     with gloves in a yard, which was a comfort argument rather than a conformance one -- said
     plainly here so the next reader does not restore it believing AA depended on it.
     Saves ~48px of modal height across the six rail items. */
  color: var(--tblr-body-color);
  border-radius: 6px;
  white-space: nowrap;
}

.flx-section-rail .nav-link i { width: 1rem; text-align: center; }
.flx-section-rail .nav-link .badge { margin-left: auto; font-size: 0.625rem; padding: 0.125rem 0.375rem; }

.flx-section-rail .nav-link:hover { background: var(--flx-paper); }

/* The active state was Tabler's default pale tint, which simply does not read on a screen held
   outdoors. Solid brand blue on white clears WCAG AA at a wide margin, and the inset bar gives a
   second, non-colour cue for anyone reading it at a glance or in glare. */
.flx-section-rail .nav-link.active,
.flx-section-rail .nav-link.active:hover {
  background: var(--flx-blue);
  color: #fff;
  box-shadow: inset 3px 0 0 rgba(255, 255, 255, 0.65);
}

.flx-section-rail .nav-link.active i { color: #fff; }

/* Inline labels. The request was made twice: stacked labels cost roughly three times the vertical
   space per field. Collapses back to stacked below sm, where a side-by-side input is too narrow to
   read what it holds. */
.flx-fld { align-items: center; }
.flx-fld > .col-form-label {
  padding-top: 0;
  padding-bottom: 0;
  font-size: 0.8125rem;
  color: var(--tblr-secondary);
  line-height: 1.2;
}

@media (max-width: 575.98px) {
  .flx-fld > .col-form-label { padding-bottom: 0.25rem; }
}

/* Tablet and phone: the rail becomes a horizontal strip. Stacked, col-md-3 put SIX rows of
   navigation between the user and the first input -- the single worst thing about the first
   version on a real device. */
@media (max-width: 767.98px) {
  .flx-section-rail-col { border-bottom: 1px solid var(--tblr-border-color); }
  .flx-section-rail {
    flex-direction: row;
    flex-wrap: nowrap;
    overflow-x: auto;
    position: static;
    padding: 0.5rem;
    -webkit-overflow-scrolling: touch;
  }
  .flx-section-rail .nav-item { flex: 0 0 auto; }
  .flx-section-rail .nav-link { width: auto; }
  .flx-section-rail .nav-link.active { box-shadow: none; }
  /* Height is a desktop concern: on a phone the content is one column and taller anyway, and a
     fixed minimum would only add dead space above the keyboard. */
  .flx-asset-panes { min-height: 0; padding: 1rem; }
}

/* The cover photo in the header. Read-only -- photos are managed by the gallery on the detail
   page, and a second editing surface for the same picture is how two sources of truth begin. */
.flx-asset-thumb {
  display: inline-block;
  width: 56px;
  height: 56px;
  border-radius: 8px;
  background-color: var(--flx-paper);
  background-size: cover;
  background-position: center;
  flex: 0 0 auto;
}

/* ================================================================================================
   FLX-1044 -- PROPERTY ROWS. A reusable label-left / value-right editor.

       <dl class="flx-props">
         <div class="flx-prop">
           <label class="flx-prop-label required" for="x">Part number</label>
           <div class="flx-prop-value"><input id="x" class="form-control"></div>
         </div>
         ...
       </dl>

   Named with no module in it on purpose: this is meant to be adopted by customers, work orders,
   invoices and tenant settings unchanged. Anything fleetfix-specific belongs in the page, not here.

   WHY THE INPUTS ARE BORDERLESS UNTIL TOUCHED. Two of the requirements pull against each other --
   inputs should feel part of the row rather than cards sitting on it, and there should not be
   heavy borders everywhere -- but a field with no affordance at all is a field nobody knows is
   editable. Resolved the way every property editor resolves it: the control is flat at rest, shows
   a border on hover, and takes the full focus ring when focused. The row itself reacts to the
   pointer, so the whole line reads as live.
   ================================================================================================ */
.flx-props {
  /* Override per form where labels are longer -- style="--flx-prop-label-w: 40%" is not needed,
     a utility class or a scoped rule can set it. This is the knob, so no second class is required
     for a form with wordier labels. */
  --flx-prop-label-w: 34%;
  margin: 0;
}

.flx-prop {
  display: grid;
  grid-template-columns: var(--flx-prop-label-w) minmax(0, 1fr);
  gap: 0 1rem;
  align-items: center;
  padding: 0.25rem 0;
  border-bottom: 1px solid var(--tblr-border-color-translucent);
}

/* No trailing rule -- a divider under the final row draws a line to nothing. Also suppressed for a
   row hidden by d-none, so a create-only row that is switched off does not leave its divider
   behind as a stray line. */
.flx-prop:last-child,
.flx-prop.d-none { border-bottom: 0; }

/* modified: FLX-1046 -- a nowrap flex row | was: a plain block that wrapped.
   "Catalog product" and "Opening stock" broke across two lines once their hint icon was added.
   Flex plus nowrap keeps a label on one line whatever it contains, and gap replaces the hand-set
   spacing between the text, the asterisk and the icon. */
.flx-prop-label {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  min-width: 0;
  margin: 0;
  padding-right: 0.25rem;
  font-size: 0.8125rem;
  line-height: 1.3;
  color: var(--tblr-secondary);
  white-space: nowrap;
}

/* modified: FLX-1047 -- content is declared HERE | was: inherited from Tabler, which never
   applied.
   Tabler's rule is SCOPED and this is worth knowing before reusing .required anywhere:
       .col-form-label.required:after, .form-label.required:after { content: "*" }
       .form-check-label.required:after                           { content: "*" }
   A bare .required gets nothing. .flx-prop-label carries none of those three, so the asterisk was
   never rendered at all -- FLX-1046 read a grep hit on the tail of ".col-form-label.required:after"
   as an unscoped selector and fixed the ordering instead, which was a real effect but not the
   reason it was invisible. Declaring content here removes the dependency on Tabler's selector
   matching a class this pattern does not use.
   The ordering below is still wanted: :after is by definition last, so on a label ending in a hint
   icon it would otherwise read "Part number (i) *". The anonymous text node is order 0. */
.flx-prop-label.required::after {
  content: "*";
  order: 1;
  margin-left: 0;
  color: var(--tblr-danger);
}
.flx-prop-label .flx-hint-icon { order: 2; }

.flx-prop-value { min-width: 0; }

/* modified: FLX-1047 -- tinted fill, no outline | was: ordinary bordered controls (FLX-1046),
   which was itself a revert of transparent-until-hover (FLX-1044).

   Both earlier attempts drew a real complaint, and the two are not in conflict:
     FLX-1044  transparent at rest  ->  "no indication that there is a text input"
     FLX-1046  full bordered box    ->  "it looks like a form. I like the inline style"
   One had no affordance, the other had a whole box. The fill is the resolution: a tinted ground
   is what says "you can type here", and dropping the outline is what keeps the row reading as a
   property line rather than a form field. Focus still takes the full ring, so the active control
   is never ambiguous, and hover lifts the fill so the target is obvious before clicking. */
.flx-prop-value .form-control,
.flx-prop-value .form-select {
  /* modified: FLX-1052 -- --flx-blue at 6% alpha | was: --tblr-bg-surface-secondary.
     Ties the editable ground to the palette rather than a neutral grey. This is a fixed
     alpha, not a theme token, so unlike the value it replaces it does not adapt between
     light and dark -- at 6% over a dark surface it still tints, just more subtly. */
  background-color: #0353a40f;
  border-color: transparent;
  box-shadow: none;
}

.flx-prop-value .form-control:hover,
.flx-prop-value .form-select:hover { background-color: var(--flx-paper); }

.flx-prop-value .form-control:focus,
.flx-prop-value .form-select:focus {
  background-color: var(--tblr-bg-forms);
  border-color: var(--tblr-primary);
  box-shadow: var(--tblr-shadow-input);
}

/* A textarea is the one control that should not be vertically centred against its label. */
.flx-prop-textarea { align-items: start; }
.flx-prop-textarea .flx-prop-label { padding-top: 0.375rem; }

/* An optional heading inside a list. Spans both columns and carries no divider of its own -- the
   row beneath it already draws one. */
.flx-prop-head {
  grid-column: 1 / -1;
  margin: 0.75rem 0 0.25rem;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--tblr-secondary);
}
.flx-props > .flx-prop-head:first-child { margin-top: 0; }

/* Phone: stack. A label column beside a value column leaves the input too narrow to read what it
   holds, which is the same reason .flx-fld collapses at this breakpoint. */
@media (max-width: 575.98px) {
  .flx-prop {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.125rem;
    padding: 0.5rem 0;
  }
  .flx-prop-textarea .flx-prop-label { padding-top: 0; }
  /* A label may wrap on a phone: it has the full row width to itself once the columns stack, and
     nowrap there would push a long label off the screen instead. */
  .flx-prop-label { white-space: normal; }
}

/* ================================================================================================
   FLX-1053 -- PROPERTY ROWS FOR EXISTING MODAL FORMS, WITHOUT TOUCHING THEIR MARKUP.

   Add .flx-prop-form to a .modal-body. Every col-* inside it that contains a .form-label becomes
   a property row: label left, value right, hairline between.

   WHY CSS AND NOT MARKUP. There are ~40 files, ~85 modals and ~500 form-labels in this app.
   Rewriting 500 field blocks by hand is the single most reliable way to lose one silently -- and
   a lost field is a column that stops being written, which surfaces weeks later as missing data
   rather than as a broken page. Measured before committing to this: 272 of 339 field blocks are
   the identical plain shape (one col, one .form-label, one control), 14 more add a .form-text,
   and the rest are small tails of input-group / form-check / datalist. Regular enough to reshape
   from the stylesheet, so no field markup is touched and nothing can go missing.

   The trade-off, stated: this leans on :has(), which is Chrome 105+, Safari 15.4+, Firefox 121+.
   An older browser gets the ordinary stacked form it has today -- the layout degrades, nothing
   breaks, and no data is affected either way.

   NOT APPLIED TO apply.php, and that is deliberate rather than an omission. It is the public
   student application form, and FLX-870 already established it must not inherit admin form
   styling: it has no .page wrapper because 0.75rem type is under the 16px threshold at which iOS
   zooms on focus, and it is the one form here that strangers fill in on a phone. Opt-in means it
   is excluded by default rather than by a rule someone has to remember.
   ================================================================================================ */
/* modified: FLX-1055 -- 40% label column, top-aligned, more vertical air | was: 34%, centred,
   0.25rem padding. A 34% label column is about 125px in a modal-lg half, and three labels in the
   customer modal alone exceed that. */
/* modified: FLX-1126 -- 0.375rem cell padding | was: 0.5rem. The cell's own padding plus the
   hairline IS the row separation; the Bootstrap g-* gutter that was stacking on top of it is
   cancelled below, so this no longer has to be generous to read as a row. */
.flx-prop-form .row > [class*="col-"]:has(> .form-label) {
  display: grid;
  grid-template-columns: var(--flx-prop-label-w, 40%) minmax(0, 1fr);
  align-items: start;
  column-gap: 1rem;
  row-gap: 0.125rem;
  padding: 0.375rem 0;
  border-bottom: 1px solid var(--tblr-border-color-translucent);
}
/* added: FLX-1126 -- the Bootstrap vertical gutter (g-3 puts margin-top: 1rem on every col and a
   matching negative margin on the row) was never cancelled, so every converted modal carried
   ~2rem between controls: gutter + padding + padding. The hairline rows carry their own spacing;
   the gutter is dead air here at every breakpoint. */
.flx-prop-form .row:has(> [class*="col-"] > .form-label) { margin-top: 0; }
.flx-prop-form .row:has(> [class*="col-"] > .form-label) > * { margin-top: 0; }
.flx-prop-form--halves .row > [class*="col-"]:has(> .form-label) { --flx-prop-label-w: 9rem; }

/* The label takes column one. Flex so an inline hint icon and the required asterisk sit on the
   same line as the text rather than wrapping under it -- the FLX-1046 lesson. */
.flx-prop-form .row > [class*="col-"] > .form-label {
  grid-column: 1;
  grid-row: 1;
  display: flex;
  align-items: center;
  gap: 0.25rem;
  min-width: 0;
  margin: 0;
  padding-right: 0.25rem;
  font-size: 0.8125rem;
  line-height: 1.3;
  color: var(--tblr-secondary);
  /* modified: FLX-1055 -- labels WRAP | was: white-space: nowrap.
     nowrap came from the Add Part pattern, where it was correct: that modal was widened to
     modal-xl under FLX-1046 precisely so its labels fit one line, and its labels are known ones.
     Carrying it into a generic converter over 71 modals with arbitrary labels guaranteed overflow
     -- a label wider than its column has nowhere to go and lands on top of the value beside it.
     A wrapped label is untidy; an overlapping one is broken. Padding aligns a wrapped label with
     the top of its control rather than letting it float against the middle. */
  padding-top: 0.375rem;
}

/* Everything that is not the label goes in column two and stacks there. That is what keeps a
   .form-text hint, a datalist or a second control under its own field instead of being
   auto-placed into the label column of the next row. */
.flx-prop-form .row > [class*="col-"]:has(> .form-label) > *:not(.form-label) {
  grid-column: 2;
}

/* Tabler scopes its required asterisk to .col-form-label / .form-label / .form-check-label, so
   .form-label already gets one here -- unlike .flx-prop-label, which had to declare its own
   (FLX-1047). Only the position needs correcting: :after is last, so on a label ending in a hint
   icon it would otherwise render after the icon. */
.flx-prop-form .row > [class*="col-"] > .form-label.required::after {
  order: 1;
  margin-left: 0;
}
.flx-prop-form .row > [class*="col-"] > .form-label .flx-hint-icon { order: 2; }

/* Two per line, whatever widths the markup happens to carry. Existing modals mix col-md-3,
   col-md-4 and col-md-6 freely, which as property rows would render three or four cramped rows
   across. col-12 keeps its full width -- that is what textareas and notes already use. */
/* modified: FLX-1055 -- the row becomes a real two-column grid | was: forcing each col to
   flex 0 0 50% and relying on Bootstrap's gutter for separation.
   That gutter is 8px per side at g-3, which is not enough space between one field's VALUE and the
   next field's LABEL -- they read as one run-on line. An explicit column gap is the only way to
   guarantee the separation regardless of which gutter class a given modal happens to use. */
@media (min-width: 768px) {
  /* modified: FLX-1126 -- 1.5rem between the two halves | was: 2.5rem. 24px keeps a value and the
     next label apart (the FLX-1055 reason for an explicit gap); 40px was a third column of air. */
  .flx-prop-form .row:has(> [class*="col-"] > .form-label) {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 1.5rem;
    margin-left: 0;
    margin-right: 0;
  }
  .flx-prop-form .row:has(> [class*="col-"] > .form-label) > * {
    flex: none;
    width: auto;
    max-width: none;
  }
  /* Full-width things span both tracks: col-12 is what textareas and notes already use, and
     anything that is not a column at all (an <hr>, an alert) must not be squeezed into one. */
  .flx-prop-form .row:has(> [class*="col-"] > .form-label) > .col-12,
  .flx-prop-form .row:has(> [class*="col-"] > .form-label) > *:not([class*="col-"]) {
    grid-column: 1 / -1;
  }
}

/* Phone: stack label over value. A label column beside a value column leaves the input too narrow
   to read what it holds -- same reason .flx-prop and .flx-fld both collapse here. */
@media (max-width: 767.98px) {
  .flx-prop-form .row > [class*="col-"]:has(> .form-label) {
    grid-template-columns: minmax(0, 1fr);
    padding: 0.5rem 0;
  }
  .flx-prop-form .row > [class*="col-"]:has(> .form-label) > *:not(.form-label) { grid-column: 1; }
  .flx-prop-form .row > [class*="col-"] > .form-label { padding-top: 0; }
}

/* added: FLX-1134 -- a column whose control is a SWITCH or CHECKBOX (label.form-check, no
   .form-label) is also a property row: the same vertical padding and hairline, so Billable /
   Active / Default rows stop sitting crammed against the field above them. Every modal with a
   switch had this gap; the FLX-1053 selector only knew columns led by a .form-label. */
.flx-prop-form .row > [class*="col-"]:has(> .form-check) {
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--tblr-border-color-translucent);
}
.flx-prop-form .row > [class*="col-"] > .form-check { margin-bottom: 0; }

/* Fields inside these rows get the same treatment as .flx-prop-value: brand-tinted ground, no
   outline, full ring on focus. Declared alongside rather than by adding a class to 500 controls. */
.flx-prop-form .row > [class*="col-"]:has(> .form-label) .form-control,
.flx-prop-form .row > [class*="col-"]:has(> .form-label) .form-select {
  background-color: #0353a40f;
  border-color: transparent;
  box-shadow: none;
}
.flx-prop-form .row > [class*="col-"]:has(> .form-label) .form-control:hover,
.flx-prop-form .row > [class*="col-"]:has(> .form-label) .form-select:hover {
  background-color: var(--flx-paper);
}
.flx-prop-form .row > [class*="col-"]:has(> .form-label) .form-control:focus,
.flx-prop-form .row > [class*="col-"]:has(> .form-label) .form-select:focus {
  background-color: var(--tblr-bg-forms);
  border-color: var(--tblr-primary);
  box-shadow: var(--tblr-shadow-input);
}

/* added: FLX-1053 -- .flx-fld folded into the same visual language.
   FLX-1041 introduced it for the asset modal and FLX-1044 flagged it as a second answer to one
   question, to be reconciled once the pattern proved out. It has: the asset modal keeps its
   section rail, and only the field treatment converges. */
.flx-fld > .col-form-label {
  font-size: 0.8125rem;
  color: var(--tblr-secondary);
}
.flx-fld { border-bottom: 1px solid var(--tblr-border-color-translucent); padding-bottom: 0.25rem; }

/* ================================================================================================
   FLX-1120 -- SELECT2 CONTROLS ARE ORDINARY CONTROLS.

   select2-bootstrap-5-theme ships Bootstrap's stock metrics: 1rem type, 1.5 line-height, white
   ground, #ced4da border, a 38px box. Every other control in the admin shell is FLX-870's
   compact one -- .75rem, bold, brand blue, 32px -- so a Select2 beside a plain select read as a
   different, larger component from a different app (the Open Work Order modal after FLX-1116
   was the first place four of them sat in one form). The theme cannot be told a size, so the
   same metrics are restated here against its own selectors, at the same specificity the theme
   uses plus the .page scope, which is why they win.

   SCOPED TO .page for the same reason FLX-870 is: apply.php has no .page wrapper and .75rem is
   under the 16px iOS zoom threshold. It carries no Select2 today; if it ever does, it keeps the
   theme's size on purpose.
   ================================================================================================ */
.page .select2-container--bootstrap-5 .select2-selection {
  min-height: 32px;
  padding: .3125rem 2rem .3125rem .5rem;
  font-size: .75rem;
  font-weight: 700;
  line-height: 1.25rem;
  color: var(--flx-blue);
  border-color: #bdc5d0;
  border-radius: var(--tblr-border-radius-sm);
}
.page .select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered {
  font-weight: inherit;
  line-height: 1.25rem;
  color: inherit;
}
.page .select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__placeholder,
.page .select2-container--bootstrap-5 .select2-search--inline .select2-search__field::placeholder {
  font-weight: 400;
  color: #adb8c4;
}
/* Multi: chips sit inline at control size and wrap, rather than one 1rem chip per line. */
.page .select2-container--bootstrap-5 .select2-selection--multiple {
  padding-top: .1875rem;
  padding-bottom: 0;
  padding-right: .5rem;
}
.page .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered {
  gap: 0 .25rem;
  margin-bottom: 0;
}
.page .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
  padding: .0625rem .375rem;
  margin: 0 0 .1875rem 0;
  font-size: .75rem;
  font-weight: 600;
  line-height: 1.25rem;
  color: var(--flx-blue);
  background-color: var(--tblr-bg-forms);
  border-color: #bdc5d0;
  border-radius: var(--tblr-border-radius-sm);
}
.page .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
  width: .625rem;
  height: .625rem;
  padding: .125em;
}
.page .select2-container--bootstrap-5 .select2-search--inline .select2-search__field {
  height: 1.25rem;
  margin: 0 0 .1875rem 0;
  font-size: .75rem;
  font-weight: 400;
}
/* The list matches the control it opened from. */
.page .select2-container--bootstrap-5 .select2-dropdown {
  font-size: .75rem;
  border-color: var(--flx-navy);
  border-radius: var(--tblr-border-radius-sm);
}
.page .select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field {
  padding: .25rem .5rem;
  font-size: .75rem;
  border-radius: var(--tblr-border-radius-sm);
}
.page .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option {
  padding: .25rem .5rem;
  font-size: .75rem;
  line-height: 1.25rem;
}
/* Focus ring in the brand navy, as [data-bs-theme="light"] .form-control:focus already is. */
.page .select2-container--bootstrap-5.select2-container--focus .select2-selection,
.page .select2-container--bootstrap-5.select2-container--open .select2-selection {
  border-color: var(--flx-navy);
  box-shadow: var(--tblr-shadow-input);
}
/* Inside a property row (FLX-1053) the control takes the row's tinted ground, like .form-select. */
.flx-prop-form .row > [class*="col-"]:has(> .form-label) .select2-container--bootstrap-5 .select2-selection {
  background-color: #0353a40f;
  border-color: transparent;
  box-shadow: none;
}
.flx-prop-form .row > [class*="col-"]:has(> .form-label) .select2-container--bootstrap-5 .select2-selection:hover {
  background-color: var(--flx-paper);
}
.flx-prop-form .row > [class*="col-"]:has(> .form-label) .select2-container--bootstrap-5.select2-container--focus .select2-selection,
.flx-prop-form .row > [class*="col-"]:has(> .form-label) .select2-container--bootstrap-5.select2-container--open .select2-selection {
  background-color: var(--tblr-bg-forms);
  border-color: var(--tblr-primary);
  box-shadow: var(--tblr-shadow-input);
}
.flx-fld .form-control,
.flx-fld .form-select {
  background-color: #0353a40f;
  border-color: transparent;
  box-shadow: none;
}
.flx-fld .form-control:focus,
.flx-fld .form-select:focus {
  background-color: var(--tblr-bg-forms);
  border-color: var(--tblr-primary);
  box-shadow: var(--tblr-shadow-input);
}

html { scrollbar-gutter: stable; }
@supports not (scrollbar-gutter: stable) {
  html { overflow-y: scroll; }
}
/* fixed: FLX-1160 -- a 17px blank strip either side of every page at >= 992px (the user saw it on
   the login split layout). Tabler's own compiled CSS sets `:root { margin-left: calc(100vw - 100%) }`
   at that breakpoint: a layout-shift trick that nudges the page left by the scrollbar width only
   while a scrollbar is present. The stable gutter above already reserves that width permanently,
   so `100vw - 100%` is a constant 17px on every page whether or not it scrolls, and the two
   compensations stack: html starts 17px in from the left and ends 17px short of the right. With a
   permanent gutter the trick has nothing left to compensate, so it is neutralised here; the gutter
   alone keeps the modal-open shift fixed (FLX-1074). The three auth pages (body.flx-auth-page:
   login, forgot, reset) open no modal, so they also release the gutter and print edge to edge. */
@media (min-width: 992px) {
  html { margin-left: 0; }
}
html:has(> body.flx-auth-page) { scrollbar-gutter: auto; overflow-y: auto; }
body { padding-right: 0 !important; }
.modal { padding-left: 0 !important; padding-right: 0 !important; }
body.modal-open .sticky-top,
body:has(.offcanvas.show) .sticky-top { padding-right: 0 !important; }

/* added: FLX-1079 -- search-result preview in the shared SEO panel
   (modules/website/_seo_panel.php). Deliberately imitates a Google result rather than looking
   like the rest of the admin form: the point is for an admin to recognise it instantly as
   "what the public sees", and to watch their own title get truncated as they type past 60
   characters. Colours come from Tabler tokens so it follows the theme, and the blue/green are
   the two literal exceptions -- a result snippet that is not blue-on-green does not read as
   a search result at all. */
.flx-seo-serp {
  border: 1px solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius);
  background: var(--tblr-bg-surface);
  padding: 0.875rem 1rem;
  max-width: 600px;
  font-family: arial, sans-serif;
}
.flx-seo-serp-url {
  color: #202124;
  font-size: 0.75rem;
  line-height: 1.3;
  margin-bottom: 0.125rem;
  word-break: break-all;
}
.flx-seo-serp-title {
  color: #1a0dab;
  font-size: 1.125rem;
  line-height: 1.3;
  margin-bottom: 0.1875rem;
  min-height: 1.3em;
}
.flx-seo-serp-desc {
  color: #4d5156;
  font-size: 0.8125rem;
  line-height: 1.58;
  min-height: 1.58em;
}
[data-bs-theme="dark"] .flx-seo-serp-url { color: var(--tblr-body-color); }
[data-bs-theme="dark"] .flx-seo-serp-title { color: #8ab4f8; }
[data-bs-theme="dark"] .flx-seo-serp-desc { color: var(--tblr-secondary); }
/* An inherited value is shown at reduced emphasis so it is obvious the field is empty and this
   is the fallback, not something that was typed. */
.flx-seo-serp-inherited { opacity: 0.62; font-style: italic; }

/* added: FLX-1082 -- SEO audit scorecard. One row per check, colour-coded down the left edge
   so a long report scans vertically without reading a word of it. */
.flx-seo-score {
  font-size: 2.5rem;
  font-weight: 700;
  line-height: 1;
}
.flx-seo-check {
  border-left: 3px solid var(--tblr-border-color);
  padding: 0.625rem 0 0.625rem 0.875rem;
}
.flx-seo-check + .flx-seo-check { border-top: 1px solid var(--tblr-border-color-translucent); }
.flx-seo-check--pass { border-left-color: var(--tblr-success); }
.flx-seo-check--warn { border-left-color: var(--tblr-warning); }
.flx-seo-check--fail { border-left-color: var(--tblr-danger); }
/* added: FLX-1093 -- a finding the application cannot grade. Keeps the stock border colour so it
   reads as neutral rather than as a fourth severity, and is excluded from the score. */
.flx-seo-check--info { border-left-color: var(--tblr-border-color); }
.flx-seo-check-title { font-weight: 600; font-size: 0.875rem; }
.flx-seo-check-detail { font-size: 0.8125rem; color: var(--tblr-secondary); }

.flx-seo-page-id { flex: 1 1 auto; min-width: 0; }
.flx-seo-pages .accordion-button { font-size: 0.875rem; padding-top: 0.625rem; padding-bottom: 0.625rem; }
.flx-seo-pages .flx-seo-page-id .small { font-size: 0.8125rem; }
.accordion-button:not(.collapsed) .accordion-button-toggle { transform: rotate(180deg); }
/* added: FLX-1132 -- one accordion header size for the whole admin app: the compact row the user
   approved on the work order Jobs card. Tabler's default is 1rem/1.25rem padding at body size,
   which reads as a card header, not a row in a list. */
.accordion-button { padding: 0.5rem 0.75rem; gap: 0.75rem; font-size: 0.875rem; }

/* added: FLX-1127 -- the Jobs accordion on the work order page. A job's lines table is a
   DataTable with paging, info and length switched off (a job has a handful of lines), which
   still emits cardTableDom's footer strip, empty; hiding it is what removes the chrome the user
   objected to. The excerpt is the job's complaint on a collapsed row, bounded so a long one
   cannot push the total and the chevron off the header. */
.flx-jobs .dataTables_wrapper > .card-footer { display: none !important; } /* !important: the strip carries d-flex, itself !important */
.flx-jobs .accordion-item:first-of-type { border-top: 0; }
/* modified: FLX-1132 -- the header row. The toggle button grows; the action group sits beside it
   outside the button (a button cannot hold buttons). Line-count badge and total carry fixed
   widths so they land on the same x on every row; the name truncates, the rest never wraps.
   An open row draws a hairline under its header (Tabler removes it) so the narrative below reads
   as a section rather than running on from the title. | was: .flx-job-excerpt max-width 40% */
.flx-jobs .accordion-button { flex: 1 1 auto; min-width: 0; }
.flx-jobs .flx-job-no { font-family: var(--tblr-font-monospace); font-weight: 600; flex-shrink: 0; }
.flx-jobs .flx-job-name { min-width: 0; }
.flx-jobs .flx-job-lines { min-width: 4.25rem; justify-content: center; }
.flx-jobs .flx-job-total { min-width: 5.25rem; text-align: right; font-variant-numeric: tabular-nums; }
.flx-jobs .flx-job-actions { padding-right: 0.5rem; }
/* FLX-1132 round 3. A hovered or open row sits on a paper face (slate in dark) so the eye can tell
   which job it is reading; the transition keeps hover from flickering as the pointer crosses
   rows. The narrative is a tight label/value grid with a 6.5rem label column, its values are
   click-to-edit cells that keep their line breaks. The totals band under a job's lines is steel
   with white figures. */
.flx-jobs .flx-job-header { transition: background-color 0.15s ease; }
.flx-jobs .flx-job-header:hover,
.flx-jobs .flx-job-header:has(> .accordion-button:not(.collapsed)) { background-color: var(--flx-paper); }
.flx-jobs .flx-job-header .accordion-button:not(.collapsed) { background-color: transparent; }
.flx-jobs .flx-job-cc { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr); column-gap: 0.75rem; row-gap: 0.25rem; align-items: start; }
.flx-jobs .flx-job-cc > dt { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em; color: var(--tblr-body-color); padding-top: 0.25rem; margin: 0; }
.flx-jobs .flx-job-cc > dd { margin: 0; min-width: 0; }
.flx-jobs .flx-job-cc .flx-inline-edit--block { display: flex; width: 100%; min-width: 0; }
.flx-jobs .flx-job-cc .flx-inline-value,
.flx-jobs .flx-job-cc .flx-job-cc-text { white-space: pre-wrap; overflow-wrap: anywhere; }
.flx-jobs .flx-job-cc .flx-inline-edit textarea { min-height: 4.5rem; }
.flx-jobs .flx-job-totals { background-color: var(--flx-steel); color: #fff; }
.flx-jobs .flx-job-totals strong { font-weight: 600; font-variant-numeric: tabular-nums; }
.flx-jobs .flx-job-totals-sum { padding-left: 0.75rem; border-left: 1px solid rgba(255,255,255,0.25); }
.flx-jobs .dropdown-item-icon { width: 1rem; margin-right: 0.5rem; text-align: center; color: var(--tblr-secondary); }
.flx-jobs .accordion-collapse.collapsing { transition: height 0.3s ease; }
[data-bs-theme="dark"] .flx-jobs .flx-job-header:hover,
[data-bs-theme="dark"] .flx-jobs .flx-job-header:has(> .accordion-button:not(.collapsed)) { background-color: var(--flx-slate); }
/* Tabler's .btn-action is sized for its SVG .icon (negative margins); with a Font Awesome glyph it
   needs an explicit square. 1.75rem matches the compact header row. */
.flx-jobs .btn-action { width: 1.75rem; height: 1.75rem; min-height: 0; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 0.8125rem; }
.flx-jobs .accordion-button:not(.collapsed) { box-shadow: none; }
.flx-jobs .flx-job-header:has(> .accordion-button:not(.collapsed)) { border-bottom: 1px solid var(--tblr-border-color); }
.flx-jobs .flx-job-narrative { font-size: 9pt; color: var(--tblr-body-color); } /* modified: FLX-1175 -- 9pt, the job card's one size, per the user | was: 0.8125rem */
/* The lines table under a job: a step smaller and dark, like the narrative above it; the row
   action buttons are the same compact icon face as the header's. */
/* modified: FLX-1175 -- 9pt for the list, header row included, and the part number in the card's
   own font: FLX-1172's 8.5pt read too small and its monospace Part # stood out as a different
   family from the rest of the card, per the user. | was: 8.5pt, Part # monospace */
/* modified: FLX-1172 -- one size for the whole list, header row included (Tabler's th is .75rem,
   so it is pinned to the body's size). Part # is its own column, unwrapped; the stock icon in
   front of the number is the old Stock column's three facts in one glyph, help cursor for its
   tooltip. | was: font-size 0.8125rem, no Part # rules */
.flx-jobs .js-lines-table { font-size: 9pt; color: var(--tblr-body-color); }
.flx-jobs .js-lines-table > thead > tr > th { font-size: 9pt; }
.flx-jobs .js-lines-table > tbody > tr > td { padding-top: 0.375rem; padding-bottom: 0.375rem; color: var(--tblr-body-color); }
.flx-jobs .js-lines-table .flx-line-part { white-space: nowrap; }
.flx-jobs .js-lines-table .flx-line-stock { cursor: help; margin-right: 0.3rem; font-size: 1.1em; vertical-align: -0.05em; }
/* modified: FLX-1171 -- one kebab per line (the header's btn-action face) in place of the pencil +
   Issue/Return + x trio, and Description gets a 300px floor: with three controls gone from the
   Actions column the description no longer wraps at ~150px. The Issue/Return outline-button
   rule went with the button. | was: .btn-outline-primary compact sizing */
/* modified: FLX-1226 - Description is the table's one elastic column: width:100% on a cell of an
   auto-layout table means "everything the other columns do not need", and the browser clamps it
   so the eight nowrap columns keep their content width. The FLX-1171 300px floor did the opposite
   of what it was for: with every other column nowrap the table's minimum was ~865px, wider than
   the col-lg-8 card on any display under ~1900px, so the wrapper went into horizontal scroll and
   the Actions column was cut off at the card edge (the bleed the user circled). Now the
   description takes the slack at every width and gives it back first, wrapping at word
   boundaries (the td carries .text-wrap); the table only scrolls sideways once the description
   is down to its longest word. Selector dropped the .flx-jobs scope so the legacy single Lines
   card (a work order with no job rows) gets the same column model - same table, same init.
   Pairs with autoWidth:false on the .js-lines-table init in work-order.php: DataTables otherwise
   pins pixel widths on every th at init time (0px for a table inside a collapsed accordion, the
   then-current widths for a visible one), and a pinned table cannot reflow when the viewport
   changes. | was: .flx-jobs .js-lines-table .flx-line-desc { min-width: 300px; } */
.js-lines-table .flx-line-desc { width: 100%; }
.flx-jobs .js-lines-table .flx-line-actions { display: flex; justify-content: flex-end; }

/* modified: FLX-1101 -- the top margin moves onto the base rule so it applies to the FIRST step
   too, which FLX-1099's :not(:first-child) scoping left flush against whatever sits above the
   timeline. Per user direction.
   The :not(:first-child) rule below is now redundant -- same property, same value -- and is left
   in place only so this change stays a one-line diff; it can go whenever that file is next
   touched. The ::after connector is unaffected: its calc(100% + 1em) spans the gap BELOW each
   item, and every item that has a successor still carries the same 1em. */
.flx-activity-steps .step-item { padding-bottom: 0.875rem; margin-top: 1em; }
.flx-activity-steps .step-item:not(:first-child) { margin-top: 1em; }
.flx-activity-steps .step-item:not(:last-child)::after { height: calc(100% + 1em); }
.flx-activity-steps .step-item:last-child { padding-bottom: 0; }
.flx-activity-title { font-size: 0.875rem; line-height: 1.35; }
.flx-activity-detail { font-size: 0.8125rem; color: var(--tblr-secondary); }
.flx-activity-meta { font-size: 0.75rem; color: var(--tblr-secondary); }

.card-title {
  display: block;
  margin: 0 0 1rem;
  font-size: 0.85rem;
  color: inherit;
  line-height: 1.5rem;
  text-transform: uppercase;
}

/* added: FLX-1100 -- status badge on the page title line, beside the record accent.
   Sized to sit with the accent rather than compete with the title: the same small type as the
   breadcrumbs, so it registers without becoming the loudest thing on the page. */
.flx-title-badge {
  margin-left: .5em;
  vertical-align: middle;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .01em;
  text-transform: none;
}
.flx-title-badge[data-bs-toggle="tooltip"] { cursor: help; }

/* added: FLX-1102 -- a badge whose tooltip carries the detail that used to print beneath it.
   The cursor is the only affordance that the badge holds more than it shows, so it is not
   decoration; tabindex on the element itself makes the same text reachable without a mouse. */
.flx-badge-hint { cursor: help; }

/* added: FLX-1100 -- the quiet stage tracker (modules/fleetfix/work-order.php).
   Fixes three things reported on the FLX-1085 original: the type was body-sized and read as the
   loudest text on the page, the active step was bold, and the strip carried a stray left border
   and indent.

   THAT LAST ONE IS TABLER'S, NOT OURS. This build ships a SECOND, unscoped `.steps` rule after
   the first -- `margin-left:1rem; padding-left:2rem; border-left:1px solid; margin-bottom:2rem`,
   properties that belong to the vertical variant -- so every horizontal `.steps` in the app
   inherits a vertical rail it was never meant to have. Neutralised here rather than globally:
   .flx-activity-steps elsewhere is genuinely vertical and wants them. */
.flx-steps-quiet {
  /* modified: FLX-1152 -- the completed spine is GREEN (user direction); the two modifiers below
     turn it amber on hold and red when cancelled. | was: --tblr-secondary */
  --tblr-steps-color: var(--tblr-success);
  --tblr-steps-inactive-color: var(--tblr-border-color);
  margin: 0;
  padding-left: 0;
  border-left: 0;
}
.flx-steps-quiet .step-item { cursor: default; font-size: .8125rem; line-height: 1.35; }
/* Tabler sets .step-item.active{font-weight:bold}. Position is marked by colour here, not by
   weight -- weight on one item in a row of four is what made the strip shout. */
.flx-steps-quiet .step-item.active { font-weight: 400; }
.flx-steps-quiet .step-item.active::before { background: var(--tblr-success); } /* modified: FLX-1152 | was: primary */
.flx-steps-quiet .flx-step-label { display: block; color: var(--tblr-body-color); }
.flx-steps-quiet .step-item.active .flx-step-label { color: var(--tblr-success); font-weight: 500; } /* modified: FLX-1152 | was: primary */
/* added: FLX-1152 -- on hold: the whole spine amber, the step it is paused on included. */
.flx-steps-quiet--held { --tblr-steps-color: var(--tblr-warning); }
.flx-steps-quiet--held .step-item.active::before { background: var(--tblr-warning); }
.flx-steps-quiet--held .step-item.active .flx-step-label { color: var(--tblr-warning); }
/* added: FLX-1152 -- the clock icon on a step date sits at the date's own size. */
.flx-steps-quiet .flx-step-when .flx-time-tip { font-size: .7rem; }
.flx-steps-quiet .step-item.active ~ .step-item .flx-step-label { color: var(--tblr-disabled-color); }
/* The hold is a modifier on the step it pauses, so it sits inline with that step's label rather
   than as a badge beneath it. Amber carries the meaning; the parentheses carry the grammar. */
.flx-steps-quiet .flx-step-note { color: var(--tblr-warning); font-weight: 500; }
/* modified: FLX-1100 -- timestamp size and colour set by direct user direction after seeing it
   render. | was: font-size .75rem, color var(--tblr-secondary)
   Recorded rather than silently applied: #b5b5b5 on the card background measures about 2.05:1,
   against the 4.5:1 WCAG AA asks for at this size, and the literal hex does not follow the theme
   the way the token it replaces did. Deliberate call, made with the rendered page in view. */
.flx-steps-quiet .flx-step-when {
  display: block;
  margin-top: .125rem;
  font-size: .6rem;
  color: #b5b5b5;
  font-variant-numeric: tabular-nums;
}
/* A cancelled work order still shows the spine, with the step it stopped on marked in red
   instead of brand blue -- the shape of the job is still worth seeing.
   FLX-1106: the spine now ENDS at a Cancelled step, so .active is that step and the red lands on
   the word Cancelled rather than on Open, which was a stage the job genuinely completed. */
.flx-steps-quiet--stopped { --tblr-steps-color: var(--tblr-danger); } /* added: FLX-1152 -- the whole truncated spine red, not only the Cancelled step */
.flx-steps-quiet--stopped .step-item.active::before { background: var(--tblr-danger); }
.flx-steps-quiet--stopped .step-item.active .flx-step-label { color: var(--tblr-danger); }

/* added: FLX-1106 -- the FleetFix record summary list (work order details, work order totals).
   Applied to the <dl>, NOT to .card-body: the direction that prompted it was tested in devtools
   as `.card-body { font-size: small; line-height: 1 }`, which is every card body in the
   application -- dashboards, settings, every module -- for the sake of one list.
   line-height is 1.25 rather than the 1 that was tried: with a rule now drawn under each row,
   1 puts the descenders of one row against the line below them. Row rhythm comes from the
   padding instead, which is what the printed work order does between its line items.
   The rule colour is the border token, the themed equivalent of the PDF's #E5E7EB. */
.flx-detail-dl { font-size: .8125rem; line-height: 1.25; }
.flx-detail-dl > dt,
.flx-detail-dl > dd {
  margin-bottom: 0;
  padding-top: .375rem;
  padding-bottom: .375rem;
  border-bottom: 1px solid var(--tblr-border-color);
}
/* The last pair closes the list; a rule there would read as a divider with nothing under it. */
.flx-detail-dl > dt:last-of-type,
.flx-detail-dl > dd:last-of-type { border-bottom: 0; padding-bottom: 0; }
.flx-detail-dl > dt:first-of-type,
.flx-detail-dl > dd:first-of-type { padding-top: 0; }
/* added: FLX-1194 -- the session-view fact cards (Student, Session Details, Financial Details):
   the .flx-detail-dl treatment above, requested for this page -- 9pt, labels black and bold
   instead of text-muted, and a rule under every row but the last. Dotted rather than the
   FleetFix solid line, per the request. Applied to the <table> / the Financial card body, not
   to .card-body globally, for the same reason FLX-1106 gives above. The Financial rows keep
   their .border-bottom utility (an !important shorthand), so only its style is overridden. */
.flx-session-facts { font-size: 9pt; line-height: 1.3; }
.flx-session-facts .small { font-size: 9pt; }
.flx-session-facts > tbody > tr > th,
.flx-session-facts > tbody > tr > td {
  font-size: 9pt;
  padding: .375rem .25rem;
  vertical-align: top;
  border-bottom: 1px dotted var(--tblr-border-color);
}
.flx-session-facts > tbody > tr:last-child > th,
.flx-session-facts > tbody > tr:last-child > td { border-bottom: 0; }
.flx-session-facts > tbody > tr > th,
.flx-session-facts .flx-fact-label { color: #000; font-weight: 700; }
[data-bs-theme="dark"] .flx-session-facts > tbody > tr > th,
[data-bs-theme="dark"] .flx-session-facts .flx-fact-label { color: var(--tblr-body-color); }
.flx-session-facts > .d-flex.border-bottom { border-bottom-style: dotted !important; }
.flx-time-tip{font-size:.85em;margin-left:.15em;color:var(--tblr-secondary,#6c757d);opacity:.7;cursor:help;vertical-align:baseline}.flx-time-tip:hover{opacity:1}.flx-time-tooltip .tooltip-inner{font-size:.72rem;line-height:1.35;white-space:pre-line;text-align:left;padding:.3rem .5rem}
.flx-part-suggest .list-group-item{padding:.3rem .625rem;font-size:.8125rem;line-height:1.35;text-align:left}
/* added: FLX-1135 -- the typeahead never grows past ten-odd rows: its own scrollbar past 15rem,
   and a muted note row for "no match" / "first 10, keep typing" / "search failed". */
.flx-part-suggest{max-height:15rem;overflow-y:auto}
.flx-part-suggest .flx-part-note{color:var(--tblr-secondary);font-style:italic;cursor:default}
/* FLX-1128 -- .flx-btn-reveal: an icon button whose label slides out on hover (or keyboard focus),
   for utility actions whose icon already says what they do -- View PDF, Download, Email, Edit.
   Colours are the brand palette only: steel text at rest on a transparent face, navy text on a
   paper face with a stone hairline on hover, stone face when pressed. Lifecycle and destructive
   actions are NOT built on this -- they stay labelled, see docs 14.7. On a device with no hover
   the label is always out: a hover-only label on a phone is no label at all. */
.flx-btn-reveal{--tblr-btn-color:var(--flx-steel);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--flx-navy);--tblr-btn-hover-bg:var(--flx-paper);--tblr-btn-hover-border-color:var(--flx-stone);--tblr-btn-active-color:var(--flx-navy);--tblr-btn-active-bg:var(--flx-stone);--tblr-btn-active-border-color:var(--flx-stone);--tblr-btn-box-shadow:none;display:inline-flex;align-items:center;transition:color .15s ease,background-color .15s ease,border-color .15s ease}
.flx-btn-reveal .fa{font-size:.875rem;line-height:1}
.flx-btn-reveal-label{display:inline-block;max-width:0;opacity:0;overflow:hidden;white-space:nowrap;margin-left:0;transition:max-width .18s ease,opacity .12s ease,margin-left .18s ease}
.flx-btn-reveal:hover .flx-btn-reveal-label,.flx-btn-reveal:focus-visible .flx-btn-reveal-label,.flx-btn-reveal[aria-expanded="true"] .flx-btn-reveal-label{max-width:10rem;opacity:1;margin-left:.375rem}
@media (hover:none){.flx-btn-reveal-label{max-width:10rem;opacity:1;margin-left:.375rem}}
/* added: FLX-1129 -- the same button on a NAVY card header. Steel on navy is invisible, so the
   face is light at rest and goes dark (navy on paper) on hover / focus / open -- the user's ask. */
.flx-btn-reveal--on-dark{--tblr-btn-color:#fff;--tblr-btn-hover-color:var(--flx-navy);--tblr-btn-hover-bg:var(--flx-paper);--tblr-btn-hover-border-color:var(--flx-paper);--tblr-btn-active-color:var(--flx-navy);--tblr-btn-active-bg:var(--flx-stone);--tblr-btn-active-border-color:var(--flx-stone)}
.flx-on-dark-muted{color:rgba(255,255,255,.75)}
[data-bs-theme="dark"] .flx-btn-reveal{--tblr-btn-color:var(--flx-stone);--tblr-btn-hover-color:#fff;--tblr-btn-hover-bg:var(--flx-steel);--tblr-btn-hover-border-color:var(--flx-steel);--tblr-btn-active-color:#fff;--tblr-btn-active-bg:var(--flx-slate);--tblr-btn-active-border-color:var(--flx-steel)}

/* added: FLX-1199 (Epic FLX-1197) -- kiosk experiences table: a compact price input (currency
   prefix + amount) and a three-digit order field, so the row stays on one line at desktop width. */
.flx-price-input {
  max-width: 160px;
}
.flx-order-input {
  max-width: 80px;
}
/* added: FLX-1199 -- kiosk theme swatch preview on the Content tab. */
.flx-kiosk-swatch {
  display: inline-block;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 4px;
  border: 1px solid rgba(0,0,0,.15);
  vertical-align: middle;
}.flx-linx-ticks .table-responsive{max-height:26rem;overflow-y:auto}.flx-linx-ticks thead th{position:sticky;top:0;z-index:1;background:#fff}.flx-inline-number{max-width:8rem}.flx-settings-dl>dt{padding-top:.45rem}.flx-settings-dl .flx-inline-edit--block .flx-inline-value{white-space:pre-wrap;word-break:break-word;font-family:var(--tblr-font-monospace);font-size:.75rem}.flx-settings-dl .flx-inline-edit::after{opacity:.45}.flx-settings-dl .flx-inline-edit:hover::after,.flx-settings-dl .flx-inline-edit:focus-visible::after{opacity:1}.flx-settings-dl .flx-inline-edit{border-color:var(--tblr-border-color-translucent,rgba(0,0,0,.08))}.flx-settings-dl>dt.flx-settings-dt-wide{border-bottom:0;padding-bottom:.125rem}.flx-settings-dl>dd.flx-settings-dd-wide{padding-top:.125rem}.flx-settings-dl .flx-inline-edit--block{display:flex;width:100%}.flx-settings-dl .flx-inline-editor textarea{min-height:15rem;font-family:var(--tblr-font-monospace);font-size:.75rem}
.flx-public-page{background:#fff}.flx-public-container{max-width:880px}.flx-public-header{border-bottom:1px solid var(--tblr-border-color,rgba(0,0,0,.08))}.flx-legal-title{font-size:2rem;font-weight:600;line-height:1.2}.flx-legal-body{font-size:.9375rem}.flx-legal-body p,.flx-legal-body li{line-height:1.7}.flx-legal-body .lead{font-size:1.0625rem;color:var(--tblr-secondary)}.flx-legal-body h2{font-size:1.25rem;font-weight:600;margin-top:2.25rem;margin-bottom:.75rem;scroll-margin-top:1rem}.flx-legal-body h2:first-child{margin-top:0}.flx-legal-body h3{font-size:1rem;font-weight:600;margin-top:1.5rem;margin-bottom:.5rem}.flx-legal-body ul,.flx-legal-body ol{padding-left:1.25rem}.flx-legal-body li{margin-bottom:.375rem}.flx-legal-body .table{font-size:.875rem}.flx-legal-body .table td,.flx-legal-body .table th{vertical-align:top}.flx-legal-body code{font-size:.85em}.flx-legal-toc ol li{margin:.125rem 0}.flx-contact-side h3{font-size:1rem;margin-top:1.25rem}.flx-hp{position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;overflow:hidden;opacity:0}.flx-form-status{display:block;font-size:.9375rem}.flx-form-status--success{color:var(--tblr-green)}.flx-form-status--error{color:var(--tblr-red)}.flx-lp-editor{font-family:var(--tblr-font-monospace);font-size:.75rem;line-height:1.5;min-height:40rem;white-space:pre-wrap;overflow-wrap:anywhere;resize:vertical}.flx-lp-preview{display:block;width:100%;min-height:40rem;height:100%;border:1px solid var(--tblr-border-color);border-radius:var(--tblr-border-radius);background:#fff}.flx-lp-note{width:18rem;max-width:100%}@media (max-width:767.98px){.flx-lp-note{width:100%}.flx-lp-editor,.flx-lp-preview{min-height:24rem}}@media print{.flx-public-page{font-size:11pt}.flx-legal-body h2{page-break-after:avoid}.flx-legal-body a[href]::after{content:" (" attr(href) ")";font-size:.85em;color:#666}.flx-legal-body a[href^="#"]::after,.flx-legal-body a[href^="/"]::after{content:""}}
/* ================================================================================================
   FLX-1257 (Epic FLX-1256) -- UI quality baseline for data tables, toolbars and touch.
   Reported from production on 2026-09-18: Bookings > Pending Booking Requests, the Notes text
   painted underneath Convert to Student / Link to Existing Student. Measured in an offline
   harness (real stylesheet, real DataTables): the Notes <td> carried max-width:180px (.mw-180)
   under the FLX-679 app-wide nowrap rule. Chrome honours a max-width on a cell as a column cap
   and lets the content overflow the cell box; WebKit ignores it, so an iPad rendered the same
   row as one 1,100px line and scrolled instead. Two failures from one idiom. The rules below
   fix the idiom class, not the page:
     1. a column ROLE for free-text cells (.flx-col-text) that wraps with a floor and is elastic;
     2. the app's own width-cap helpers, when they land on a cell, wrap instead of overflowing;
     3. row-action lists never wrap their buttons onto a second line inside a cell;
     4. every .card-header wraps its children instead of shrinking or clipping them;
     5. DataTables pagination wraps, so a footer can never push a page into horizontal scroll
        (measured: at 375px every FlxUI.cardTableDom footer escaped its card to x=555);
     6. row-action buttons get the same coarse-pointer touch floor .btn-list already has.
   Alternatives rejected: scrollX:true on ~77 inits (DataTables then splits the header into a
   second table that must be kept in step by JS on every resize, the one thing that is fragile
   on rotation); the DataTables Responsive extension (not vendored, a new dependency, and it
   hides columns, which the owner ruled out); an inner wrapper block on every free-text cell
   (FLX-952's .flx-cell-wrap -- correct, but a markup change per cell; the td-level role is
   honoured by Blink and WebKit, the two engines that matter here, and Gecko degrades to a
   wrapping auto-width column with no overflow).
   Fails safe: a cell that is not marked wraps only when it carries a cap; everything else keeps
   the FLX-679 nowrap + horizontal-scroll behaviour the app already has, which the harness shows
   never overlaps once no cell is capped. Load: pure CSS, no JS, no DOM growth.
   ================================================================================================ */

/* 1. Free-text column roles. The floor stops the column collapsing to one character per line when
   the table is squeezed; the column is elastic (width:100%, below) so spare width lands on the
   text and not in a dead band beside it. overflow-wrap:anywhere is what actually reduces the cell's min-content width (FLX-952);
   word-break:break-word is the fallback for WebKit before 15.4. Two sizes: .flx-col-text for a
   note, description, message or reason (10rem floor, about 26 characters a line at table size);
   .flx-col-text-sm for a short free-text value such as a location or a source label (7rem). The
   floors were tuned in the harness: a 14rem floor on two columns made the Bookings request row
   1,363px wide at every width, which is a horizontal scroll on a 1280px display for no gain; at
   10rem / 7rem with icon row actions the same row is 916px and fits from 1366px up with the
   sidebar open, from 1280px with it collapsed. */
.table td.flx-col-text,
.table td.flx-col-text-sm,
.table-responsive .table td.flx-col-text,
.table-responsive .table td.flx-col-text-sm {
  white-space: normal;
  overflow-wrap: anywhere;
  word-break: break-word;
  min-width: 10rem;
}
/* modified: FLX-1257 follow-up (same day, production screenshot) -- the text column is the ELASTIC
   column, not a capped one | was: max-width 30rem / 16rem. A max-width on the cell caps the cell's
   own box, not the column: on a wide display the automatic layout still hands the column its share
   of the spare width, so the text wrapped at 480px inside a 585px column and the difference read
   as a dead band between the note and the row actions (the second Bookings capture). width:100%
   is the FLX-1226 elastic-column model: every pixel the other columns do not need goes to the
   text column, the short-token columns and the actions column sit at their content width, and
   the 10rem floor still holds when the table is squeezed. Two text columns share the spare width.
   Needs autoWidth:false on the DataTable init, otherwise DataTables pins a pixel width on the
   header cell at init time and the header and the body disagree about the column. */
.table td.flx-col-text,
.table-responsive .table td.flx-col-text,
.table th.flx-col-text,
.table-responsive .table th.flx-col-text {
  width: 100%;
}
.table td.flx-col-text-sm,
.table-responsive .table td.flx-col-text-sm {
  min-width: 7rem;
}
/* The floor lives on the td only. A floor on the th adds DataTables' 26px sort-icon padding on
   top of it and widened every text column by that much for nothing; the column is as wide as its
   widest cell either way. The class is still valid on a th (it marks the column in the markup). */

/* 2. A width cap on a cell must wrap, never overflow. Guards the app's own helpers so the FLX-1257
   defect cannot be re-created by markup; the UI lint (FLX-DT-05) refuses the idiom outright. The
   floor is there because wrap-anywhere alone let the harness squeeze the old Notes cell to about
   60px, one word per line, once the cap stopped it overflowing. */
.table td.mw-160, .table td.mw-180, .table td.mw-220, .table td.mw-240,
.table-responsive .table td.mw-160, .table-responsive .table td.mw-180,
.table-responsive .table td.mw-220, .table-responsive .table td.mw-240 {
  white-space: normal;
  overflow-wrap: anywhere;
  min-width: 8rem;
}

/* 3. Row actions stay on one line. Tabler's .btn-list wraps by design (a toolbar); inside a cell
   a wrapped button list is the "buttons wrap unpredictably" defect. The th/td that holds them
   uses Tabler's .w-1 so the column shrinks to its content and the free width goes elsewhere. */
.table td .btn-list {
  flex-wrap: nowrap;
}

/* 4. Card headers wrap. Tabler's .card-header is display:flex with no wrap, so a title beside a
   button shrank or clipped on a phone; 78 headers in the app add d-flex without flex-wrap. The
   FLX-734 canonical header already carries flex-wrap and gap-2; this gives every other header the
   same behaviour without touching them. row-gap only shows once a header has wrapped. */
.card-header {
  flex-wrap: wrap;
  row-gap: .5rem;
}

/* 5. DataTables chrome never pushes a page sideways. The pagination list is a flex row that could
   not wrap; nine buttons need about 330px and a phone card offers 302. The footer's right-hand
   group (length + pagination, FlxUI.cardTableDom) wraps too, see flx-ui.js. */
div.dataTables_wrapper div.dataTables_paginate ul.pagination {
  flex-wrap: wrap;
  justify-content: flex-end;
  row-gap: .25rem;
}
.dataTables_wrapper > .card-footer > .flx-dt-footer-tools {
  flex-wrap: wrap;
  min-width: 0;
  justify-content: flex-end;
}

/* 6. Touch floor for row-action buttons. The coarse-pointer block near the top of this file grows
   .btn-list .btn-sm and .btn-icon.btn-sm to 44px; about thirty older list pages write their row
   actions as <td><div class="d-flex gap-1"> and got nothing. Same floor, same phone tier: below
   576px the paint stays compact (--flx-ctl-h) and the FLX-854 synthetic ::after hit area, which
   already matches every .btn-sm, supplies the target. */
@media (pointer: coarse) {
  .table td .btn-sm {
    min-height: 44px;
  }
  .table td .btn-icon.btn-sm {
    min-width: 44px;
  }
}
@media (max-width: 575.98px) {
  .table td .btn-sm {
    min-height: var(--flx-ctl-h);
  }
  .table td .btn-icon.btn-sm {
    min-width: var(--flx-ctl-h);
  }
}

/* 7. The wrapper's horizontal scroll is the deliberate touch behaviour for a wide table (the
   owner's ruling: scroll, never drop a column). Contain the overscroll so a swipe that reaches the
   table's edge does not chain into the page. */
.table-responsive {
  overscroll-behavior-x: contain;
}
.flx-legal--size-small .flx-legal-body{font-size:.875rem}.flx-legal--size-small .flx-legal-body .lead{font-size:1rem}.flx-legal--size-small .flx-legal-body h2{font-size:1.125rem}.flx-legal--size-small .flx-legal-body h3{font-size:.9375rem}.flx-legal--size-medium .flx-legal-body{font-size:.9375rem}.flx-legal--size-large .flx-legal-body{font-size:1.0625rem}.flx-legal--size-large .flx-legal-body .lead{font-size:1.1875rem}.flx-legal--size-large .flx-legal-body h2{font-size:1.4375rem}.flx-legal--size-large .flx-legal-body h3{font-size:1.125rem}.flx-legal--size-large .flx-legal-title{font-size:2.25rem}.flx-legal--color-navy .flx-legal-body,.flx-legal--color-navy .flx-legal-title{color:var(--flx-navy,#081220)}.flx-legal--color-slate .flx-legal-body,.flx-legal--color-slate .flx-legal-title{color:var(--flx-slate,#1F2937)}.flx-legal--color-steel .flx-legal-body,.flx-legal--color-steel .flx-legal-title{color:var(--flx-steel,#374151)}.flx-legal--color-blue .flx-legal-body,.flx-legal--color-blue .flx-legal-title{color:var(--flx-blue,#0353A4)}.flx-legal--color-navy .flx-legal-body .lead,.flx-legal--color-slate .flx-legal-body .lead,.flx-legal--color-steel .flx-legal-body .lead,.flx-legal--color-blue .flx-legal-body .lead{color:inherit;opacity:.85}.flx-code-editor,.flx-code-editor.form-control,.flx-settings-dl .flx-inline-editor textarea{background:var(--flx-navy,#081220);color:var(--flx-paper,#F7F8FA);border-color:var(--flx-steel,#374151);font-family:var(--tblr-font-monospace);caret-color:#fff;tab-size:2}.flx-code-editor:focus,.flx-code-editor.form-control:focus,.flx-settings-dl .flx-inline-editor textarea:focus{background:var(--flx-navy,#081220);color:var(--flx-paper,#F7F8FA);border-color:var(--flx-blue,#0353A4);box-shadow:0 0 0 .25rem rgba(3,83,164,.35)}.flx-code-editor::placeholder,.flx-settings-dl .flx-inline-editor textarea::placeholder{color:rgba(247,248,250,.45)}.flx-code-editor::selection,.flx-settings-dl .flx-inline-editor textarea::selection{background:var(--flx-blue,#0353A4);color:#fff}.flx-code-editor:disabled,.flx-code-editor[readonly]{background:var(--flx-slate,#1F2937);color:var(--flx-stone,#E5E7EB)}.flx-contact-captcha .g-recaptcha{margin-top:.25rem}.flx-contact-captcha:empty{display:none}