/*
 * Bootstrap 5 Specific Overrides for LB-APP-BASE
 * This file contains CSS that adapts the application styles for Bootstrap 5
 *
 * Console/bricklet module fully converted to native BS5 classes (April 2026).
 * Remaining compat rules below are for classes still in use outside console/bricklets,
 * or utility classes used by JS (e.g. .hidden).
 */

/* App fonts (Manrope body, Outfit display; used via --app-font-* below).
   MUST be the first statement in the file: CSS ignores any @import that
   follows a style rule, which is exactly the bug that had this import dead
   further down the file - the fonts never loaded and everything silently
   fell back to Segoe UI/Arial. */
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Manrope:wght@400;500;600&display=swap");

/* ================================
 * WCAG 2.4.7 Focus Visibility
 * ================================
 */
/* Scoped to plain, un-componentized focusable elements only (links, custom
   tabindex widgets) — anything BS5 doesn't already style on focus. A prior
   version of this rule also targeted .btn/.form-control/.form-select/
   .form-check-input and cancelled their box-shadow, but those already get
   a correctly-colored, per-variant, shape-matching focus ring natively
   from BS5 (--bs-btn-focus-box-shadow keyed off --bs-btn-focus-shadow-rgb
   per .btn-* variant; .form-control:focus's own box-shadow). Overriding
   them flattened every variant to the same primary-colored rectangle and
   discarded BS5's native semantics for no benefit — removed. */
:focus-visible {
    outline: 2px solid var(--bs-primary, #0d6efd);
    outline-offset: 2px;
}

/* ================================
 * Modernization (2026): Default Typography
 * ================================
 * Source-of-truth for app fonts. Same contract as the --tln-* chrome knobs:
 * themes OWN --app-font-sans / --app-font-display and may set them in their
 * :root; main5.css only CONSUMES the knobs with the Performance baseline
 * (Manrope body, Outfit display) as the fallback. main5.css must never assign
 * --app-font-sans/--app-font-display itself - it loads AFTER the theme
 * stylesheet, so an assignment here would override every theme's choice and
 * make the knob dead (that was the case until 2026-07).
 * The font @import lives at the very top of this file (spec requirement).
 */
:root {
    --app-font-sans-default:    "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --app-font-display-default: "Outfit", "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

    /* Map Bootstrap's body font var to ours so .form-control, .btn, etc. follow */
    --bs-font-sans-serif: var(--app-font-sans, var(--app-font-sans-default));
    --bs-body-font-family: var(--app-font-sans, var(--app-font-sans-default));
}

body {
    font-family: var(--app-font-sans, var(--app-font-sans-default));
    line-height: 1.55;   /* Slightly more breathing room than BS5 default 1.5 */
}

h1, h2, h3, h4, h5, h6,
.h1, .h2, .h3, .h4, .h5, .h6 {
    font-family: var(--app-font-display, var(--app-font-display-default));
    font-weight: 600;
}

/* Modernized type scale — smaller, denser headings than Bootstrap defaults.
   Aligns with the Performance style guide and modern SaaS dashboards
   (Linear, Stripe, GitHub use similar steeper scales).

   Defaults below for reference vs new:
       BS5 default      Modernized
   h1: 2.5rem  (40px)   1.875rem (30px)   page title
   h2: 2rem    (32px)   1.5rem   (24px)   section title
   h3: 1.75rem (28px)   1.25rem  (20px)
   h4: 1.5rem  (24px)   1.125rem (18px)
   h5: 1.25rem (20px)   1rem     (16px)
   h6: 1rem    (16px)   0.875rem (14px)   small heading / caption */
h1, .h1 { font-size: 1.875rem; }
h2, .h2 { font-size: 1.5rem; }
h3, .h3 { font-size: 1.25rem; }
h4, .h4 { font-size: 1.125rem; }
h5, .h5 { font-size: 1rem; }
h6, .h6 { font-size: 0.875rem; }

/* Card-header sizing — applies regardless of inner markup. Widget JSPs use
   <span class="card-title"> for the title (sometimes alongside indicator
   icons); the .card-header font-size/weight governs the whole row. */
.card-header {
    font-size: 0.875rem;     /* 14px */
    font-weight: 600;
}

/* Console widget editor drag rows (PERF-14949). They look like small
   cards but are admin chrome, not content cards - dedicated classes keep
   branding card-header overrides (BrandCardHeaderFontSize via
   brand_overrides.jsp) from restyling them. Mirrors the main5 .card look;
   flex centers the widget name against the taller action buttons. */
.lb-drag-card {
    background-color: var(--bs-body-bg);
    border: var(--bs-border-width) solid rgba(0, 0, 0, 0.08);
    border-radius: 10px;
    box-shadow: var(--app-card-shadow);
    margin-bottom: 0.625rem;
}
.lb-drag-card-header {
    display: flex;
    align-items: center;
    padding: 0.625rem 1rem;
}
.lb-drag-card-header .widget-name {
    font-size: 1.125rem;
    font-weight: 600;
}

/* ============================================================
   Left-hand menu (campus config setting MenuLayout = "left")
   ============================================================
   Vertical rail rendered by leftMenu.xsl. Same data + permissions
   as the top menu, different shape. Theme-driven colors via
   var(--bs-primary). Collapsible to icon-only via .collapsed class
   (toggle button) or below 768px viewport (responsive). */

:root {
    --lb-left-menu-width: 240px;
    --lb-left-menu-collapsed-width: 50px;
}

body.has-left-menu {
    /* Shift main content right to make room for the fixed rail. */
    padding-left: var(--lb-left-menu-width);
    transition: padding-left 200ms ease;
}
body.has-left-menu.left-menu-collapsed {
    padding-left: var(--lb-left-menu-collapsed-width);
}

/* ── Brand override: menu surfaces ───────────────────────────────────
   The menu surface (top nav strip .banner-navbar + left rail .lb-left-menu)
   is painted from --tln-menu-bg / --tln-menu-fg, which the active theme
   sets at :root (only `default` and `bumblebee` do; other themes leave
   them unset so the consumers below fall through to --bs-primary).

   A campus brand override (BrandMenuBg / BrandMenuFg) is layered on by
   brand_overrides.jsp, which RE-EMITS --tln-menu-bg / --tln-menu-fg at
   :root. Because brand_overrides.jsp is the last stylesheet loaded (after
   the theme CSS and this file — see ui.jsp), its :root value wins over
   the theme's via normal cascade order. No per-element re-scoping rule is
   used here: an earlier attempt redefined --tln-menu-bg ON these elements
   as `var(--lb-menu-bg, var(--tln-menu-bg))`, but that self-references
   --tln-menu-bg in its own fallback, which the CSS Custom Properties spec
   treats as a cycle (invalid-at-computed-value-time) — so the consumers
   fell through to --bs-primary and bumblebee's yellow menu rendered black.

   --tln-menu-bg is read ONLY by .banner-navbar / .lb-left-menu rules in
   this file, so re-emitting it at :root affects only the menu surfaces.
   .lb-banner (the logo strip above the menu) reads --tln-header-bg, never
   --tln-menu-bg, so it stays on the BrandHeaderBg / theme header cascade
   independently. */

.lb-left-menu {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    width: var(--lb-left-menu-width);
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
    /* Thin custom scrollbar so the rail doesn't shift width when content
       overflows. Firefox uses scrollbar-* properties, WebKit uses pseudos. */
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
    z-index: 1000;
    transition: width 200ms ease;
    background-color: var(--tln-menu-bg, var(--bs-primary));
    color: var(--tln-menu-fg, rgba(255, 255, 255, 0.92));
    font-size: 0.875rem;
    /* Reuse BS5 dropdown classes (SmartMenus expects them) but override the
       defaults so nested submenus paint primary-rail-on-light, not white.
       Link colors honor --tln-menu-fg for themes with a light banner bg. */
    --bs-dropdown-bg: transparent;
    --bs-dropdown-color: var(--tln-menu-fg, rgba(255, 255, 255, 0.92));
    --bs-dropdown-border-width: 0;
    --bs-dropdown-border-color: transparent;
    --bs-dropdown-padding-y: 0;
    --bs-dropdown-padding-x: 0;
    --bs-dropdown-font-size: 0.875rem;
    --bs-dropdown-link-color: var(--tln-menu-fg, rgba(255, 255, 255, 0.78));
    --bs-dropdown-link-hover-color: var(--tln-menu-fg, #fff);
    --bs-dropdown-link-hover-bg: color-mix(in srgb, var(--tln-menu-bg, var(--bs-primary)) 80%, #000);
    --bs-dropdown-link-active-color: var(--tln-menu-fg, #fff);
    --bs-dropdown-link-active-bg: color-mix(in srgb, var(--tln-menu-bg, var(--bs-primary)) 70%, #000);
    --bs-dropdown-divider-bg: rgba(0, 0, 0, 0.15);
}
.lb-left-menu::-webkit-scrollbar {
    width: 6px;
}
.lb-left-menu::-webkit-scrollbar-track {
    background: transparent;
}
.lb-left-menu::-webkit-scrollbar-thumb {
    background-color: rgba(255, 255, 255, 0.25);
    border-radius: 3px;
}
.lb-left-menu::-webkit-scrollbar-thumb:hover {
    background-color: rgba(255, 255, 255, 0.4);
}
.lb-left-menu.collapsed,
body.left-menu-collapsed .lb-left-menu {
    width: var(--lb-left-menu-collapsed-width);
}

/* Header strip: home icon + collapse toggle */
/* Header strip aligns with the top banner row's height so the top-left
   square reads cleanly against the banner. Same token both places, so a
   change to the rail's collapsed width keeps them aligned. */
.lb-left-menu-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: var(--lb-left-menu-collapsed-width);
    padding: 0 1rem;
    border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
.lb-left-menu-home,
.lb-left-menu-toggle {
    color: var(--tln-menu-fg, rgba(255, 255, 255, 0.92));
    background: transparent;
    border: 0;
    font-size: 1.25rem;
    cursor: pointer;
}
.lb-left-menu-home:hover,
.lb-left-menu-toggle:hover {
    color: var(--tln-menu-fg, #fff);
}

/* Top-level nav-link rows */
.lb-left-menu .nav-link {
    display: flex;
    align-items: flex-start;
    color: var(--tln-menu-fg, rgba(255, 255, 255, 0.92));
    padding: 0.5rem 1rem;
    background: transparent;
    text-decoration: none;
    /* Long labels (e.g. "Campus Administrative Functions") wrap within the
       fixed-width rail instead of being clipped off the right edge. */
    white-space: normal;
    overflow-wrap: anywhere;
}
.lb-left-menu .nav-link:hover,
.lb-left-menu .nav-link:focus {
    color: var(--tln-menu-fg, #fff);
    background-color: color-mix(in srgb, var(--tln-menu-bg, var(--bs-primary)) 80%, #000);
}
.lb-left-menu .nav-link i.bi {
    flex-shrink: 0;
    width: 1.25rem;
    font-size: 1rem;
    /* Match the label's line box so the icon sits on the first wrapped line
       (align-items: flex-start) rather than centered against the whole block. */
    line-height: 1.5;
}

/* Collapsed-state: hide labels visually but keep them in the a11y tree
   (BS5 .visually-hidden pattern, inlined). Screen readers still announce them. */
.lb-left-menu.collapsed .menu-label,
body.left-menu-collapsed .lb-left-menu .menu-label {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important;
    white-space: nowrap !important;
    border: 0 !important;
}

/* Rail tooltips are useful only in icon-only (collapsed) state. When
   expanded, the menu labels are visible so a popup of the same text is
   noise. Marker class lb-rail-tooltip is set on the popup template at
   init (frameset_include.jsp), so this rule scopes precisely to those
   popups — never affects tooltips on other parts of the page. */
body:not(.left-menu-collapsed) .lb-rail-tooltip {
    display: none !important;
}

/* Collapsed: only the hamburger fits comfortably at 50px — hide the
   header-strip home icon (a list-row Home shows in the menu instead). */
body.left-menu-collapsed .lb-left-menu-home {
    display: none;
}
body.left-menu-collapsed .lb-left-menu-header {
    justify-content: center;
}
/* Expanded: hide the list-row Home (the header-strip home is showing). */
body:not(.left-menu-collapsed) .lb-left-menu-home-row {
    display: none;
}
.lb-left-menu.collapsed .nav-link,
body.left-menu-collapsed .lb-left-menu .nav-link {
    justify-content: center;
    padding-left: 0.5rem;
    padding-right: 0.5rem;
}
.lb-left-menu.collapsed .nav-link i.bi,
body.left-menu-collapsed .lb-left-menu .nav-link i.bi {
    margin: 0 !important;
}
/* SmartMenus5 BS5 addon draws a rounded-square border around the +/- glyph
   on collapsible menus (.navbar-nav.sm-collapsible .sub-arrow, see
   jquery.smartmenus.bootstrap-5.css). Drop the chrome so it's the glyph
   alone — applies to both the left rail (sm-vertical) and the top menu's
   collapsed (hamburger-expanded) mode. Specificity must beat the addon's
   bare `.navbar-nav.sm-collapsible .sub-arrow` rule. */
.lb-left-menu .navbar-nav.sm-collapsible .sub-arrow,
.banner-navbar .navbar-nav.sm-collapsible .sub-arrow {
    border: 0;
    background: transparent;
}

/* Collapsed rail has no room for the +/- caret on parent items, and clicking
   the icon already expands the rail — hide the caret in collapsed state. */
.lb-left-menu.collapsed .nav-link .sub-arrow,
body.left-menu-collapsed .lb-left-menu .nav-link .sub-arrow {
    display: none !important;
}
/* Without the caret, drop SmartMenus' reserved right-padding too. */
.lb-left-menu.collapsed .has-submenu,
body.left-menu-collapsed .lb-left-menu .has-submenu {
    padding-right: 0.5rem !important;
}

/* Nested submenu — flat vertical (SmartMenus collapsible), not a floating popover.
   BS5 sets --bs-dropdown-* on .dropdown-menu itself, beating the inherited values
   from .lb-left-menu. Override the vars at this scope so they propagate to items.
   SmartMenus toggles display:block/none. Wrap content in a grid row that animates
   from 0fr → 1fr for a smooth slide-down without measuring heights in JS. */
.lb-left-menu .dropdown-menu {
    position: static !important;
    transform: none !important;
    float: none;
    border: 0;
    margin: 0;
    box-shadow: none;
    --bs-dropdown-bg: transparent;
    --bs-dropdown-color: var(--tln-menu-fg, rgba(255, 255, 255, 0.92));
    --bs-dropdown-padding-y: 0;
    --bs-dropdown-padding-x: 0;
    --bs-dropdown-font-size: 0.875rem;
    --bs-dropdown-link-color: var(--tln-menu-fg, rgba(255, 255, 255, 0.78));
    --bs-dropdown-link-hover-color: var(--tln-menu-fg, #fff);
    --bs-dropdown-link-hover-bg: color-mix(in srgb, var(--tln-menu-bg, var(--bs-primary)) 80%, #000);
    --bs-dropdown-link-active-color: var(--tln-menu-fg, #fff);
    --bs-dropdown-link-active-bg: color-mix(in srgb, var(--tln-menu-bg, var(--bs-primary)) 70%, #000);
    --bs-dropdown-divider-bg: rgba(0, 0, 0, 0.15);
}
.lb-left-menu .dropdown-item {
    padding: 0.4rem 1rem 0.4rem 3rem;
    white-space: normal;
}
/* SmartMenus tags the "current" item with .highlighted plus BS utilities
   (bg-light/text-dark). Override so the current item stays in our palette. */
.lb-left-menu .dropdown-item.highlighted,
.lb-left-menu .dropdown-item.bg-light,
.lb-left-menu .nav-link.highlighted,
.lb-left-menu .nav-link.bg-light {
    background-color: color-mix(in srgb, var(--tln-menu-bg, var(--bs-primary)) 70%, #000) !important;
    color: var(--tln-menu-fg, #fff) !important;
}
.lb-left-menu .dropdown-item.text-dark,
.lb-left-menu .nav-link.text-dark {
    color: var(--tln-menu-fg, #fff) !important;
}
.lb-left-menu.collapsed .dropdown-menu,
body.left-menu-collapsed .lb-left-menu .dropdown-menu {
    /* Hide nested submenus when rail is collapsed — only icons show. */
    display: none !important;
}

/* Responsive: below 768px the JS adds .left-menu-collapsed to <body>
   (frameset_include.jsp uses matchMedia). All collapsed-state styling
   lives on body.left-menu-collapsed selectors above. */

/* Below sm (<576px): on phones, hide the left rail entirely and let the
   top-menu hamburger nav take over. Even the 50px collapsed rail steals
   ~13% of a 390px viewport, and a phone has no use for a permanently
   visible side rail. Tablets (>=576px) keep the rail (collapsed to 50px
   at <768px via the existing JS matchMedia handler). menu_include.jsp
   renders the top navbar AND the left rail when MenuLayout=left; CSS
   swaps which one is visible at this breakpoint. The .lb-mobile-only-nav
   class is added to the top navbar wrapper when in left-menu mode so it's
   hidden above this breakpoint. */
@media (max-width: 575.98px) {
    body.has-left-menu,
    body.has-left-menu.left-menu-collapsed {
        padding-left: 0;
    }
    body.has-left-menu .lb-left-menu {
        display: none;
    }
    .lb-mobile-only-nav {
        display: block;
    }
}
@media (min-width: 576px) {
    /* Above sm: top navbar in left-menu mode is hidden; the rail takes over. */
    .lb-mobile-only-nav {
        display: none;
    }
}

/* Headless widget — admin opted out of the panel header per-instance via
   pc_consolewidget.headless. The console renderer adds .widget-headless on
   the slot wrapper (and widget.jsp adds it to the standalone container).
   Hides the header (title, chevron, header-embedded controls). The card
   body becomes the full visual surface; top accent stripe from
   .border-{style} still shows. */
.widget-headless .card-header {
    display: none;
}

/* Borderless widget — admin opted out of the card chrome per-instance via
   pc_consolewidget.borderless. The console renderer adds .widget-borderless
   on the slot wrapper (and widget.jsp adds it to the standalone container).
   Removes the shadow and the hairline border so the widget melts into the
   console background; the card surface (background) stays. Combines with
   .widget-headless for a fully chromeless widget. */
.widget-borderless .card {
    box-shadow: none;
    border-color: transparent;
}

/* App menu strip (frameset top navigation).
   Markup uses .navbar.navbar-dark for white text; this rule fills in the
   background color from the theme's secondary token. Themes that want a
   different menu color should override --bs-secondary (preferred) or
   .banner-navbar's background-color directly.

   Themes that want a LIGHT menu strip need to swap navbar-dark -> navbar-light
   in markup AND override these rules — see plan for the override pattern. */
.banner-navbar {
    background-color: var(--tln-menu-bg, var(--bs-primary));
    /* Feed BS5's own navbar CSS vars from --tln-menu-fg so the built-in
       .navbar-nav .nav-link { color: var(--bs-navbar-color) } rule resolves
       to the theme's banner foreground. Fallback values match navbar-dark
       defaults so unchanged themes look the same. */
    --bs-navbar-color: var(--tln-menu-fg, rgba(255, 255, 255, 0.55));
    --bs-navbar-hover-color: var(--tln-menu-fg, rgba(255, 255, 255, 0.75));
    --bs-navbar-active-color: var(--tln-menu-fg, #fff);
    --bs-navbar-brand-color: var(--tln-menu-fg, #fff);
    --bs-navbar-brand-hover-color: var(--tln-menu-fg, #fff);
    --bs-navbar-disabled-color: var(--tln-menu-fg, rgba(255, 255, 255, 0.25));
}

/* navbar-dark defaults are intentionally dim (white@55%/75% for hierarchy on
   dedicated dark navbars). For Performance's app menu strip, links are the
   primary navigation surface — full white reads better.
   --tln-menu-fg lets themes override the link color when --tln-menu-bg is
   a light color (e.g. bumblebee yellow needs dark text). */
.banner-navbar .nav-link {
    color: var(--tln-menu-fg, rgba(255, 255, 255, 0.92));
    background-color: transparent;
    font-size: 0.875rem;   /* 14px — was 1rem (16px) */
}
.banner-navbar .nav-link:hover,
.banner-navbar .nav-link:focus {
    color: var(--tln-menu-fg, #fff);
    background-color: transparent;
}
/* Active / dropdown-open parent items: keep transparent bg (SmartMenus/BS5
   default would paint a light bg on .show or aria-expanded=true, which flashes
   white against the dark strip). */
.banner-navbar .nav-link.active,
.banner-navbar .nav-link.show,
.banner-navbar .nav-item.show > .nav-link,
.banner-navbar .nav-link[aria-expanded="true"] {
    color: var(--tln-menu-fg, #fff);
    background-color: transparent;
}

/* SmartMenus5 adds .bg-light + .highlighted to active parents on hover/open.
   .bg-light is a BS utility with !important — must override with !important.
   TOP-STRIP parent items only (direct children of the navbar's nav list) —
   darken from --tln-menu-bg so the highlight matches the strip color.
   Parent items INSIDE the popout panel are handled below by the dropdown's
   own --tln-menu-dropdown-bg, so they darken from the panel color (e.g.
   white -> grey on bumblebee) instead of the banner color (yellow). */
.banner-navbar > .container-fluid > .navbar-collapse > .navbar-nav > li > .nav-link.bg-light,
.banner-navbar > .container-fluid > .navbar-collapse > .navbar-nav > li > .nav-link.highlighted {
    background-color: color-mix(in srgb, var(--tln-menu-bg, var(--bs-primary)) 80%, #000) !important;
    color: var(--tln-menu-fg, #fff) !important;
}

/* Parent items inside the popout panel: darken from the dropdown panel
   color so a yellow-banner / white-panel theme like bumblebee gets a
   grey highlight on nested parents (matching leaf-item hover), not
   a yellow highlight that bleeds the banner color into the panel. */
.banner-navbar .dropdown-menu .nav-link.bg-light,
.banner-navbar .dropdown-menu .nav-link.highlighted,
.banner-navbar .dropdown-menu .dropdown-item.bg-light,
.banner-navbar .dropdown-menu .dropdown-item.highlighted {
    background-color: color-mix(in srgb, var(--tln-menu-dropdown-bg, var(--tln-menu-bg, var(--bs-primary))) 80%, #000) !important;
    color: var(--tln-menu-dropdown-fg, var(--tln-menu-fg, #fff)) !important;
}

/* Top-banner dropdown (children) bg/fg.
   Independent variables so a theme can make the popout panel a different
   palette than the banner strip itself — e.g. bumblebee keeps a yellow
   banner but pops white-bg dark-text children for high contrast, the way
   the BS3 theme always did. Defaults chain: dropdown-bg falls through to
   --tln-menu-bg, which falls through to --bs-primary; dropdown-fg falls
   through to --tln-menu-fg, which falls through to white. */
.banner-navbar .dropdown-menu {
    --bs-dropdown-bg: var(--tln-menu-dropdown-bg, var(--tln-menu-bg, var(--bs-primary)));
    --bs-dropdown-color: var(--tln-menu-dropdown-fg, var(--tln-menu-fg, rgba(255, 255, 255, 0.92)));
    --bs-dropdown-link-color: var(--tln-menu-dropdown-fg, var(--tln-menu-fg, rgba(255, 255, 255, 0.92)));
    /* Hover/active darken the ACTUAL surface (--bs-dropdown-bg), not a fixed
       reference. White panels → gray hover; yellow banner panels → dark-yellow
       hover. Single formula, follows whatever surface the current menu inherits. */
    --bs-dropdown-link-hover-color: var(--tln-menu-dropdown-fg, var(--tln-menu-fg, #fff));
    --bs-dropdown-link-hover-bg: color-mix(in srgb, var(--bs-dropdown-bg) 80%, #000);
    --bs-dropdown-link-active-color: var(--tln-menu-dropdown-fg, var(--tln-menu-fg, #fff));
    --bs-dropdown-link-active-bg: color-mix(in srgb, var(--bs-dropdown-bg) 70%, #000);
    --bs-dropdown-border-color: color-mix(in srgb, var(--tln-menu-dropdown-bg, var(--tln-menu-bg, var(--bs-primary))) 80%, #000);
    --bs-dropdown-divider-bg: color-mix(in srgb, var(--tln-menu-dropdown-bg, var(--tln-menu-bg, var(--bs-primary))) 70%, #000);
    --bs-dropdown-header-color: var(--tln-menu-dropdown-fg, var(--tln-menu-fg, rgba(255, 255, 255, 0.7)));
    --bs-dropdown-font-size: 0.875rem;   /* 14px to match top-nav links */

    /* Direction of the slide animation. Set by JS at runtime on top-level
       dropdowns only. Nested submenus stay at 0 → just fade. */
    --menu-slide-distance: 0px;

    /* Fade-in/out + directional slide. CSS transitions don't fire on
       display:none→block, so @starting-style defines the "just appeared"
       state for the open. SmartMenus writes inline opacity:0 during its
       fade-out — we catch that to trigger a parallel slide via transform.
       Matches SmartMenus's default hideDuration (200ms) for symmetry. */
    transition: opacity 200ms ease-out, transform 200ms ease-out;
    opacity: 1;
    transform: translateX(0);
}
/* Slide direction during fade-out. Top-level dropdowns get a non-zero
   distance from JS; nested submenus inherit the default 0 → fade only. */
.banner-navbar .dropdown-menu[style*="opacity: 0"] {
    transform: translateX(var(--menu-slide-distance));
}
/* Slide direction during fade-in. Same variable, so nested submenus fade
   without horizontal movement. */
@starting-style {
    .banner-navbar .dropdown-menu {
        opacity: 0;
        transform: translateX(var(--menu-slide-distance));
    }
}

@media (prefers-reduced-motion: reduce) {
    .banner-navbar .dropdown-menu {
        transition: none;
        transform: none;
    }
}

/* Below the navbar-expand-md breakpoint the top menu collapses to a vertical
   stack inside the hamburger panel. In that mode, nested dropdowns should
   inherit the banner color all the way down (matches BS3 behavior) — not
   the light popover surface (--tln-menu-dropdown-bg) used in expanded mode. */
@media (max-width: 767.98px) {
    .banner-navbar .dropdown-menu {
        --bs-dropdown-bg: var(--tln-menu-bg, var(--bs-primary));
        --bs-dropdown-color: var(--tln-menu-fg, rgba(255, 255, 255, 0.92));
        --bs-dropdown-link-color: var(--tln-menu-fg, rgba(255, 255, 255, 0.92));
        --bs-dropdown-link-hover-color: var(--tln-menu-fg, #fff);
        --bs-dropdown-link-hover-bg: color-mix(in srgb, var(--bs-dropdown-bg) 80%, #000);
        --bs-dropdown-link-active-color: var(--tln-menu-fg, #fff);
        --bs-dropdown-link-active-bg: color-mix(in srgb, var(--bs-dropdown-bg) 70%, #000);
        --bs-dropdown-border-color: transparent;
        --bs-dropdown-divider-bg: color-mix(in srgb, var(--tln-menu-bg, var(--bs-primary)) 70%, #000);
    }
    /* SmartMenus current-item highlight: in collapsed hamburger mode, the
       nested dropdown panel inherits banner bg (rules above), so the existing
       .dropdown-menu .nav-link.highlighted override (which uses
       --tln-menu-dropdown-bg) lands on the wrong color anchor. Override here
       so highlight darkens from banner-bg, matching the panel itself. */
    .banner-navbar .dropdown-menu .nav-link.bg-light,
    .banner-navbar .dropdown-menu .nav-link.highlighted,
    .banner-navbar .dropdown-menu .dropdown-item.bg-light,
    .banner-navbar .dropdown-menu .dropdown-item.highlighted {
        background-color: color-mix(in srgb, var(--tln-menu-bg, var(--bs-primary)) 70%, #000) !important;
        color: var(--tln-menu-fg, #fff) !important;
    }
}

/* User-dropdown info header (username + version), shown above the menu items
   in the user popover. Two short lines, muted, non-interactive. */
.lb-banner-user-info {
    line-height: 1.3;
    white-space: normal;
    font-size: 0.75rem;
}

/* Guest/external users have no user dropdown, so banner_include.jsp emits a bare
   standalone build-version string instead (.lb-banner-user-info-external, in the
   iuserIsExternal branch = the guest-login flag), gated by the ShowServerInfoInBanner
   campus config. A logged-out guest has no reason to see the build version, so
   hide it. display:none KEEPS it in the DOM, so the build is still readable by
   inspecting. The logged-in version (inside the dropdown popover,
   .lb-banner-user-info above) is unaffected. (PERF-14937) */
.lb-banner-user-info-external {
    display: none;
}

/* Banner button cluster. The overall band height still tracks the collapsed
   left-rail width (via the pill height + margins below), so the top-left corner
   stays aligned with the rail. */
.lb-banner-row {
    /* Inset the pill vertically: reduce its height by the top+bottom margins so
       the band still totals the collapsed-rail width (38px pill + 0.375rem x2),
       leaving a sliver of header above and below so all four corners round
       instead of the pill sitting flush against the window top. */
    min-height: calc(var(--lb-left-menu-collapsed-width) - 0.75rem);
    margin-top: 0.375rem;
    margin-bottom: 0.375rem;
    padding-top: 0;
    padding-bottom: 0;
    /* Opt-in rounded backing behind the banner action buttons, for campuses
       whose custom header image makes the buttons hard to read. Hugs its
       buttons (fit-content, pushed to the right edge) instead of filling the
       column as a square. Background is the SAME menu colour as the nav strip
       (.banner-navbar) at --lb-banner-bg-opacity, which defaults to 0 = fully
       transparent, so unbranded campuses see nothing. Text colour is unchanged
       (the .lb-banner rule sets --tln-header-fg); a campus that turns the pill
       dark pairs it with Header Text in the branding editor.

       PERF-14983: this row (and everything inside it - the button group and
       the user block) is intentionally nowrap at every width, never wrapped.

       width: fit-content hugs the content so the background pill isn't a
       full-column-width block with dead space to the left of the buttons.
       max-width: 100% is REQUIRED alongside it and must never be removed:
       for nowrap content, min-content == max-content == the full content
       width (nothing can shrink), so fit-content alone resolves to that
       full width. Without the max-width clamp, when the content is wider
       than the column (e.g. left-menu layout eats 240px, so col-lg-6 is
       only ~390px - not enough for all labels + the username) the pill
       grows past the column's right edge, extends the document, and
       creates a page-level horizontal scrollbar with the rightmost item
       (username) pushed off-screen. max-width: 100% clamps the pill to the
       column so it can never push the page wider; any content that still
       doesn't fit spills LEFT over the logo instead of scrolling the page.
       Fully preventing that leftward spill (hiding labels when the column
       is genuinely too narrow) is a separate, deferred concern - the
       priority fix here is just: the pill never causes a page scrollbar. */
    /* flex-wrap lives HERE, not as a flex-nowrap utility in banner_include.jsp:
       the utility's !important would beat the .lb-banner-wrap last-resort
       override below. */
    flex-wrap: nowrap;
    width: fit-content;
    max-width: 100%;
    margin-left: auto;
    padding-left: 0.5rem;
    padding-right: 0.5rem;
    border-radius: var(--bs-border-radius, 0.375rem);
    /* PERF-14983: old-iOS-safe base value; the exact colour is layered on by
       the @supports (color-mix) block right below - the two rules are a pair.
       color-mix() needs Safari 16.2+ (iOS 16.2, Dec 2022); on older iOS the
       original color-mix-only declaration failed and the pill painted nothing
       (white on white). This rgba(tuple) form works back to iOS ~10. It reads
       the menu colour as an RGB TUPLE: core themes that set --tln-menu-bg
       also set --tln-menu-bg-rgb (to-rgb() in theme.scss), and
       brand_overrides.jsp emits both when BrandMenuBg is set. NOTE: a plain
       same-property fallback pair does NOT work here - declarations
       containing var() always win the cascade at parse time and only fail at
       computed-value time (-> transparent, the bug again), which is why the
       color-mix version lives in an @supports block instead. */
    background-color: rgba(var(--tln-menu-bg-rgb, var(--bs-primary-rgb)),
        var(--lb-banner-bg-opacity, 0));
}
/* Second half of the PERF-14983 pair above. On browsers with color-mix()
   (Safari/iOS 16.2+, Chrome 111+, Firefox 113+) mix the menu colour directly,
   so themes that set only --tln-menu-bg (no -rgb tuple - e.g. client-repo
   campus themes in PPC/Daimler) still get the exact colour; their old-iOS
   fallback tints with --bs-primary-rgb (visible, approximate) until they add
   the tuple. The condition is deliberately var()-free so old parsers evaluate
   it and skip the block. */
@supports (color: color-mix(in srgb, red 50%, blue)) {
    .lb-banner-row {
        background-color: color-mix(in srgb,
            var(--tln-menu-bg, var(--bs-primary)) calc(var(--lb-banner-bg-opacity, 0) * 100%),
            transparent);
    }
}

/* Banner background color is opt-in per theme via --tln-menu-bg. Themes
   that set it win; themes that don't fall back to --bs-primary, which is
   what existing branded themes (TLN, etc.) want. Default theme picks a
   calm slate so the unbranded look isn't the BS5 default-blue. */
.banner-navbar {
    background-color: var(--tln-menu-bg, var(--bs-primary));
}
/* Banner row child wrappers — legacy padding from the old multi-row layout
   pushed elements vertically. Reset so flex centering aligns them cleanly.
   Also re-assert the header text colour directly on these wrappers (not just
   inherited from .lb-banner) so a campus's old per-element custom CSS — e.g.
   Daimler's server-side `.lb-banner-user { color:#444 }` — can't override the
   branded Header Text. A direct child rule beats an inherited value even when
   the parent is !important, so the only way to win without editing their
   branding files is to match these elements directly at higher specificity
   (0,0,2,0 here vs the custom rule's 0,0,1,0). No !important needed. */
.lb-banner-row .lb-banner-links,
.lb-banner-row .lb-banner-links-user,
.lb-banner-row .lb-banner-user {
    padding: 0;
    color: var(--tln-header-fg, inherit);
}
/* Space the user block off the last action button. The row has NO gap
   utility - the buttons sit flush (their own padding separates them, same
   look as the old btn-group), so the only inter-child spacing needed is
   before the user block. Logical property so RTL mirrors. */
.lb-banner-row .lb-banner-links-user {
    margin-inline-start: 0.5rem;
}
/* Banner action affordances (Messages / Search / Help / Log Off). Plain
   icon+label links, no BS5 button machinery. Theme-agnostic - inherits
   the banner row's text color and stays transparent against whatever
   background the theme provides for the banner. */
.lb-banner-action {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.25rem 0.5rem;
    border-radius: 0.375rem;
    color: inherit;
    text-decoration: none;
    cursor: pointer;
    background-color: transparent;
    border: 0;
    transition: background-color 120ms ease;
}
/* PERF-14983 auto-fit: fitBannerActions() (frameset_include.jsp) adds
   .lb-banner-icons-only to the row when the labelled buttons + username would
   not fit the actions column's real available width, collapsing every label
   (button text AND the username) to icons only. Icons keep their title
   attribute so the labels are still available as tooltips. This is the
   runtime, measurement-based replacement for the old viewport breakpoint -
   see the .lb-banner-actions-col comment in banner_include.jsp. */
.lb-banner-row.lb-banner-icons-only .banner-btn-text {
    display: none;
}
/* Compact tier (between icons-only and wrap): fitBannerActions() adds this
   when the icons-only cluster still doesn't fit - tightens side padding
   (0.5rem -> 0.25rem, ~8px/button) so narrow phones fit on one line before
   the wrap tier has to break the row. */
.lb-banner-row.lb-banner-compact .lb-banner-action {
    padding: 0.25rem;
}
/* Last-resort tier (PERF-14983, iPhone report): fitBannerActions() adds this
   only when even the icons-only cluster is wider than the actions column
   (very narrow phones). Lets the icon buttons wrap to a second line INSIDE
   the pill instead of spilling out of it; normal sizes stay nowrap. Works
   at the row level because the action buttons are DIRECT flex children of
   .lb-banner-row (banner_include.jsp - no wrapper divs around them), so
   wrap can break between individual buttons. */
.lb-banner-row.lb-banner-wrap {
    flex-wrap: wrap;
}
.lb-banner-action:hover,
.lb-banner-action:focus-visible {
    background-color: rgba(0, 0, 0, 0.05);
    color: inherit;
    text-decoration: none;
    outline: none;
}
/* Banner-row icons: uniform sizing + softening across all icon fonts. Applies
   to both icons-only mode (banner-btn-image-div) and the user-name person
   icon. Theme-agnostic — was previously in tln-light theme as component
   chrome, moved here so every theme renders the banner identically. */
.lb-banner .banner-btn-icon,
.lb-banner .banner-btn-icon-lg,
.lb-banner-user > .bi {
    font-weight: 300;
    line-height: 1;
    opacity: 0.62;
    transition: opacity 120ms ease;
}
/* Keep the person icon + name + dropdown caret on a single line. The name
   ("First Last") contains a normal space, so without this the row can break
   between the name and the caret (or between first/last name), dropping the
   caret onto its own line under the name. PERF-14983. */
.lb-banner-user {
    white-space: nowrap;
}
/* Person icon is in a different container (no banner-btn-image-div wrapper)
   and renders ~5px higher than the action icons. Nudge to align. */
.lb-banner-user > .bi-person {
    position: relative;
    top: 1px;
}
/* Center the icon glyph in its box. Measured: the bi glyph's rendered box is
   taller than its container and, in a display:block div, rides the TOP (glyph
   center ~2.4px above the label center) instead of centering. Making the icon
   div a flex box distributes that overflow evenly so the glyph centers and
   lines up with the label. Covers both the labelled buttons and icons-only mode. */
.lb-banner .banner-btn-image-div {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.lb-banner .banner-btn-image-div:hover .banner-btn-icon,
.lb-banner .banner-btn-image-div:hover .banner-btn-icon-lg,
.lb-banner .banner-button:hover .banner-btn-icon,
.lb-banner .banner-button:focus .banner-btn-icon,
.lb-banner-user:hover > .bi {
    opacity: 0.85;
}

/* Link underline policy.
   WCAG 1.4.1 requires non-color cues for links *within blocks of text* (so
   colorblind users don't miss them). It does NOT require underlines on
   lists of explicit links — sidebars, widget link rows, nav items, etc.
   are already understood as links by position and context.

   Default for widget/card links: no underline at rest, underline on hover.
   Body text links inside paragraphs keep the always-on underline. */
.card a,
.card-body a,
.list-group-item a {
    text-decoration: none;
}
.card a:hover,
.card a:focus-visible,
.card-body a:hover,
.card-body a:focus-visible,
.list-group-item a:hover,
.list-group-item a:focus-visible {
    text-decoration: underline;
}

/* Re-enable always-underline for links inside body-text contexts (paragraphs,
   help text, descriptions). These ARE blocks of text so WCAG applies. */
.card-body p a,
.card-body .text-body a,
.card-body .lb-text a,
.text-body a {
    text-decoration: underline;
}

/* Restore readable rest-state text on .list-group-item-action.
   Bootstrap 5.3.4+ regressed --bs-list-group-action-color to a very muted
   secondary-color, so enabled actionable rows look indistinguishable from
   .disabled ones until you hover. Pin the rest color to the plain
   list-group-item color (body text) - hover/active/focus rules in BS5
   stay intact and still flip background/contrast on interaction.
   Upstream tracking: https://github.com/twbs/bootstrap/issues/41725 */
.list-group-item-action {
    --bs-list-group-action-color: var(--bs-list-group-color);
}

/* ================================
 * Remaining Compatibility Rules
 * These are still referenced in converted files, by JS, or from DB-stored content
 * ================================
 */

/* Badge default background — BS3 .badge had gray bg (#777) by default, BS5 has none.
   Restores visibility for bare .badge and custom badge classes (badge-circle-sm, scorecard-badge, etc.).
   Pulls from theme secondary so each theme's badges match its own palette. */
.badge:not([class*="bg-"]) {
    background-color: var(--bs-secondary, #6c757d);
}

/* Filter panel spacing */
.panel-filter {
    margin-bottom: 10px;
}
.panel-filter.filter-child,
.panel-filter.filter-child-last {
    margin-bottom: 0 !important;
    border-top: none;
}

/* btn-block: removed in BS5, but used in DB-stored HTML content (w2_html_grid_1, etc) */
/* Need !important to override BS5's .btn { display: inline-block } */
.btn-block {
    display: block !important;
    width: 100% !important;
}

/* btn-default: renamed to btn-secondary in BS5, but used in DB-stored HTML content.
   Pulls from theme --bs-secondary so the button matches each theme's palette.
   Hover/active states use color-mix shades of the same base. */
.btn-default {
    --bs-btn-color: #fff;
    --bs-btn-bg: var(--bs-secondary, #6c757d);
    --bs-btn-border-color: var(--bs-secondary, #6c757d);
    --bs-btn-hover-color: #fff;
    /* Hover shade — bigger delta so it's visibly different on dark theme secondaries.
       Was 85%/80% (~7% darker, barely perceptible on #555756). */
    --bs-btn-hover-bg: color-mix(in srgb, var(--bs-secondary, #6c757d) 70%, #000);
    --bs-btn-hover-border-color: color-mix(in srgb, var(--bs-secondary, #6c757d) 65%, #000);
    --bs-btn-focus-shadow-rgb: var(--bs-secondary-rgb, 130,138,145);
    --bs-btn-active-color: #fff;
    --bs-btn-active-bg: color-mix(in srgb, var(--bs-secondary, #6c757d) 60%, #000);
    --bs-btn-active-border-color: color-mix(in srgb, var(--bs-secondary, #6c757d) 55%, #000);
    --bs-btn-disabled-color: #fff;
    --bs-btn-disabled-bg: var(--bs-secondary, #6c757d);
    --bs-btn-disabled-border-color: var(--bs-secondary, #6c757d);
}

/* .hidden is used by JS for dynamic show/hide — not a BS3 responsive class */
.hidden {
    display: none !important;
}

/* Visible utilities — still referenced in 1 bricklet */
.visible-xs,
.visible-sm,
.visible-md,
.visible-lg {
    display: none !important;
}

@media (max-width: 575.98px) {
    .visible-xs {
        display: block !important;
    }
}

@media (min-width: 576px) and (max-width: 767.98px) {
    .visible-sm {
        display: block !important;
    }
}

@media (min-width: 768px) and (max-width: 991.98px) {
    .visible-md {
        display: block !important;
    }
}

@media (min-width: 992px) {
    .visible-lg {
        display: block !important;
    }
}

/* Well component — still referenced in 7 bricklets */
.well {
    min-height: 20px;
    padding: 19px;
    margin-bottom: 20px;
    background-color: var(--bs-tertiary-bg, #f8f9fa);
    border: 1px solid var(--bs-border-color, #dee2e6);
    border-radius: var(--bs-border-radius, 0.375rem);
}

.well-sm {
    padding: 9px;
    border-radius: var(--bs-border-radius-sm, 0.25rem);
}

.well-lg {
    padding: 24px;
    border-radius: var(--bs-border-radius-lg, 0.5rem);
}

/* ================================
 * Dashboard card border colors
 * Preserves BS3 panel look where the entire widget had colored borders
 * ================================
 */
/* PERF-14831: the old BS3 "default" panel style is normalized to "none" at
   the JSP read site for every bricklet, so .text-bg-default / .border-default
   should never be emitted anymore. The aliases that used to live here (mapping
   default to secondary) caused confusion because the .card-dshbd block below
   then overrode the alias on dashboard widgets, leaving a neutral hairline
   instead of the secondary color. Cleaner to make "default" a no-op upstream
   and have nothing claim those classes here. */


/* App-level card shadow token. Light-mode default; dark-mode themes should
   set this to `none` (or a subtle inner border) since shadows don't work
   on dark backgrounds. */
:root {
    --app-card-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 4px 10px rgba(0,0,0,0.06);
}

/* Frameset shell scroll model (PERF-15029). The shell document itself never
   scrolls: the body is a fixed-height flex column (banner/menu on top,
   #MFWrapper filling the rest), and the iframe document is the scroller.
   #MFWrapper keeps overflow:auto so it can take over if the iframe ever
   reports a height larger than its box. This replaces the JS height math
   (resizeMainFrame) the old shell needed, and structurally rules out the
   doubled-scrollbar states the overflow:visible/hidden history on this
   element kept trading between. dvh (with vh fallback) so the shell tracks
   a viewport whose height changes. Verified on desktop only. */
body.lb-frameset-shell {
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
}
body.lb-frameset-shell #MFWrapper {
    flex: 1 1 auto;
    min-height: 0;
    position: relative;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
}
body.lb-frameset-shell #mainFrame {
    display: block;
    width: 100%;
    height: 100%;
    border: 0;
}
/* Legacy-iOS fallback (< 15.4, no dvh): without dvh the shell falls back to
   100vh, so the bottom of it can end up behind Safari's toolbar. Reserve that
   strip by shortening the iframe box. Fixed 80px rather than
   env(safe-area-inset-bottom), which is 0 without viewport-fit=cover.
   -webkit-touch-callout gates this to iOS; dvh devices (and all desktops)
   never match. NOT verified on a device - written from spec. */
@supports (-webkit-touch-callout: none) and (not (height: 100dvh)) {
    body.lb-frameset-shell #MFWrapper {
        padding-bottom: 80px;
    }
}

/* Header strip above the menu (logo, user dropdown). Defaults to
   transparent so the underlying theme / page surface (--bs-body-bg)
   shows through instead of forcing an opaque white block over themes
   that colour the header via another surface. Set --tln-header-bg
   (theme :root or brand_overrides.jsp from BrandHeaderBg) to paint it.
   The companion --tln-header-fg drives the text/icon colour on top of
   that surface (defaults to black so an unbranded light header reads
   correctly; brand_overrides.jsp emits a contrast-derived value when
   BrandHeaderBg is set so a dark header gets light text automatically). */
.lb-banner,
#topFrame {
    background-color: var(--tln-header-bg, transparent);
}
.lb-banner {
    color: var(--tln-header-fg, var(--bs-body-color, #000));
}

/* Brand block in the .lb-banner cell (left side of the banner row).
   topFrame_include.jsp builds this composite when BrandingAdminEnabled
   is on so the brand can show:
     • logo image only (BrandLogoUrl set), or
     • app-name text only (no logo, BrandAppName set), or
     • either of the above with BrandTagline below.
   When neither logo nor name is set, the original LTBOOrganizationBanner
   HTML renders here instead and these classes are absent. */
.lb-brand-block {
    display: flex;
    flex-direction: column;
    justify-content: center;
    /* Pin children to their natural cross-axis (width) size so the
       logo image and tagline span don't stretch to fill the banner
       column — without this, an SVG logo with width:auto + max-height
       (set by brand_overrides.jsp) gets stretched full-cell-width,
       collapsing the visual stack with the tagline. flex-start keeps
       both items left-aligned to match the bp-logo-area placement in
       the admin's live preview. */
    align-items: flex-start;
    gap: 2px;
    line-height: 1.15;
    /* Fill the banner cell vertically so the brand block aligns with
       the action buttons on the right (which use align-items: center
       inside .lb-banner-row). BS5 .row stretches its cols to row
       height, so height: 100% picks up the tallest sibling's height. */
    height: 100%;
    /* Inherit the banner's text color (--tln-header-fg) so brand text
       stays legible against whatever header bg is in effect. */
    color: inherit;
}
.lb-brand-name {
    /* Block so the optional tagline always wraps below, never trails
       on the same baseline as the name. */
    display: block;
    font-size: 1.25rem;
    font-weight: 700;
    line-height: 1.1;
    color: inherit;
}
.lb-brand-tagline {
    /* Block so the tagline sits on its own line directly under the
       brand name or logo, matching the bp-tagline placement in the
       admin's live preview. */
    display: block;
    /* Smaller than the 1.25rem brand name so the tagline reads as a
       sub-label, not a competing wordmark, but large enough to stay
       legible whether it sits below or beside the logo. */
    font-size: 0.75rem;
    line-height: 1.2;
    /* Muted shade of the inherited header text colour — works equally
       well on a light or dark header without picking specific RGB. */
    opacity: 0.7;
    color: inherit;
}
/* Breathing room beneath the tagline when it stacks UNDER the logo / name
   (default "below" layout) so it isn't crammed against the menu strip. The
   banner grows to accommodate it (the menu sits a touch lower) — acceptable
   per design. Not applied in the side-by-side "right" layout. */
.lb-brand-block:not(.lb-brand-tagline-right) .lb-brand-tagline {
    padding-bottom: 6px;
}
/* When .lb-brand-block contains a logo image, the image already has its
   own height cap (set inline by brand_overrides.jsp from BrandLogoMaxHeight).
   The block just needs to align the optional tagline below it without
   adding extra vertical chrome. */
.lb-brand-block .lb-brand-logo {
    display: block;
}

/* Logo bottom gap so a logo sized to the row height isn't flush against the
   menu strip below. Covers both logo paths:
     - legacy org-banner logo (img.banner-img) - top-aligned, fills the row
     - Branding (Beta) brand logo (.lb-brand-logo) - can be sized large too
   Big logos are still allowed to grow the banner; they just keep this gap.
   Scoped to logo images, so text-only headers (no logo image) keep the square. */
.lb-banner .banner-img,
.lb-banner .lb-brand-logo {
    margin-bottom: 0.4rem;
}

/* PERF-14983: contain the logo to its column. Branding only caps the logo's
   HEIGHT (BrandLogoMaxHeight), so a wide source file (e.g. TLN 446x113)
   rendered at natural width and escaped its col-5 on narrow screens, sliding
   under the action pill - which then painted on top of it. fitBannerActions()
   measures the pill against its own column, so no pill tier can reclaim
   logo-invaded space; the logo must simply never leave its column. height:auto
   keeps the aspect ratio when the width clamp bites; at widths where the logo
   already fits, nothing changes. Covers both logo paths (legacy org-banner
   img.banner-img and Branding lb-brand-logo). */
.lb-banner-logo-col img {
    max-width: 100%;
    height: auto;
}

/* Optional "tagline to the right of the logo" layout — opt in via
   BrandTaglinePosition=right (topFrame_include.jsp adds this modifier).
   Lay the logo / name + tagline out as a centered row instead of the
   default stacked column. */
.lb-brand-block.lb-brand-tagline-right {
    flex-direction: row;
    align-items: center;
    /* Base .lb-brand-block sets justify-content: center for VERTICAL
       centering in its default column layout. In row layout that same
       property centers horizontally, floating the logo/name toward the
       middle — pin it to the start so the brand stays left-aligned. */
    justify-content: flex-start;
    gap: 10px;
}
/* Narrow banner: drop the tagline back under the logo (the default "below"
   layout) so it never crowds the logo / name on small screens. Uses the
   same md breakpoint as the rest of the banner/menu responsive rules. The
   tagline keeps the same font size in both layouts. */
@media (max-width: 767.98px) {
    .lb-brand-block.lb-brand-tagline-right {
        flex-direction: column;
        align-items: flex-start;
        gap: 2px;
    }
}

/* ============================================================
   Dashboard Filters Panel
   ============================================================
   Owned 100% by main5.css. BS3 rules were removed from main.css
   (only consumer is dashboard_filters.jsp which is BS5-only).
   Class names match the existing JSP/JS hooks (.filter-panel,
   .panel-filter, .filter-toggle, .filter-edit, .filter-view, etc.)
   so no markup-renaming was needed. */
.filter-panel {
    /* Fixed so the panel doesn't participate in the page's scroll width.
       Previously position:absolute against a parent that didn't clip caused
       the off-screen (translateX(100%)) state to extend document width
       and push the page left/right on narrower viewports. */
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: 280px;
    z-index: 100;
    background: rgba(0, 0, 0, 0.3);
    padding: 0.5rem;
    overflow-y: auto;
    transform: translateX(100%);
    opacity: 0;
    font-size: 0.875rem;
}

/* Outer "Filters" header - clickable strip to close the panel.
   The JSP currently lands a .btn-primary class on this header (BS3 era;
   the only thing it did was paint a primary bg). Scope BS5 primary chrome
   to the actual header so the visual lands regardless of the class soup. */
.filter-card-header {
    background-color: var(--bs-primary);
    color: #fff;
    padding: 0.5rem 0.75rem;
    border-radius: 0.25rem 0.25rem 0 0;
    font-weight: 600;
    user-select: none;
}
.filter-card-header .btn {
    /* Reset header buttons since they sit on a primary surface. */
    line-height: 1;
}

.apply-all-div {
    padding: 0.5rem 0;
    text-align: end;
    min-height: 2.25rem;
}

/* Top-level filter cards: direct children of #filterPanel. Each is a
   self-contained card with rounded corners and a small gap between siblings
   so an expanded body doesn't bleed into the next. Filter GROUPS (My Team
   etc) live at this level too - they wrap nested child filters but read as
   one card. Use > to NOT match nested children. */
.filter-panel > .panel-filter {
    margin-bottom: 0.4rem;
    border-radius: 0.375rem;
    background-color: #fff;
    overflow: hidden;
}
.filter-panel > .panel-filter:last-of-type {
    margin-bottom: 0;
}

/* Nested child filters inside a .filter-group (e.g. Organizations under
   My Team). These render inside the parent's body, so they shouldn't carry
   their own border/radius/margin chrome - they're items within the parent
   card, separated by a subtle divider only. */
.filter-panel .panel-filter .panel-filter {
    margin: 0;
    border: 0;
    border-radius: 0;
    border-top: 1px solid var(--bs-border-color);
    background-color: transparent;
}
/* The first nested child sits flush against the group's own card-body
   padding, so it doesn't need a top divider. */
.filter-panel .panel-filter .card-body > .panel-filter:first-child {
    border-top: 0;
}
/* Nested child headers: less weight than the group's outer header, but keep
   horizontal padding so the title and edit-pencil don't sit flush to the edge.
   Reset color/pencil-color too - parent painted them white for the primary
   band; nested headers sit on a white body so they need dark text again. */
.filter-panel .panel-filter .panel-filter > .card-header {
    background-color: transparent;
    color: var(--bs-body-color);
    padding: 0.3rem 0.6rem;
    font-size: 0.8125rem;
    border-bottom: 0;
}
.filter-panel .panel-filter .panel-filter .filter-toggle {
    color: var(--bs-secondary-color);
}
.filter-panel .panel-filter .panel-filter .filter-toggle:hover {
    color: var(--bs-primary);
}
/* Nested child bodies: match the header's horizontal padding so values
   indent under the title rather than touching the parent card's edge. */
.filter-panel .panel-filter .panel-filter > .card-body {
    padding: 0 0.6rem 0.3rem 0.6rem;
}

/* Per-filter card header - title + edit-toggle pencil. Primary-colored band
   matches the BS3 panel-primary > panel-heading look the filters had before
   modernization. BS5 doesn't cascade .border-primary to the header so we
   paint it explicitly. */
.filter-panel .panel-filter > .card-header {
    background-color: var(--bs-primary);
    color: #fff;
    padding: 0.4rem 0.6rem;
    font-size: 0.8125rem;
    font-weight: 600;
    border-bottom: 0;
}
/* Secondary-bordered cards (nested children that ARE top-level here, e.g.
   non-group filters using border-secondary) get a gray band instead. */
.filter-panel > .panel-filter.border-secondary > .card-header {
    background-color: var(--bs-secondary);
    color: #fff;
}
.filter-panel .filter-toggle {
    color: rgba(255, 255, 255, 0.85);
    font-size: 0.875rem;
    line-height: 1;
}
.filter-panel .filter-toggle:hover {
    color: #fff;
}

/* Card body holds either the compact .filter-view (selected bubbles) or
   the expanded .filter-edit (checklist + search). */
.filter-panel .card-body {
    padding: 0.4rem 0.6rem;
    font-size: 0.8125rem;
}

/* Scrollable lists: cap height so a 200-option filter doesn't blow out the panel. */
.filter-choices,
.filter-view {
    max-height: 14rem;
    overflow-y: auto;
    overflow-x: hidden;
}

/* Tighten the inner table used to render checkbox rows. The legacy markup
   uses a <table> per filter; rather than rewrite that to native list markup,
   just kill the table's default cell padding so rows are dense. */
.filter-panel .table-filter,
.filter-panel .filter-view .table {
    margin: 0;
    font-size: 0.8125rem;
}
.filter-panel .table-filter td,
.filter-panel .filter-view td {
    padding: 0.1rem 0.25rem;
    border: 0;
}

/* Form-check rows. BS5 ships .form-check with padding-left:1.5em + the
   input at margin-left:-1.5em - DO NOT zero padding-left or the checkbox
   disappears off-screen. Just tighten vertical rhythm. */
.filter-panel .checkbox-filter {
    margin: 0;
    min-height: auto;
}
.filter-panel .checkbox-filter .form-check-input {
    margin-top: 0.2rem;
}
.filter-panel .checkbox-filter .form-check-label {
    font-size: 0.8125rem;
    line-height: 1.3;
}

/* Search input + clear button: kept tight so the clear icon doesn't visually
   crash into the input. */
.filter-panel .search-filter-group,
.filter-panel .input-group-sm .search-filter {
    font-size: 0.8125rem;
}
.filter-panel .input-group-sm .input-group-text {
    background-color: var(--bs-tertiary-bg, #f8f9fa);
    padding: 0.25rem 0.5rem;
}

/* Selected-value badges shown in the compact .filter-view (default state). */
.filter-bubble {
    display: inline-block;
    max-width: 100%;
    margin: 0.1rem 0.2rem 0.1rem 0;
    font-weight: 500;
    font-size: 0.75rem;
    white-space: normal;
    overflow-wrap: anywhere;
}

/* Apply footer per filter. Right-aligned button, light divider above. */
.filter-panel .panel-footer-filter {
    padding: 0.4rem 0.6rem;
    background-color: var(--bs-tertiary-bg, #f8f9fa);
    border-top: 1px solid var(--bs-border-color);
}

/* Console widget cards — modernized 2026 styling.
   Softer border, slight shadow, larger radius. The colored "status" border
   (border-primary etc) becomes a top-only accent stripe instead of a full
   1px outline, which reads cleaner on a white page.
   Transparent card-cap-bg so the header zone reads as part of the card
   surface (BS5 defaults to a very faint gray which adds visual noise). */
/* Modern card chrome — applies to EVERY .card in the app.
   Stock BS5's .card-header is a light-grey strip with a bottom border. That
   reads "BS4-era control panel" rather than "2026 dashboard card." Soften
   the wrapper (rounded, shadowed, hairline border), make the header sit on
   the same surface as the body (transparent bg, no bottom border), and
   give cards a little more interior breathing room. */
.card {
    --bs-card-cap-bg: transparent;
    border-radius: 10px;
    border-color: rgba(0, 0, 0, 0.08);
    box-shadow: var(--app-card-shadow);
}
.card > .card-header {
    border-bottom: 0;
    --bs-card-cap-padding-y: 0.625rem;
    --bs-card-cap-padding-x: 1.25rem;
}
.card > .card-body {
    --bs-card-spacer-y: 1.25rem;
    --bs-card-spacer-x: 1.25rem;
}
/* The transparent card-cap-bg above is a HEADER decision; footers keep the
   stock BS5 faint shade (panel-footer heritage) so summary strips read as a
   distinct zone. Palette-agnostic: derives from the body color. */
.card > .card-footer {
    background-color: rgba(var(--bs-body-color-rgb), 0.03);
}

/* Dashboard widget specifics: inter-card gap, header hover affordance.
   The chrome itself now lives on the bare .card rule above. */
.card-dshbd {
    /* Modern dashboard inter-card gap. main.css ships 10px for BS3 compat;
       this BS5-scope rule lifts to 1.25rem (20px) to match the new card
       padding and align with current SaaS dashboard standards. */
    margin-bottom: 1.25rem;
    /* Borderless by default. The base .card rule (above) adds a soft hairline
       suitable for admin/editor pages; dashboard widgets read cleaner with
       the shadow alone, and only show a border when a status color is
       requested (the .border-primary/.border-success/etc rules below).
       Result: style=none, style=default, and any unrecognized style class
       all render the same chrome-less look. Use width-only (NOT shorthand
       `border:0`) so border-style stays solid - the colored-status rules
       below set border-top-width to 3px and an element with border-style:none
       won't paint that width at any size. */
    border-width: 0;
    /* Theme-set widget-card background. A theme may set --lb-card-bg (e.g.
       performance-dark's soft grey radial gradient) to paint all dashboard
       cards; every other theme falls back to the solid --bs-card-bg, so this
       is a no-op for them. */
    background: var(--lb-card-bg, var(--bs-card-bg));
}

/* Subtle hover affordance on widget headers - says "this responds when you
   reach for it" without competing with the accent stripe. */
.card-dshbd > .card-header {
    transition: background-color 120ms ease;
}
.card-dshbd > .card-header:hover {
    background-color: rgba(0, 0, 0, 0.04);
}

/* When the widget body is collapsed, the header alone IS the widget. Make
   the entire header click-to-expand (open state keeps its existing controls
   intact). :has() lets us scope this purely to collapsed cards. */
.card-dshbd:has(> .card-body.collapse:not(.show)) > .card-header {
    cursor: pointer;
}

/* Top-edge status accent. Native border-top approach — color follows the
   rounded-corner curve naturally. The colored top is thicker (3px) for
   emphasis; sides/bottom inherit the soft hairline from .card-dshbd above. */
.card-dshbd.border-primary,
.card-dshbd.border-secondary,
.card-dshbd.border-success,
.card-dshbd.border-danger,
.card-dshbd.border-warning,
.card-dshbd.border-info {
    border-color: rgba(0, 0, 0, 0.08) !important;
    border-top-width: 3px !important;
}
/* Status accent uses the full theme color. */
.card-dshbd.border-primary   { border-top-color: var(--bs-primary)   !important; }
.card-dshbd.border-secondary { border-top-color: var(--bs-secondary) !important; }
.card-dshbd.border-success   { border-top-color: var(--bs-success)   !important; }
.card-dshbd.border-danger    { border-top-color: var(--bs-danger)    !important; }
.card-dshbd.border-warning   { border-top-color: var(--bs-warning)   !important; }
.card-dshbd.border-info      { border-top-color: var(--bs-info)      !important; }

/* Panel Style "No Style" (border-nostyle): the explicit opt-out that must stay
   chrome-less even when the branding editor's Card Top color is set. It already
   escapes the BrandCardTop accent in brand_overrides.jsp (it is not .border-0,
   and its class name contains "border-" so the :not([class*="border-"]) rule
   skips it), but assert borderless here so the contract survives any future
   change to that selector. Distinct from style=none / None Selected, which
   intentionally DO pick up the Card Top accent. */
.card-dshbd.border-nostyle { border-width: 0 !important; }

/* Console widgets must never push past their grid cell boundary. If a widget's
   content (DataTable with long unbreakable tokens, wide images, embedded
   iframes, etc.) is wider than the card body, scroll horizontally inside the
   widget rather than letting it bleed into adjacent widgets. Scoped to widget
   bodies (.card-body-dshbd) so admin/editor page card bodies are unaffected.

   overflow-y is clip (computes to hidden because overflow-x is auto): widget
   bodies must NEVER show a vertical scrollbar. Auto-height widgets grow with
   their content (the absolute grid reflows around them), so the only thing
   clip ever trims is a sub-pixel flex artifact (e.g. a wrapped button sitting
   ~3px taller than its row) that overflow-y:auto would otherwise turn into a
   bogus full scrollbar. Horizontal scroll is preserved via overflow-x:auto. */
.card-body-dshbd {
    overflow-x: auto;
    overflow-y: clip;
}
/* Exception: widgets that pin an explicit body height (w2_facebook_1 height:Npx;
   w2_chart_title_1 / w2_chart_subtitle_1 / w2_title_1 heightStyle) keep a real
   vertical scrollbar, so content taller than that fixed height stays reachable
   instead of being clipped/hidden. */
.card-body-dshbd[style*="height:"] {
    overflow-y: auto;
}

/* Catalog accordion sections (w2_certificationsbycatalog / w2_coursesbycatalog).
   Each catalog is a flat section inside the single widget card - NOT a nested
   .card. Nesting a bordered+shadowed card inside the widget card produced
   doubled edges (inner card border + trailing margin against the widget edge).
   A plain header bar + collapsible body, divided by a hairline, reads cleanly
   and matches other list widgets. */
.lb-catalog-section + .lb-catalog-section {
    border-top: 1px solid var(--bs-border-color);
}
/* Optional full-section colour. A theme sets --lb-catalog-bg / --lb-catalog-fg to
   paint the WHOLE catalog section (heading + body + content text), restoring the
   pre-BS5 solid-colour catalog look (e.g. the performance theme's navy that EVR
   had). Both default to transparent/inherit, so every other theme keeps the
   modern light section untouched. */
.lb-catalog-section {
    background-color: var(--lb-catalog-bg, transparent);
    color: var(--lb-catalog-fg, inherit);
}
/* Cert/course name + status links inside a coloured section follow
   --lb-catalog-fg; unset falls back to the normal link colour so uncoloured
   sections are unchanged. */
.lb-catalog-section a {
    color: var(--lb-catalog-fg, var(--bs-link-color));
}
.lb-catalog-heading {
    padding: 0.5rem 0.75rem;
    font-weight: 600;
    /* Catalog/category heading was inheriting 1rem - identical to the course
       rows below it. Bump so it reads as a section heading (BS3 parity, where
       the catalog title was larger than its rows). Scoped to .lb-catalog-heading
       so only these two catalog widgets are affected. */
    font-size: 1.125rem;   /* 18px */
}
.lb-catalog-heading .icon-catalog {
    color: var(--lb-catalog-fg, var(--bs-secondary-color));
    font-size: 0.875rem;
}
.lb-catalog-body {
    padding: 0.5rem 0.75rem;
}
/* Table inside a catalog body fills it without its default bottom margin
   (that margin + the widget edge was part of the doubled-border look). */
.lb-catalog-body .table {
    margin-bottom: 0;
}
/* A long unbroken course/cert name (e.g. underscore-joined, no spaces) has no
   wrap opportunity and would push the table past the widget edge, giving the
   whole widget a horizontal scrollbar. Let such tokens break so the table fits. */
.lb-catalog-body .table td {
    overflow-wrap: anywhere;
}
/* Open catalog header reads like a BS5 .accordion-button:not(.collapsed): a
   primary tint + inset divider so the expanded catalog is clearly distinct
   (PERF-14744). The open state differs per widget - the certs widget toggles
   .d-none on its body, the courses widget uses BS5 .collapse/.show - so match
   both via :has(). Tint is theme-aware (primary at 10%), not a fixed hex. */
.lb-catalog-section:has(> .catalog-group:not(.d-none)) > .lb-catalog-heading,
.lb-catalog-section:has(> .lb-catalog-body.show) > .lb-catalog-heading {
    /* Coloured sections keep the solid --lb-catalog-bg when open (no tint, exact
       parity); uncoloured themes get the modern primary tint. */
    background-color: var(--lb-catalog-bg, rgba(var(--bs-primary-rgb), 0.1));
    /* The divider between an open heading and its body matches --bs-border-color
       for uncoloured themes, but on a coloured section that grey hairline cuts the
       block in two; bind it to --lb-catalog-bg so it disappears into the colour and
       the open section reads as one solid block (PERF-14927). */
    box-shadow: inset 0 -1px 0 var(--lb-catalog-bg, var(--bs-border-color));
}
/* Hover affordance on the clickable catalog headers (open header keeps its tint -
   the :has rule above is more specific). */
.lb-catalog-heading:hover {
    background-color: var(--lb-catalog-bg, var(--bs-tertiary-bg));
}
/* Table inside a coloured catalog body: BS5 paints the CELLS (not the table
   element) with --bs-table-bg #fff, which would put the section's white text on
   white cells. Bind the cell fill + row borders to --lb-catalog-bg so they
   match the section colour (navy) and the white text is readable; uncoloured
   themes keep the normal white cells / hairline rows via the fallback. */
.lb-catalog-body .table {
    --bs-table-bg: var(--lb-catalog-bg, #ffffff);
    /* Non-link cell text (the "Not Started"/"Expired" status) is painted by BS5's
       --bs-table-color (dark), which overrides the section's inherited white. Bind
       it to --lb-catalog-fg so status text is white on the colour too; the course
       /cert name links already follow --lb-catalog-fg via the .lb-catalog-section a
       rule. Uncoloured themes keep the normal body text colour. */
    --bs-table-color: var(--lb-catalog-fg, var(--bs-body-color));
    --bs-table-border-color: var(--lb-catalog-bg, var(--bs-border-color));
}
/* Round the catalog section block to the card radius so a coloured section
   (e.g. the performance navy) reads as a rounded block instead of squaring off
   the card corners. overflow clips the inner heading/body fill to the radius. */
.lb-catalog-section {
    overflow: hidden;
}
.lb-catalog-section:first-child {
    border-top-left-radius: var(--bs-border-radius, 0.625rem);
    border-top-right-radius: var(--bs-border-radius, 0.625rem);
}
.lb-catalog-section:last-child {
    border-bottom-left-radius: var(--bs-border-radius, 0.625rem);
    border-bottom-right-radius: var(--bs-border-radius, 0.625rem);
}

/* Stacked cards: add vertical rhythm like BS3 panels had by default.
   Scoped to cards that are NOT inside a .row (grids use gutters for spacing). */
:not(.row) > .card:not(:last-child) {
    margin-bottom: 1rem;
}

/* btn-xs: BS3 button size, removed in BS5. Used by action buttons globally via snippets.properties.
   NOTE: bootstrap5-toggle.min.css redefines .btn-xs with direct padding/font-size/line-height
   (and a broken line-height: 0.5), so we have to override with concrete properties — the
   --bs-btn-* CSS vars alone get bypassed. */
.btn-xs {
    --bs-btn-padding-y: 0.125rem;
    --bs-btn-padding-x: 0.5rem;
    --bs-btn-font-size: 0.75rem;
    --bs-btn-border-radius: 0.2rem;
    padding: 0.125rem 0.5rem;
    font-size: 0.75rem;
    line-height: 1.5;
    border-radius: 0.2rem;
}
/* Action button main label: prevent text-wrap and let button grow with the
   wrapper's min-width so labels like "Configure" aren't cut off in BS5. */
.lb-action-btn {
    white-space: nowrap;
    flex: 1 1 auto;
}
/* Restore the BS3 visual divider between the main action button and its dropdown caret.
   In BS5 btn-group, adjacent same-color buttons blend together; this adds a subtle
   separator line that adapts to any theme color. */
.lb-action-btn-group .lb-action-btn-dropdown {
    border-left: 1px solid rgba(0, 0, 0, 0.2);
}

/* BS3 <span class="caret"> inside dropdown buttons produces nothing in BS5.
   Hide it so BS5's own ::after caret (rendered by .dropdown-toggle) can show. */
.dropdown-toggle .caret {
    display: none;
}

/* btn-outline-secondary readability: BS5 ships it with text color = --bs-secondary
   (#6c757d, medium gray), which reads as faded against a white card. BS3's btn-default
   used near-black text (~#333) — much more visible. Override the resting-state text
   color to body color while keeping the secondary border, and let hover invert to
   filled-secondary for the visual cue. */
.btn-outline-secondary {
    --bs-btn-color: var(--bs-body-color, #212529);
}

/* Horizontal-form labels: BS5's .col-form-label is left-aligned and not bold by default,
   but admin/editor JSPs originally used BS3's .control-label which was right-aligned and
   bold. Keep that look for visual continuity in the admin UI. Only applies inside .row
   so vertical-form labels (.form-label) get bold but not the right-align.
   Bold applies at every breakpoint. Right-align only at md+ to match the canonical
   editor column convention (col-md-*, the by-value migration of BS3's 768px col-sm):
   below md those forms stack the label above the field, where a right-aligned label
   reads as broken (PERF-15003). Editors still on col-sm-* columns simply get
   left-aligned labels in the 576-767px band while side by side - cosmetic only. */
.row > .col-form-label {
    font-weight: 600;
}
@media (min-width: 768px) {
    .row > .col-form-label {
        text-align: right;
    }
}

/* Vertical/inline form labels: BS5's .form-label is not bold by default, but BS3's
   default <label> styling was. Restore the bold to preserve admin/editor look-and-feel
   and visual labels used as styling hooks (e.g. <span class="form-label">). */
.form-label {
    font-weight: 600;
}

/* Restore the BS3 .row negative-margin gutter cancel where a BS3 .row was dropped
   during migration but the parent .container-fluid's horizontal padding still needs
   to be cancelled out (e.g., custom grid layouts that can't use .row's flex-wrap).
   PERF-14668: console.jsp's #consoleBody used class="row" in BS3 to negate the
   container's 15px padding; in BS5 the class was removed (BS5 .row's flex-wrap
   conflicts with the Masonry grid), leaving an uncompensated 12px on each side. */
.lb-row-cancel {
    margin-left: calc(-0.5 * var(--bs-gutter-x, 1.5rem));
    margin-right: calc(-0.5 * var(--bs-gutter-x, 1.5rem));
}

/* Filter / picker <select>s in widget headers (and similar tight UI spots)
   shouldn't grow indefinitely with the selected option's text. Native
   <select> truncates the closed-state display with `...` when it can't fit;
   the open dropdown still renders the full option name. Apply where a
   <select> sits inline with other header controls and long option text
   would otherwise push the row out of shape. */
.lb-select-truncate {
    max-width: 400px;
}

/* Companion to .lb-select-truncate for the opposite failure: BS5's
   .form-select is width:100%, so inside a table cell a content-heavy
   sibling column can squeeze the select to an unusable sliver (BS3 selects
   had a natural width the cell could not shrink below). Apply where a
   select lives in a shared-width layout (table cell, flex row) and must
   stay readable. */
.lb-select-min {
    min-width: 180px;
}

/* Same guarantee for text inputs that display looked-up names (picker
   input-groups in inline filter bars): the input keeps a readable floor
   and the flex unit sizes to content instead of to a grid tier that can
   squeeze it to a sliver (PERF-15102). Pair with .input-group.w-auto.
   The .input-group > variant matches Bootstrap's own
   .input-group > .form-control { min-width: 0 } specificity so the floor
   survives inside groups. */
.lb-input-min,
.input-group > .lb-input-min {
    min-width: 240px;
}

/* Icon-only button scaled down to badge/tag height so it can sit inline
   with .badge tag rows without overlapping the following wrapped line.
   Combine with .btn-sm. */
.lb-btn-tag {
    padding-top: 0;
    padding-bottom: 0;
    line-height: 1.5;
}

/* Standard Flatpickr date-input sizing. Every Flatpickr date picker in the
   app uses the .input-group.date convention (with data-input on the text
   field and data-toggle on the calendar button — see appbase rule). Width
   fits long date formats ("Mon May 11 2026") + the calendar icon button,
   with enough headroom for an optional Start/End label addon. max-width:100%
   lets it shrink in narrow containers. No opt-in class needed; the
   convention IS the selector. */
.input-group.date {
    /* Fixed width so every Flatpickr date field is identical regardless of its
       container. max-width:100% was letting them shrink below the date format
       width in narrow columns (squashing to "mm," truncation) - on narrow
       viewports the row should wrap, not crush the input. */
    width: 230px;
    min-width: 230px;
    flex: 0 0 auto;
}

/* Date-range filter: a Start + End .input-group.date pair that adapts to the
   width of its widget/container (NOT the viewport - a widget can be narrow in a
   multi-column console on a wide screen). Pickers sit side by side when there's
   room, wrap to stacked when there isn't, grow to fill a narrow column (full-
   width feel + tap target), and are capped so a wide widget doesn't produce
   oversized date fields. Reusable across any Start/End date filter. */
.lb-date-range {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}
.lb-date-range > .input-group.date {
    /* Release the global fixed 230px so the picker can grow/fill; min-width:230px
       (inherited from the rule above) still floors it so the date never truncates. */
    width: auto;
    flex: 1 1 230px;
    max-width: 400px;
}

/* Video thumbnail tiles: BS3 sized .thumb-container at 257px, which only allowed 255px
   inside the card's 1px borders — making the 256px thumbnail cover the right border.
   Bump to 258px in BS5 so the thumb fits with both borders visible. */
.thumb-container {
    width: 258px;
}
/* Video thumbnail tiles: thumb-container is 257px wide (sized for BS3 panel-body's tight
   padding). BS5 card-body adds 16px horizontal padding which makes the 256px thumbnail
   overflow on the right. Zero-out horizontal card-body padding so the thumb sits flush
   with the card border, then re-pad the rows beneath the thumb so titles/ratings still
   have breathing room. */
.thumb-container > .card-body {
    padding-left: 0;
    padding-right: 0;
}
.thumb-container > .card-body > .row {
    padding-left: 10px;
    padding-right: 10px;
    margin-left: 0;
    margin-right: 0;
}
.thumb-container > .card-body > .row:first-child {
    padding-left: 0;
    padding-right: 0;
    --bs-gutter-x: 0;
}
.thumb-container > .card-body > .row:first-child > .col {
    padding-left: 0;
    padding-right: 0;
}
/* Round the thumbnail's top corners to match the card's border radius so the
   square image corners don't overlap the rounded card corners. */
.thumb-container .lb-thumb-img {
    border-top-left-radius: var(--bs-card-inner-border-radius, 0.375rem);
    border-top-right-radius: var(--bs-card-inner-border-radius, 0.375rem);
    display: block;
}
.thumb-container > .card-body > .row:first-child .video-title {
    padding-left: 10px;
    padding-right: 10px;
}
/* The comment widget carries margins and a right alignment for its standalone use, and
   the tile is not that context: the card body's gap owns the spacing and the line is
   centred. The empty .LBCommentArea is the one that mattered - its 10px bottom margin
   collapses out through the nowrap wrapper and made the comment block ~10px taller than
   the rating, which is what pushed the text off the line. The tile opens comments in a
   modal, so that inline area is never populated here. */
.thumb-container .LBCommentArea,
.thumb-container .LBCommentLink-top-bubble,
.thumb-container .LBCommentLink-right-bubble {
    margin: 0;
    text-align: inherit;
}
/* Video tiles: main.css sizes .thumb-container/.lb-thumb-img at fixed 256px.
   DataTables 2 lays the video list out as a 100%-width table with percentage
   columns, so once a cell drops under 257px the fixed-size tiles paint over
   their neighbors (PERF-15100). Let them shrink with the cell; height:auto
   keeps the 16:9 ratio. */
.thumb-container,
.lb-thumb-img,
.lb-thumb-img-lg {
    max-width: 100%;
}
.lb-thumb-img,
.lb-thumb-img-lg {
    height: auto;
}

/* Modal depth: BS3 gave .modal-content a strong drop shadow (0 5px 15px at 50%),
   BS5.3 ships a barely-visible sm shadow. With modals that open on top of other
   modals (video editor -> sharing/category/catalog pickers) the top dialog
   blended into the one beneath it (PERF-15265). Restore the BS3 depth. */
.modal-content {
    --bs-modal-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
    box-shadow: var(--bs-modal-box-shadow);
}

/* Modal header: BS3 placed the close button before the title; BS5 expects it after.
   Force the .btn-close to the right regardless of source order, for converted modals. */
.modal-header {
    justify-content: space-between;
}
.modal-header .btn-close {
    order: 1;
    margin-left: auto;
}

/* ================================
 * DataTables Bootstrap 5 adjustments
 * ================================
 */

.tbl-export-controls {
    margin-bottom: 1rem;
}

/* Responsive puts its expand arrow on the first cell as an inline-block ::before.
   When that cell also holds a select-row checkbox the two are laid out inline, but
   the column keeps its narrow natural width (measured 33.6px against ~17.6px of
   arrow plus a 13px box), so with the cell's default white-space:normal they wrap
   and the checkbox drops onto a second line - the row doubles in height and the
   box sits under the arrow instead of beside it. The header cell escapes this
   because it carries .lb-cell-min (width:1px;white-space:nowrap) from main.css;
   the body cells come from the dataset config and do not. nowrap here lets the
   cell take the width it needs and keeps the pair on one line. */
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control,
table.dataTable.dtr-inline.collapsed > tbody > tr > th.dtr-control {
    white-space: nowrap;
}
/* The expand arrow (an inline-block ::before) and a checkbox sharing the control
   cell both default to vertical-align baseline, which leaves their centres a
   couple of px apart. Centre both on the line box (PERF-15250). */
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control::before {
    vertical-align: middle;
}
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control input[type="checkbox"] {
    vertical-align: middle;
}
/* When the control cell is a numeric column, DT2 right-aligns its content and the
   inline expand arrow travels with each number's width, so the arrows sit at a
   different indent on every row (points history). Pin the arrow to the cell's
   left edge; the numbers keep their numeric alignment. */
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control.dt-type-numeric::before {
    float: left;
}

/* While a table is initializing (class="hidden" until ui.jsp initComplete)
   or actively loading (dt-loading, the PERF-14965 processing.dt window that
   already hides the not-yet-collapsed headers), the DT2 controls render and
   accept input - typing in the search box or changing page length then
   triggers draws against a hidden/empty table and freezes degenerate column
   widths (PERF-15077: Name column crushed to minimum until a revisit). Hide
   the controls through BOTH phases, same trigger as the header hiding -
   state-driven, no JS timing. visibility (not display) keeps the layout
   from jumping on reveal. dt-loading only fires on the initial fetch and
   explicit reloads (no serverSide anywhere), so settled tables never lose
   their controls. */
/* (PERF-15077 round 2: scrollY/grid tables clone the table for scroll
   header/footer with a stale copy of the hidden class - ui.jsp's reveal
   now strips hidden from the clones too, so no grid exclusion is needed
   here.) */
.dt-container:has(table.dataTable.hidden) .dt-search,
.dt-container:has(table.dataTable.hidden) .dt-length,
.dt-container:has(table.dataTable.hidden) .dt-paging,
.dt-container:has(table.dataTable.hidden) .dt-info,
.dt-container:has(table.dataTable.dt-loading) .dt-search,
.dt-container:has(table.dataTable.dt-loading) .dt-length,
.dt-container:has(table.dataTable.dt-loading) .dt-paging,
.dt-container:has(table.dataTable.dt-loading) .dt-info {
    visibility: hidden;
}

/* Truncating table cell for name columns that can hold long unbreakable
   tokens (underscored template/audience names). Without this the token
   forces the cell wider than its card (horizontal bleed) and shoves the
   Responsive expand arrow onto its own line, doubling the row height
   (PERF-15037/PERF-15054). max-width:0 makes the cell truncate to whatever
   width the table allocates it - fluid, and no table-layout:fixed (which
   broke the responsive details column, PERF-11400). Pair with a title
   attribute on the cell so the full name is available on hover. */
table.dataTable td.lb-cell-truncate {
    max-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Expanded DataTables Responsive detail (custom formatDetails markup in ui.jsp,
   shared BS3/BS5). Indent the block under the row and give rows BS3-era
   breathing room. */
.details-table {
    margin: 0.25rem 0 0.25rem 1.5rem;
}

.details-row td {
    padding: 0.2rem 0.75rem 0.2rem 0;
    vertical-align: top;
}

/* Label column hugs its text (no forced 30% width) so the value sits right
   next to the label, like BS3. Bold the actual text - it lives in a nested
   <label class="small">, so font-weight on the td alone never reached it. */
.details-label {
    width: 1%;
    white-space: nowrap;
}
.details-label label {
    font-weight: 600;
    color: var(--bs-body-color);
    /* block, not inline-block: on a text line the label sits on the line
       box's baseline and rides ~2px lower than the block-level content in
       the sibling data cell; as a block it starts at the padding edge, so
       label and content align top-to-top exactly */
    display: block;
}

.details-data {
    color: var(--bs-body-color);
    /* One unbreakable value (an AICC url, a token, a long filename) used to
       drag the whole table off the page once a row was expanded: formatDetails
       renders the detail as a nested table, a table cell's min-content width
       is its widest unbreakable run, and the outer table has to honour that
       through the child row's colspan cell - RCC list measured 582px of table
       inside a 378px card (PERF-15044). anywhere lets the break land
       mid-string AND counts in intrinsic sizing, so the cell's min-content
       collapses to one character and the table stays inside its container
       with the value wrapped over as many lines as it needs. */
    overflow-wrap: anywhere;
}

/* BS-agnostic label/value list used inside DataTables Responsive detail rows
   (e.g. w2_course_transcript_1 Details + History cells). Native <dl><dt><dd>
   markup, styled as a 2-column grid: term left, value right. No Bootstrap
   .row/.col dependency, so renders identically on BS3 and BS5 pages. */
.lb-detail-dl {
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: 0.5rem;
    row-gap: 0.25rem;
    margin: 0;
    padding: 0;
}
/* Inline variant: label/value pairs laid out on uniform grid tracks
   (PERF-15009 rework). The original flex-wrap flow sized each pair to its
   content, so nothing lined up vertically across History sessions. Grid
   tracks are container-driven (auto-fill against the actual cell width), so
   the same component gets ~4 columns in a wide transcript cell, 1-2 in the
   narrow widget cell, and every session in a cell shares identical tracks -
   columns align by construction, like the BS3 col-2 layout did, without its
   fixed-percentage crush. Each dt+dd pair is wrapped in a <div> (valid
   inside <dl>) so a pair never splits across cells. */
.lb-detail-dl-inline {
    display: grid;
    /* 200px min track: wide enough that a bold date label + short date value
       ("Date Complete: 05/22/2024" ~190px) stays on one line inside a track */
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 0.25rem 1.25rem;
    margin: 0;
    padding: 0;
    /* The component owns its type size, in rem so it renders 14px no matter
       what context it lands in. The markup previously carried .small, which
       compounded with the em-based contexts it renders inside (0.95em
       text-table-sm cells; the span.small wrapper formatDetails() adds in
       Responsive child rows) and left the pairs at ~12.25px riding high
       against the 14px details-label. */
    font-size: 0.875rem;
}
.lb-detail-dl-inline > div {
    display: flex;
    /* a pair may wrap internally (value drops under its label) and shrink
       below its content minimum - otherwise a nowrap label + nowrap date
       value gives the pair a hard min width, the grid container inherits the
       widest pair as ITS minimum, and the whole block reaches out of a
       narrow cell forcing a horizontal scrollbar */
    flex-wrap: wrap;
    min-width: 0;
    column-gap: 0.35rem;
}
.lb-detail-dl > dt,
.lb-detail-dl-inline dt {
    font-weight: 700;
    color: var(--bs-body-color, #212529);
    margin: 0;
    /* labels never break mid-text ("Last Accessed:" on one line); the value
       beside them wraps instead when a grid track gets tight */
    white-space: nowrap;
}
.lb-detail-dl > dd,
.lb-detail-dl-inline dd {
    margin: 0;
}
/* Visual separator between successive History sessions. */
.lb-detail-dl-sep {
    border-top: 1px solid var(--bs-border-color, #dee2e6);
    padding-top: 0.5rem;
    margin-top: 0.5rem;
}

/* ================================
 * Form Styling
 * ================================
 */

.form-control {
    font-size: 1rem;
}

/* ================================
 * WCAG Accessibility Enhancements
 * ================================
 */

.skip-link {
    position: absolute;
    top: -40px;
    left: 0;
    background: var(--bs-primary);
    color: white;
    padding: 8px;
    z-index: 100;
}

.skip-link:focus {
    top: 0;
}

a:not(.btn) {
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
}

a:not(.btn):hover {
    text-decoration-thickness: 2px;
}

/* ================================
 * Page Header Styling
 * ================================
 */

.page-header {
    padding-bottom: 0.5rem;
    margin: 0.75rem 0 1.25rem;
    border-bottom: 1px solid var(--bs-border-color);
}

/* .section-start / .section-end are deliberately NOT defined here.
   They are legacy main.css classes, retired from BS5 markup under PERF-15127 (all 60
   BS5 JSPs swept to mt-3 / mb-3). main.css is loaded on BS5 ahead of this file, so the
   only remaining users - BS3-era ajax fragments that render into BS5 hosts
   (development_goals_ajax, class_calendar_detail_ajax, team_survey_status_ajax) - get
   the main.css values, the same ones they get on their BS3 hosts. Do not re-add them
   here: a copy would be either a pure duplicate or a silent override. Use mt-3 / mb-3. */

/* ================================
 * Pagination Top
 * ================================
 */

.pagination-top {
    display: flex;
    justify-content: flex-end;
}

/* ================================
 * Development Plan goal info bar
 * ================================ */


/* ================================
 * Login pages (#loginBody)
 * Ported from BS3 tln-light theme which had these styles; the BS5 theme
 * regeneration dropped them. Written theme-agnostically — pulls all colors
 * from BS5 CSS variables so each theme's login page picks up its own palette.
 * See .cursor/rules/themes.mdc for the no-hardcoded-colors rule.
 * ================================ */

#loginBody {
    background-color: var(--bs-tertiary-bg, #f8f9fa);
}

#loginBody .login-panel {
    max-width: 900px;
    margin: 0 auto;
    border: 1px solid var(--bs-border-color, #dee2e6);
    overflow: hidden;
}

/* BS5 dropped Glyphicons, so the BS3 bullet markers in main.css
   (.lb-triangle-bullet / .lb-chevron-bullet  li:before  use the 'Glyphicons
   Halflings' font at codepoints \e250 / \e080) render as tofu boxes on BS5 pages
   - e.g. campusSelection.jsp's campus list (PERF-14887). main5.css loads after
   main.css, so re-point them at plain Unicode glyphs in the inherited font (no
   icon font needed); the BS3 spacing (position/margin/top) still cascades. */
.lb-triangle-bullet li:before {
    content: "\25B8";          /* black right-pointing small triangle */
    font-family: inherit;
}
.lb-chevron-bullet li:before {
    content: "\203A";          /* single right-pointing angle quote (chevron) */
    font-family: inherit;
}

/* Alert icon alignment (PERF-14881). alert_include.jsp's alert is display:flex
   with align-items-start so the icon anchors to the FIRST line of a multi-line
   message. Bootstrap Icons rely on vertical-align to sit on the text baseline,
   which flex ignores, so the glyph floats off the line. A fixed translate nudge
   can't satisfy both cases - it centered the multi-line alert but pushed the
   single-line icon too low. Instead make the icon a one-line-tall (1lh) flex box
   that centers its own glyph: align-self keeps that box on the first text line,
   align-items centers the glyph on it. Correct for single-line AND the first
   line of a list, with no magic offset. Scoped to the flex alert's direct-child
   icon so inline icons and non-flex alerts are untouched. */
.alert.d-flex > .bi {
    display: inline-flex;
    align-items: center;
    align-self: flex-start;
    height: 1lh;
}

/* Login heading (logo strip on the card). Defaults to transparent so the
   logo sits on the card's surface (white), matching pre-BS5 behaviour where
   .login-heading had no background. Opt in to a coloured/dark heading by
   setting --login-heading-bg — at a theme :root, the campus branding main.css
   (:root { --login-heading-bg: #212529 }), or brand_overrides.jsp. Driving it
   through a variable lets lower-specificity sources override this #id .class
   rule without a specificity fight. */
#loginBody .login-heading {
    background-color: var(--login-heading-bg, transparent);
    border-bottom: 0;
    padding: 1.5rem 2.5rem;
}

#loginBody .login-logo {
    /* Match the pre-BS5 (prod) behaviour: render the campus banner logo at its
       natural size, bounded only by the card width and the product's own logo
       ceiling. The old `height: 48px; max-width: 240px` shrank real logos
       (which run ~72-121px tall and up to ~746px wide) and overrode the inline
       BrandLoginLogoMaxHeight. max-height 120px = the [16,120] cap the branding
       editor already enforces and covers the tallest real logo (121px). */
    max-width: 100%;
    max-height: 120px;
    height: auto;
    width: auto;
}

/* App-name fallback shown in the login heading when no brand logo is set
   (mirrors the portal header's .lb-brand-name text). Sized up from the
   header default so it reads with similar weight to the 48px logo slot. */
#loginBody .login-brand-name {
    font-size: 1.75rem;
    color: var(--login-heading-fg, inherit);
}

/* Branded registration banner (reg_banner.jsp with brandLogo=true, e.g. the
   forgot-password page). Shows the brand logo or app-name text in place of the
   campus banner image, following the same rules as the login page. */
.lb-reg-banner-brand {
    padding: 1.25rem 0;
}
.lb-reg-banner-brand .lb-reg-brand-logo {
    height: 48px;
    max-width: 240px;
    width: auto;
}
.lb-reg-banner-brand .lb-brand-name {
    font-size: 1.75rem;
}

#loginBody .login-panel .card-body {
    padding: 2.5rem 3rem 1.5rem;
}

#loginBody .login-elements {
    max-width: 420px;
    margin: 0 auto;
}

/* Force labels to stack above inputs (vertical form), matching BS3 tln layout.
   Overrides the horizontal form rows the bulk sed produced. */
#loginBody .login-elements .form-label,
#loginBody .login-elements label.col-form-label {
    font-size: 1rem;
    font-weight: 700;
    color: var(--bs-body-color, #212529);
    text-align: left;
    padding-top: 0;
    margin-bottom: 0.35rem;
    width: 100%;
    max-width: 100%;
    flex: 0 0 100%;
    padding-left: 0;
    padding-right: 0;
}

#loginBody .login-elements .row > [class*="col-"] {
    flex: 0 0 100%;
    max-width: 100%;
    padding-left: 0;
    padding-right: 0;
}

#loginBody .login-elements .row.mb-3 {
    margin-bottom: 1rem;
}

/* Keep BS5 native border-radius, border-color, and focus box-shadow on
   inputs (they already default to the theme's --bs-border-color / focus
   ring) — a prior override here forced border-radius:0/box-shadow:none,
   which had no visual benefit and, worse, outranked .form-control:focus
   by specificity, leaving login fields with NO visible focus indicator
   (WCAG 2.4.7 regression). Only the addon background is a real deviation
   from BS5 default (tertiary-bg gray) worth keeping. */
#loginBody .login-elements .input-group-text {
    background-color: var(--bs-body-bg, #fff);
}

/* Sizing only — cmdLogin already carries .btn.btn-primary, which supplies
   its own theme-driven color/hover/active/focus states natively per BS5
   theme. Re-declaring those colors here just duplicated .btn-primary and
   would drift if a theme ever customizes it beyond --bs-primary. */
#loginBody .login-elements #cmdLogin {
    padding: 0.5rem 1.5rem;
    font-size: 0.875rem;
}

/* Only real deviation from native BS5 links is no underline. Color,
   font-family, and font-size were being redeclared here to values that
   just duplicated inherited defaults (body font is already Manrope,
   1rem). Worse, a flat `color: var(--bs-link-color)` here would have
   outranked plain `a` and broken BS5's hover mechanism, which works by
   swapping --bs-link-color-rgb on :hover rather than restating `color` —
   so no separate :hover/:focus rule is needed either; native a:hover
   keeps working as long as we don't shadow `color` here. */
#loginBody .login-panel a {
    text-decoration: none;
}

/* Footer bar — subtle background, centered inline links. BS3 had this in
   .panel-footer; BS5 uses .card-footer. Keep .login-footer as the hook. */
#loginBody .login-footer,
#loginBody .card-footer.login-footer {
    background-color: var(--bs-secondary-bg, #e9ecef);
    border-top: 1px solid var(--bs-border-color, #dee2e6);
    padding: 0.75rem 2rem;
    text-align: center;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.5rem 1.25rem;
}

/* Same pattern as .login-panel a above: no flat `color` here so native
   a/a:hover keeps working (BS5 swaps --bs-link-color-rgb on :hover rather
   than restating `color`). Margin/size/layout are the real deviations. */
#loginBody a.login-footer-link {
    margin: 0;
    font-size: 0.95rem;
    text-decoration: none;
    display: inline-block;
}

@media (max-width: 767px) {
    #loginBody .login-panel .card-body {
        padding: 1.5rem 1.25rem 1rem;
    }
    #loginBody .login-heading {
        padding: 1rem 1.25rem;
    }
    #loginBody .login-logo {
        max-width: 200px;
        height: auto;
    }
    #loginBody .login-footer {
        padding: 0.75rem 1rem;
        gap: 0.35rem 0.8rem;
    }
}

/* ================================
 * Main banner / navbar (#banner.banner-navbar)
 *
 * Color/variant decisions live in the JSP (menu_include.jsp) via BS5
 * utility classes (navbar-light / navbar-dark / bg-* ). Each theme's
 * --bs-* variables drive the actual colors. This file only handles
 * app-structural tweaks that aren't about color.
 * ================================ */

/* Tightens BS5's default navbar padding (0.5rem top/bottom) — banner is a
   slim bar, not a hero. Explicit padding reset overrides BS3 main.css's
   `padding-bottom: 0` which leaves the banner top-heavy when BS5 default
   padding supplies only the top.
   (Former top+bottom 1px --bs-border-color separators were removed: that
   light gray hairline read as a white gap on colored menu strips, where
   --bs-border-color stays on the neutral palette rather than the strip
   color. The colored strip separates itself from header and content.) */
#banner.banner-navbar {
    --bs-navbar-padding-y: 0.25rem;
    padding-top: var(--bs-navbar-padding-y);
    padding-bottom: var(--bs-navbar-padding-y);
}
/* BS5's .navbar-toggler paints a thick focus ring (box-shadow 0.25rem) on
   click. Drop the chrome — banner is a slim bar and the ring looks like a
   bug at this size. */
#banner.banner-navbar .navbar-toggler {
    border: 0;
    padding: 0.25rem 0.5rem;
    /* Togglers (hamburger + Home) are not .nav-link, so BS5 colors them with
       --bs-navbar-color (the dim navbar-dark 0.55 default) and they miss the
       .nav-link 0.92 brighten + hover above. Match the nav links so Home/the
       hamburger aren't washed out, and give them the hover they otherwise lack. */
    color: var(--tln-menu-fg, rgba(255, 255, 255, 0.92));
}
#banner.banner-navbar .navbar-toggler:hover,
#banner.banner-navbar .navbar-toggler:focus {
    color: var(--tln-menu-fg, #fff);
}
#banner.banner-navbar .navbar-toggler:focus,
#banner.banner-navbar .navbar-toggler:focus-visible {
    box-shadow: none;
    outline: 0;
}
/* Home uses .navbar-toggler for identical sizing/box treatment, but unlike
   the hamburger it must remain visible above the expand breakpoint. */
#banner.banner-navbar .navbar-toggler.navbar-home {
    display: inline-block;
}
/* smartmenus auto-injects .sub-arrow with position: absolute; right: 0
   per submenu row. Each <li> needs to be the positioning context so
   arrows anchor to their own row, not the enclosing <ul>. */
#banner.banner-navbar .navbar-nav > li,
#banner.banner-navbar .navbar-nav .dropdown-menu > li {
    position: relative;
}
/* Top-level nav items should not wrap (e.g., "Course Listing"). */
#banner.banner-navbar .navbar-nav > li > .nav-link {
    white-space: nowrap;
}

/* Allow the top-level nav to wrap to multiple rows above the collapse
   breakpoint, matching BS3's float-based navbar behavior. BS5's flex
   .navbar-nav defaults to nowrap so items would otherwise clip/overflow
   at narrow widths between md and full. */
#banner.banner-navbar .navbar-nav.sm-bootstrap {
    flex-wrap: wrap;
}

/* Banner button group: main.css sets .lb-banner-btn-group to display:inline-block
   which fights BS5's .btn-group (inline-flex). With inline-block, whitespace between
   <button> tags produces a visible ~3px gap, breaking the joined-pill look.
   Restore BS5's flex behavior here so buttons sit flush like in BS3.

   PERF-14983 history: flex-wrap was tried here so the row could drop to a
   second line instead of overflowing at narrow widths. Rejected - a wrapped
   two-line button row inside the pill (with the user block sometimes
   stranded alone on its own line below) looked broken/disjointed, worse
   than the overflow it fixed. The row stays nowrap always. Button label
   visibility is now driven at runtime by fitBannerActions()
   (frameset_include.jsp) via the .lb-banner-icons-only class - not a
   viewport breakpoint - see the .lb-banner-icons-only rule above.
   white-space:nowrap on the buttons stops them from shrinking their OWN
   text onto two lines (e.g. "Log Off" wrapping to "Log" / "Off") under
   flex-shrink pressure. */
.lb-banner-btn-group {
    display: inline-flex;
}
.lb-banner-action {
    white-space: nowrap;
}
/* .lb-banner-links is a BS5-shell-only class (banner_include.jsp), owned
   here. width:auto so each sub-block (buttons / user) sizes to its own
   content inside the flex row instead of stretching to fill the column.
   (The old BS3 main.css rule that forced width:100% was removed - it only
   ever applied to this same BS5 shell and fought this layout.) */
.lb-banner-links {
    width: auto;
}
/* Visible divider between joined .banner-button pills. BS5's .btn-primary sets
   border-color to the same value as background, so adjacent buttons in a .btn-group
   have no visible line between them. Derive a darker shade of the active primary
   via color-mix so each theme keeps its own button tint. */
.lb-banner-btn-group .banner-button {
    --bs-btn-border-color: color-mix(in srgb, var(--bs-primary, #0d6efd) 80%, #000);
    --bs-btn-hover-border-color: color-mix(in srgb, var(--bs-primary, #0d6efd) 70%, #000);
    --bs-btn-active-border-color: color-mix(in srgb, var(--bs-primary, #0d6efd) 65%, #000);
}

/* BS5 delta for .lb-progress-bar-empty (base rule in main.css sets the gray
   color). BS5's .progress-bar is flex with overflow:hidden AND has a default
   blue background — at 0% width the label is clipped, and giving it min-width
   to render the text paints a filled mini-bar. Suppress the fill, let the
   text escape, and let it sit just inside the empty track. */
.progress-bar.lb-progress-bar-empty {
    overflow: visible;
    background-color: transparent;
    padding-left: 5px;
}

/* Low-percentage label clipping (PERF-14975). BS5's .progress-bar has
   overflow:hidden, so when the fill is narrower than its own label (roughly
   1-15%) the "%" text gets chopped. Give the filled bar a min-width wide enough
   to hold a 2-3 char label so it stays readable; min-width only engages when the
   computed width is smaller, so normal/wide bars are untouched. The slight
   visual overstatement at very low values is the accepted Bootstrap tradeoff
   (twbs/bootstrap#16680). The 0% case keeps its own transparent handling above. */
.progress-bar:not(.lb-progress-bar-empty) {
    min-width: 2em;
}

/* Restore BS5's default `border-collapse: collapse` on .table. The legacy
   global `table { border-collapse: separate !important }` in main.css
   (left over from an IE-era workaround) gives cells a 2px border-spacing,
   so BS5's per-cell stripe paint (inset box-shadow on <td>) doesn't bleed
   into the gap between columns — leaving visible white vertical strips
   between every column in any striped table. Scope to .table to avoid
   disturbing legacy non-BS5 tables that may still rely on the separate
   collapse mode (e.g. dataTable detail rows referenced in the original
   comment). */
.table {
    border-collapse: collapse !important;
}

/* PERF-14947: While an ajax/responsive data table is still loading it holds a
   single "Loading…" row (<td class="dt-empty" colSpan=N>). With many columns
   (e.g. reports carrying the full UserFieldsInReports set), the not-yet-collapsed
   table lays out at its full natural width — wider than the viewport — so the
   Responsive extension hasn't run yet (it only collapses AFTER the first draw).
   The centered "Loading…" text then centers within that over-wide table and
   lands off to the right, with the trailing columns running off the page.
   Constrain the table to its container while it's in the empty/loading state so
   the message stays centered on screen; reverts automatically once real rows
   replace the dt-empty row.

   PERF-14965: gated on .dt-loading (added by ui.jsp on the processing.dt event)
   so this only applies while the table is ACTIVELY loading. Keying off the
   dt-empty row alone hid the headers permanently on a settled empty table
   (DataTables 2 reuses the dt-empty cell for both "Loading…" and the steady
   "no data" state), so e.g. a goal with no activities lost its column headers. */
table.dataTable.dt-loading:has(> tbody > tr > td.dt-empty) {
    table-layout: fixed;
    width: 100%;
}
/* ...and don't flash the (squished, not-yet-collapsed) column headers while
   loading — just show the centered "Loading…". visibility:hidden keeps the
   header's height so there's no vertical jump when the real rows arrive. */
table.dataTable.dt-loading:has(> tbody > tr > td.dt-empty) > thead {
    visibility: hidden;
}

/* Suspended-org row highlight (PERF-15026). DataTables 2 paints its own row
   stripe/hover/selected as an inset box-shadow on the CELLS (via --dt-row-*),
   at higher specificity than a row-level background - so the BS3 main.css
   background-color rule is fully covered on BS5 data tables. Re-assert the
   highlight using the same box-shadow mechanism; !important is needed to win
   over DataTables' nth-of-type stripe and hover cell rules. Color matches the
   BS3 rule. */
table.dataTable tbody tr.row-suspended-org > * {
    box-shadow: inset 0 0 0 9999px #ffe0e0 !important;
}

/* Striped tables highlight the hovered row, app-wide. Every BS3 theme added this
   on top of stock Bootstrap (.table-striped > tbody > tr:nth-of-type(...):hover)
   and it was lost when the BS5 themes were regenerated - QA rediscovers it one
   page at a time (PERF-15109, PERF-15117). Same variable mechanism as BS5's own
   .table-hover, so each theme's --bs-table-hover-bg supplies the color. */
.table-striped > tbody > tr:hover > * {
    --bs-table-color-state: var(--bs-table-hover-color);
    --bs-table-bg-state: var(--bs-table-hover-bg);
}

/* Flatpickr addon cursor. data-toggle/data-clear IS the "this is clickable"
   marker in wrap-mode pickers, but only 2 of ~60 addons carried the .clickable
   class - hang the cursor on the attribute so every picker (and future ones)
   gets it without per-site classes (PERF-15150). */
.input-group.date [data-toggle],
.input-group.date [data-clear] {
    cursor: pointer;
}

/* jQuery File Upload dialog: the progress panel is hidden with the BS3-era
   .fade idiom (opacity 0, element still present). In the BS5 modal layout it
   overlaps the button bar and, being invisible but hit-testable, swallowed
   every click on the Browse button (PERF-15099 QA round). While it is not
   shown (.in is the plugin's "visible" marker), let clicks pass through. */
.fileupload-progress.fade:not(.in) {
    pointer-events: none;
}

/* Same BS3-era .fade idiom on the plugin's file-list rows: it marks a queued
   or finished file row visible by adding .in, which BS5 no longer styles
   (BS5 renamed it .show). Rows rendered but stayed at opacity 0 - an empty
   scrollable area where the file table should be (PERF-15099 QA round). */
.template-upload.fade.in,
.template-download.fade.in,
.fileupload-progress.fade.in {
    opacity: 1;
}

/* The upload dialog's progress bars carry no text label, so the global
   min-width floor (label readability, PERF-14975) does not apply - at 0%
   it painted a filled-looking 2em nub before any upload ran. */
.files .progress-bar,
.fileupload-progress .progress-bar {
    min-width: 0;
}

/* Legacy show/hide idiom: LBContainer-o (main.css) reveals with
   display:block, which kills flex on BS5 grid rows - re-shown .row fields
   stacked label-over-input (asset editor type switching, PERF-15219). */
.row.LBContainer-o {
    display: flex;
}
