/*
 * Portal theme layer on top of the UniFi-style UI Kit (unifi-ui.css).
 *
 * Loaded after the kit stylesheet. The kit already re-skins every Bootstrap
 * component (cards, buttons, forms, tables, menus, modals, pagination);
 * this file carries what the kit does not provide:
 *   1. portal-level RGB token aliases used by app.css,
 *   2. portal utility backfills (fs-*, opacity-*, btn-icon, bg-faded-*),
 *   3. shell styling for the portal navbar/footer and page heads, mapped to
 *      the kit's design tokens so light/dark both follow the kit.
 *   4. portal-owned components (portal-*), e.g. the portal-pagination bar:
 *      not overrides — the component lives here (markup in portal-utils.js).
 */

/* ---------- Portal RGB token aliases ---------- */
:root,
[data-bs-theme="light"] {
    --uf-primary-rgb: 50, 50, 56;
    --uf-green-rgb: 46, 184, 114;
    /* Real info-blue for status/activity pills (is-blue). The kit's
     * --uf-primary is a neutral dark gray, so is-blue aliased it and read
     * as gray. Live activity (Streaming/Processing, dispatching, Stream
     * kind) needs an actual blue distinct from gray. */
    --uf-info: #1d4ed8;
    --uf-info-soft: #dbeafe;
    /* Starred yellow (matches btn-warning): the kit has no is-yellow badge,
     * so the starred pill gets its own warning-palette tokens instead of
     * reusing is-orange. Bootstrap alert-warning pair, proven contrast. */
    --uf-yellow: #664d03;
    --uf-yellow-soft: #fff3cd;
    --uf-role-admin: #5b21b6;
    --uf-role-admin-soft: #ddd6fe;
    --uf-role-admin-ring: rgba(91, 33, 182, 0.35);
    --uf-role-operator: #134e4a;
    --uf-role-operator-soft: #99f6e4;
    --uf-role-operator-ring: rgba(19, 78, 74, 0.35);
    --uf-role-viewer: #334155;
    --uf-role-viewer-soft: #cbd5e1;
    --uf-role-viewer-ring: rgba(51, 65, 85, 0.35);
    /* Bootstrap 6 primary theme ramp. BS6 defaults are blue and the kit's
     * BS5 `--bs-primary` override no longer reaches the new token set, so
     * map it onto the same charcoal ramp: `theme-primary` (New-group
     * button, pagination active page), checked checks/radios/switches
     * (via --bs-control-checked-bg → --bs-primary-base) and links
     * (via --bs-link-color → --bs-primary-base) all follow these. */
    --bs-primary-base: #323238;
    --bs-primary-bg: #323238;
    --bs-primary-border: #323238;
    --bs-primary-contrast: #ffffff;
    --bs-primary-fg: #323238;
    --bs-primary-fg-emphasis: #1f1f1f;
    --bs-primary-bg-subtle: #f5f5f5;
    --bs-primary-bg-muted: #dedede;
    --bs-primary-focus-ring: rgba(50, 50, 56, 0.22);
}

[data-bs-theme="dark"] {
    --uf-primary-rgb: 148, 148, 148;
    --uf-green-rgb: 55, 192, 124;
    --uf-info: #93c5fd;
    --uf-info-soft: rgba(147, 197, 253, 0.16);
    --uf-yellow: #ffda6a;
    --uf-yellow-soft: rgba(255, 218, 106, 0.16);
    --uf-role-admin: #ddd6fe;
    --uf-role-admin-soft: rgba(196, 181, 253, 0.30);
    --uf-role-admin-ring: rgba(196, 181, 253, 0.55);
    --uf-role-operator: #99f6e4;
    --uf-role-operator-soft: rgba(94, 234, 212, 0.28);
    --uf-role-operator-ring: rgba(94, 234, 212, 0.50);
    --uf-role-viewer: #e2e8f0;
    --uf-role-viewer-soft: rgba(203, 213, 225, 0.26);
    --uf-role-viewer-ring: rgba(203, 213, 225, 0.45);
    /* Dark-mode half of the BS6 primary ramp above: solid CTAs and the
     * active pagination page invert to a light fill + dark text (same as
     * the kit's dark `.btn-primary`/`.pagination` overrides), while
     * checked form controls stay on the gray accent. */
    --bs-primary-base: #e8e8e8;
    --bs-primary-bg: #e8e8e8;
    --bs-primary-border: #e8e8e8;
    --bs-primary-contrast: #1f1f1f;
    --bs-primary-fg: #e8e8e8;
    --bs-primary-fg-emphasis: #f5f5f5;
    --bs-primary-bg-subtle: #262626;
    --bs-primary-bg-muted: #2e2e2e;
    --bs-primary-focus-ring: rgba(148, 148, 148, 0.45);
    --bs-control-checked-bg: #949494;
    --bs-control-checked-border-color: #949494;
}

/* ---------- Portal utility backfills ---------- */
/* Kit quiet label (form labels and small section labels on any element). */
.form-label, .uf-label { font-size: 13px; font-weight: 500; color: var(--uf-text-2); margin-bottom: 6px; }
.fs-xs { font-size: 0.75rem !important; }
.fs-sm { font-size: 0.875rem !important; }
.fs-base { font-size: 1rem !important; }
.fs-lg { font-size: 1.125rem !important; }

.opacity-10 { opacity: 0.1 !important; }
.opacity-50 { opacity: 0.5 !important; }

/* Square icon-only button variant (toolbar actions, drawer headers).
   Size classes override the box (btn-xs 1.5rem / btn-sm 2rem, matching
   the kit's documented icon-button sizes); the bare variant keeps the
   2.25rem default the header-row X buttons and API-key actions are
   built around. */
.btn-icon {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
}
.btn-icon.btn-xs { width: 1.5rem; height: 1.5rem; }
.btn-icon.btn-sm { width: 2rem; height: 2rem; }

.bg-faded-primary { background-color: var(--uf-primary-soft) !important; }

/* Info-blue dot for live activity (streaming/dispatching). Real blue,
 * not the neutral theme accent, so active work never reads as gray. */
.uf-dot.is-blue { background: var(--uf-info); }

/* Info-blue soft pill (Streaming/Processing activity, dispatching, Stream
 * kind, input role). Real blue distinct from the gray inactive pill. */
.uf-badge.is-blue { color: var(--uf-info); background: var(--uf-info-soft); }
/* Starred pill (job-level importance): warning yellow, matching the
 * btn-warning star button — never is-orange (reserved for paused/partial/
 * unassigned attention states). */
.uf-badge.is-yellow { color: var(--uf-yellow); background: var(--uf-yellow-soft); }
/* Kit role-namespace extension (docs/role-badge-namespace-design.md): dedicated
   role colors, reusable at kit level. Roles MUST use is-role-*; status MUST
   NOT use is-role-*. Distinct from green/red/orange/primary/gray status.
   Soft-but-noticeable for identification: saturated pastel fill + inset ring
   (status pills have neither), AA text contrast both themes. */
.uf-badge.is-role-admin { color: var(--uf-role-admin); background: var(--uf-role-admin-soft); box-shadow: inset 0 0 0 1px var(--uf-role-admin-ring); }
.uf-badge.is-role-operator { color: var(--uf-role-operator); background: var(--uf-role-operator-soft); box-shadow: inset 0 0 0 1px var(--uf-role-operator-ring); }
.uf-badge.is-role-viewer { color: var(--uf-role-viewer); background: var(--uf-role-viewer-soft); box-shadow: inset 0 0 0 1px var(--uf-role-viewer-ring); }
/* Status/count pills must stay on one line ("1 Idle", not "1" over "Idle");
   the toolbar wraps the pills as whole units instead. */
.uf-badge { white-space: nowrap; }

/* Group identity glyph on its soft tile: theme accent — monochrome in the
   light mode, neutral gray in dark. Status dots/badges keep colors. */
.group-identity-icon { color: var(--uf-primary); }

/* ---------- Login password visibility toggle ---------- */
/* Bootstrap 6 pattern (see redesign_auth_controls.html): the toggle is a real
   `.input-group-btn` button addon inside an `.input-group`, so the kit owns
   its borders, focus ring and spacing — no portal overlay CSS needed. The
   pre-BS6 `.password-toggle` absolute-overlay rules were removed with that
   markup. */

/* ---------- Input-group seams (layer inversion backfill) ---------- */
/* BS6 ships the adjoining border-radius + -1px overlap rules in cascade
   layers, so the kit's UNLAYERED `.form-control` radius (var(--uf-radius))
   beats the layered seam negation and every input adjoining a button/addon
   keeps fully rounded seam corners (e.g. the login password toggle). Same
   established pattern as the utility overrides below: re-assert the seam
   rules here, unlayered, verbatim from BS6 so behaviour matches the docs. */
.input-group > :not(:last-child, .menu-toggle-split, .menu, .input-group-ignore, .form-floating, :has(+ :is(.menu, .input-group-ignore):last-child)),
.input-group > .menu-toggle-split:nth-last-child(n+3),
.input-group > .form-floating:not(:last-child) > .form-control {
    border-start-end-radius: 0;
    border-end-end-radius: 0;
}
.input-group > :not(:first-child, .menu, .input-group-ignore) {
    border-start-start-radius: 0;
    border-end-start-radius: 0;
    margin-inline-start: calc(-1 * var(--bs-border-width, 1px));
}

/* ---------- Shell: kit top bar + icon rail ---------- */
/* The kit owns .uf-topbar/.uf-rail styling; only the portal's user-menu
   trigger and the shell flex column for the footer live here. */
/* Responsive shell visibility. Bootstrap 6 ships its utilities in cascade
   layers, so the kit's unlayered display rules (.uf-icon-btn, .uf-rail)
   beat the layered responsive utilities (v5 utilities were !important and
   won). Re-assert the mobile/desktop split unlayered at the v6 lg
   breakpoint (1024px): the drawer toggle is mobile-only, the icon rail and
   the user menu are desktop-only (the drawer takes over below lg). */
@media (min-width: 1024px) {
    .uf-topbar .uf-icon-btn[data-bs-toggle="drawer"] { display: none; }
}
@media (max-width: 1023.98px) {
    .uf-rail.d-none { display: none; }
    .uf-topbar .nav.d-none { display: none; }
}
/* Same layer inversion for kit component padding/borders that utilities are
   meant to override (v5 !important won; v6 layered utilities lose to the
   unlayered kit). Re-asserted here, unlayered, only for the exact
   utility+component combinations this codebase uses. */
.card-body.p-0 { padding: 0; }
.card-body.p-7 { padding: 1.5rem; }
@media (min-width: 768px) {
    .card-body.md\:p-12 { padding: 3rem; }
}
.card-body.py-3 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.card-body.py-5 { padding-top: 1rem; padding-bottom: 1rem; }
.card-header.border-0 { border: 0; }
.uf-topbar .nav-link {
    color: var(--uf-text-1);
}
.uf-topbar .nav-link:hover { color: var(--uf-text-2); }
/* Mobile drawer + user-menu rows share the reference shell look: plain
   Bootstrap nav-link markup styled via descendant selectors (active pill,
   icon sizing), so the drawer matches the TCIDS reference. */
.drawer .nav-link { color: var(--uf-text-1); font-weight: 500; }
.drawer .nav-link:hover { color: var(--uf-text-1); background: var(--uf-bg-muted); }
.drawer .nav-link.active { color: var(--uf-text-1); background: var(--uf-bg-muted); }
.uf-mobile-nav { --bs-drawer-width: 300px; }
.uf-mobile-nav .nav-link {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border-radius: var(--uf-radius);
    color: var(--uf-text-1);
    font-size: 14px;
    font-weight: 500;
}
.uf-mobile-nav .nav-link i {
    color: var(--uf-text-2);
    font-size: 18px;
}
/* Lucide SVG sizing: the drawer rows use flex gap, so only set the box. */
.uf-mobile-nav .nav-link .lucide {
    width: 18px;
    height: 18px;
    flex: none;
}
/* Row icons only (direct children: globe/moon in the language/theme rows).
   The bottom user row nests its account/sign-out icons inside centered
   `.uf-icon-btn` squares — the 10px gutter must not reach them. */
.uf-mobile-nav .drawer-header > .lucide {
    width: 18px;
    height: 18px;
    margin-right: 10px;
    opacity: 0.6;
}
.uf-mobile-nav .nav-link:hover { background: var(--uf-bg-muted); color: var(--uf-text-1); }
.uf-mobile-nav .nav-link.active {
    background: var(--uf-bg-muted);
    color: var(--uf-text-1);
}
.uf-mobile-nav .nav-link.active i { color: var(--uf-text-1); }
/* User menu items adopt the portal row look (see the Navigation row rhythm
   block below); stock .menu-item + .active handles the highlighted row. */
/* Lucide SVG icons in the account menu (menu rows + language/theme rows):
   full strength like the rail (no faded treatment). Sizing stays at the
   drawer tier (18px vs the rail's 21px) for menu density. Spacing follows the
   row type so the icon-to-text gap matches the rail's 10px: menu rows use the
   stock .menu-item gap (margin zeroed), while the plain d-flex language/theme
   rows keep a 10px margin (they have no flex gap). */
.menu .lucide {
    width: 18px;
    height: 18px;
    flex: none;
    margin-right: 10px;
}
.menu .menu-item .lucide { margin-right: 0; }
.menu-text .lucide { opacity: 0.6; }

/* Quiet user chip replacing the bare avatar trigger: greeting text + caret +
   avatar, tinted on hover like the kit's app-switcher. The greeting hides
   below the xl breakpoint so the topbar never crowds at lg. */
.uf-user-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 6px 4px 14px;
    border: 0;
    border-radius: 999px;
    background: none;
}
.uf-user-chip:hover { background: var(--uf-bg-muted); }
/* Light-up on hover like the left-rail rows: greeting and caret each step up
   one text tone (the avatar keeps its primary/soft treatment). */
.uf-user-chip:hover .uf-user-greeting { color: var(--uf-text-1); }
.uf-user-chip:hover .uf-user-caret { color: var(--uf-text-2); }
.uf-user-greeting {
    font-size: 13.5px;
    color: var(--uf-text-2);
    max-width: 24ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.uf-user-caret { font-size: 18px; color: var(--uf-text-3); }
/* Icon rail: top-group nav links are styled via descendant selectors so the
   markup keeps the plain Bootstrap `nav-link` / `nav-link active` classes
   (reference shell pattern). */
.uf-rail .nav-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    padding: 0;
    border: 0;
    border-radius: 10px;
    /* Rests at text-2 (kit ships text-3) for readability; hover still lights
       up to text-1 and the active pill stays primary. */
    color: var(--uf-text-2);
    font-size: 0;
    transition: color 0.15s, background-color 0.15s;
}
.uf-rail .nav-link i {
    font-size: 21px;
    line-height: 1;
}
/* Rail nav links render Lucide SVGs (the kit only sizes .uf-rail-item .lucide,
   not the product nav-link markup used in the top group). */
.uf-rail .nav-link .lucide {
    width: 21px;
    height: 21px;
    flex: none;
}
.uf-rail .nav-link:hover {
    color: var(--uf-text-1);
    background: var(--uf-bg-muted);
}
.uf-rail .nav-link.active {
    color: var(--uf-text-1);
    background: var(--uf-bg-muted);
}
[data-bs-theme="light"] .uf-rail .nav-link.active {
    color: var(--uf-on-primary);
    background: var(--uf-primary);
}
/* Bottom-group/toggle rows use the kit's .uf-rail-item class (kit ships
   text-3): same one-step brightening, scoped off :hover/:is-active so the
   text-1 hover light-up and the primary active pill keep working. */
.uf-rail .uf-rail-item:not(.is-active):not(:hover) { color: var(--uf-text-2); }
/* Rail icons are Lucide SVG (kit default): the kit sizes bottom-group icons
   via .uf-rail-item .lucide { width: 21px; height: 21px; } — no portal override. */

/* Lucide spinning indicator (upload submit / picker loading states). */
.lucide-spin { animation: portal-lucide-spin 1s linear infinite; }
@keyframes portal-lucide-spin { to { transform: rotate(360deg); } }

/* Starred job: filled star glyph (survives Lucide re-renders, unlike a
   fill attribute set from JS). */
#job-importance[data-important="true"] svg { fill: currentColor; }

/* ---------- In-page anchor offset (sticky topbar) ---------- */
/* Anchor jumps land the target at the viewport top, where the sticky
   .uf-topbar (--uf-topbar-h) would otherwise cover the section heading. */
html {
    scroll-padding-top: calc(var(--uf-topbar-h) + 16px);
}

/* Preserve native anchor navigation and animate only the config page. */
html:has(#application-config-form) {
    scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
    html:has(#application-config-form) {
        scroll-behavior: auto;
    }
}

/* ---------- Shell: sidebar expanded/minimized states ---------- */
/* The kit only ships the fixed 64px icon rail; the expanded view (icon +
   label rows) and the persisted toggle are portal additions. The state lives
   in <html data-rail-state="...">, set pre-paint by static/js/sidebar.js
   (default: expanded). Minimized restores the kit's original icon-only rail,
   and mobile is unaffected — the kit hides the rail below the desktop
   breakpoint and the drawer takes over. */
:root[data-rail-state="expanded"] {
    --uf-rail-w: 224px;
}
/* Shared height for the page footer and the rail's toggle band, so the two
   bottom hairlines sit on the same line (the rail is fixed to the viewport
   bottom exactly like the shell footer). */
:root {
    --uf-footer-h: 54px;
}
.uf-shell .footer {
    height: var(--uf-footer-h);
    padding-top: 0;
    padding-bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}
.uf-rail,
.uf-shell {
    transition: width 0.2s ease, margin-left 0.2s ease;
}
@media (prefers-reduced-motion: reduce) {
    .uf-rail,
    .uf-shell { transition: none; }
}

/* Item labels: real text in the expanded view; hidden in the minimized rail
   (the item's title attribute keeps a native tooltip there). */
.uf-rail-label {
    font-size: 13.5px;
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
:root[data-rail-state="minimized"] .uf-rail-label { display: none; }

/* Expanded rows: full-width, icon leading, label trailing. Applies to both the
   kit's .uf-rail-item and the product nav-link markup used in the top group. */
:root[data-rail-state="expanded"] .uf-rail { padding-inline: 10px; }
:root[data-rail-state="expanded"] .uf-rail-group {
    align-items: stretch;
    width: 100%;
}
:root[data-rail-state="expanded"] .uf-rail .nav-link,
:root[data-rail-state="expanded"] .uf-rail-item {
    width: 100%;
    height: 40px;
    justify-content: flex-start;
    gap: 10px;
    padding: 0 12px;
}
:root[data-rail-state="expanded"] .uf-rail .nav-link {
    font-size: 13.5px;
    font-weight: 500;
}

/* Toggle band pinned to the rail bottom with the same height (and hairline
   position) as the page footer; the kit's rail bottom padding is dropped so
   the band sits flush like the footer does. The rail-bottom admin group just
   above it is a kit feature (see .uf-rail-bottom in vendor/css/unifi-ui.css),
   not a portal override. */
.uf-rail { padding-bottom: 0; }
.uf-rail-group:last-child {
    border-top: 1px solid var(--uf-border-light);
    height: var(--uf-footer-h);
    justify-content: center;
}
.uf-rail-toggle {
    border: 0;
    background: none;
    cursor: pointer;
}
.uf-rail-toggle svg, .uf-rail-toggle .lucide { transition: transform 0.2s ease; }
:root[data-rail-state="minimized"] .uf-rail-toggle svg,
:root[data-rail-state="minimized"] .uf-rail-toggle .lucide { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
    .uf-rail-toggle svg, .uf-rail-toggle .lucide { transition: none; }
}
/* The rail is fixed full-height, so the page area must be the flex sibling
   that grows; the footer stays pinned to the viewport bottom inside it. */
.uf-shell {
    flex: 1 1 auto;
    min-height: 0;
}

/* ---------- Closed dialogs ---------- */
/* The kit's .dialog keeps display:flex when closed (visibility:hidden drives
   the open/close transitions, including .hiding), so a closed dialog's body is
   still laid out. Its overflowing content then extends the page scroll height.
   Fixed positioning takes closed dialogs out of document flow without changing
   the open/close animation. */
dialog.dialog:not([open]) {
    position: fixed;
}

/* ---------- Navigation row rhythm (rail + menu) ---------- */
/* One density for every navigation surface: 40px rows, 2px between them. The
   kit ships 6px gaps with 40-42px rail rows and ties the rail to a single gap
   value, so rail and menu rows are overridden as one set here.
 *
 * This block sits AFTER the rail-state rules on purpose. Those use
 * `:root[data-rail-state="expanded"]` (specificity 0,3,0) against a plain
 * `.uf-rail .nav-link` (0,2,0); placing the overrides earlier would lose to
 * them on the expanded height. Repeating the state selector here keeps
 * specificity equal so load order decides — no !important needed. Same reason
 * for using `.menu.show` (not a bare `.menu`): that exact selector is what
 * keeps the gap alongside Bootstrap's `.menu.show` display rules.
 *
 * Covers all three rail groups plus the menu. Minimize-sidebar is a
 * `.uf-rail-item` too, so it follows automatically. The mobile drawer
 * (.uf-mobile-nav) is deliberately NOT covered: its spacing is per-row padding
 * in a plain <ul>, not a flex gap, and it is the one surface where rows are
 * finger-sized. */
.uf-rail-group,
.uf-rail-group > ul,
.menu.show {
    gap: 2px;
}

/* The rail's top group nests its rows one level deeper (a <ul> inside
   .uf-rail-group), so the group gap above does not reach them — that is why
   this used to carry a `gap-2` utility and read wider than the rest of the
   rail. The utility is gone from the markup; the value lives here with the
   other two so all three groups share one rhythm. */

/* One row height for every rail row, expanded or minimized. 40px is the
   portal's established rail density (the expanded state shipped 40px) and
   keeps a usable pointer target; the minimized icon buttons are square at the
   same 40px so the 10px radius reads identically in both states. The kit ships
   42px squares. */
.uf-rail .nav-link,
.uf-rail-item {
    width: 40px;
    height: 40px;
}

/* Expanded rail: full-width rows at the same height. Width and centering are
   re-stated because the rail-state rule owns them upstream. */
:root[data-rail-state="expanded"] .uf-rail .nav-link,
:root[data-rail-state="expanded"] .uf-rail-item {
    width: 100%;
    height: 40px;
}

/* One type treatment for every rail row. The two groups ship DIFFERENT classes
   (`.nav-link` in the top group, the kit's `.uf-rail-item` below), and only the
   first had portal typography — 13.5px/500 vs the kit's 14px/400 — which is
   the other half of "the top of the nav looks different". 13.5px/500 is the
   portal's stated rail density, so the kit side is brought up to it. Icon size
   (21px) and rest/hover colors already matched and are left alone. */
.uf-rail .nav-link,
.uf-rail-item {
    font-size: 13.5px;
    font-weight: 500;
    line-height: 1.5;
}

/* Menu rows. Bootstrap's stock .menu-item block padding (0.25rem) sits shorter
   than the rail's 40px, so padding is what has to move; 9.5px lands the row on
   the same 40px as the rail, with min-height as the floor. Padding rather than
   a fixed height, so a wrapped or longer translated label still grows the row
   instead of clipping. */
.menu-item {
    min-height: 40px;
    padding-top: 9.5px;
    padding-bottom: 9.5px;
}
/* Language + theme rows: the same 40px. The language row is sized by its
   embedded control (form-control-sm is 37px), which stretch cannot shrink inside
   an auto-height row, so the select is pinned to the 32px content box and the
   row's own 4px padding brings it to 40px. The theme row's switch keeps its
   natural 24px and is centred. */
.menu-text {
    min-height: 40px;
}
.menu-text .form-control {
    height: 32px;
    min-height: 0;
    padding-top: 0;
    padding-bottom: 0;
}

/* ---------- Shell: page head ---------- */
/* Kit page heads are compact: 20px/600 title with a 13.5px muted subtitle. */
main h1 {
    font-size: 20px;
    font-weight: 600;
    line-height: 1.3;
    letter-spacing: -0.01em;
}
main h1 + p.fg-secondary { font-size: 13.5px; }

/* ---------- Shell: footer ---------- */
/* Shell chrome shares the surface token with the topbar and rail so the frame
   stays one step above the content canvas. */
.footer { background: var(--uf-surface); }

/* ---------- Neutral secondary button follows the kit's btn-default look ---------- */
/* Formerly `.btn-secondary`; Bootstrap 6 composes it as `.btn-solid.theme-secondary`. */
.btn-solid.theme-secondary {
    --bs-btn-color: var(--uf-text-1);
    --bs-btn-bg: var(--uf-surface);
    --bs-btn-border-color: var(--uf-border);
    --bs-btn-hover-color: var(--uf-text-1);
    --bs-btn-hover-bg: var(--uf-bg-subtle);
    --bs-btn-hover-border-color: var(--uf-border-strong);
    --bs-btn-active-color: var(--uf-text-1);
    --bs-btn-active-bg: var(--uf-bg-muted);
    --bs-btn-active-border-color: var(--uf-border-strong);
    --bs-btn-disabled-color: var(--uf-text-1);
    --bs-btn-disabled-bg: var(--uf-surface);
    --bs-btn-disabled-border-color: var(--uf-border);
}

/* ---------- Muted disabled treatment for .btn-default ---------- */
/* The kit's disabled rule targets .btn-solid/.btn-outline/.btn-subtle/
/.btn-text but not the portal-era .btn-default, so disabled buttons
(expired artifact Open/Copy link, expired event files) rendered at full
strength. Mirror the kit composition: muted text on a muted fill plus
the standard disabled fade, so unavailable actions read as unavailable. */
.btn-default {
    --bs-btn-disabled-color: var(--uf-text-3);
    --bs-btn-disabled-bg: var(--uf-bg-muted);
    --bs-btn-disabled-border-color: var(--uf-border);
}
.btn-default:disabled,
.btn-default.disabled,
fieldset:disabled .btn-default {
    color: var(--bs-btn-disabled-color);
    pointer-events: none;
    background-color: var(--bs-btn-disabled-bg);
    border-color: var(--bs-btn-disabled-border-color);
    opacity: var(--bs-btn-disabled-opacity, 0.65);
    background-image: none;
}

/* Focusable wrapper around a disabled expired-file button: it owns the
   unavailability tooltip (disabled buttons get no hover/focus events),
   so it shows the not-allowed cursor the inert button cannot. */
.vpp-expired-control {
    cursor: not-allowed;
}

/* ---------- Tables: kit re-skin for Bootstrap variants ---------- */
/* Bootstrap's .table.theme-subtle hard-codes a white header (no dark-mode flip), so
   re-map the variant to the kit's table tokens like unifi-ui.css does for the
   base .table; portal pages must use plain <thead> instead. */
.table.theme-subtle {
    --bs-table-color: var(--uf-text-1);
    --bs-table-bg: transparent;
    --bs-table-border-color: var(--uf-border-light);
    --bs-table-striped-bg: var(--uf-bg-subtle);
    --bs-table-striped-color: var(--uf-text-1);
    --bs-table-active-bg: var(--uf-bg-muted);
    --bs-table-active-color: var(--uf-text-1);
    --bs-table-hover-bg: var(--uf-bg-subtle);
    --bs-table-hover-color: var(--uf-text-1);
}

/* ---------- Light-theme body/card surfaces use the kit tokens ---------- */
/* The page canvas is a touch lighter than the kit's #f0f0f0 (#ebebeb neutral
   read heavy next to the white cards and darkened the whole shell). Only the
   canvas moves: the footer follows it (same token), and the near-white cards
   keep their separation through their own shadow. Scoped to the light
   attribute, NOT `:root` — the kit sets its dark values on
   `[data-bs-theme="dark"]` at the same specificity, so a `:root` rule here
   would load later and win in dark mode too. */
[data-bs-theme="light"] {
    --uf-bg: #f6f6f7;
}

[data-bs-theme="light"] body {
    background-color: var(--uf-bg);
    color: var(--uf-text-1);
}

[data-bs-theme="light"] .footer {
    background-color: var(--uf-bg);
}

/* Settings-page label column (kit schedules pattern): 14px, primary text. */
.config-label {
    display: inline-block;
    padding-top: 0.35rem;
    font-size: 14px;
    font-weight: 500;
    color: var(--uf-text-1);
}

.config-search-hidden {
    display: none !important;
}

/* Keep page search visually separate from the configuration form. */
.config-search-panel {
    border: 1px solid var(--uf-border);
    background: var(--uf-surface);
}
.config-search-panel .card-body {
    padding: 0 16px 12px;
}
.config-search-toggle {
    width: 100%;
    justify-content: space-between;
    padding: 10px 16px;
    border: 0;
}
.config-search-toggle[aria-expanded="true"] .config-search-chevron {
    transform: rotate(180deg);
}
.config-search-controls .form-control {
    flex: 1;
    min-width: 0;
}
.config-search-controls .btn {
    flex-shrink: 0;
}
.config-search-panel #config-search-status:empty {
    display: none;
}

.config-mobile-nav {
    display: none;
}
@media (max-width: 767.98px) {
    .config-section-sidebar {
        display: none;
    }
    .config-mobile-nav {
        display: block;
    }
}

/* Job detail: source video sits under a normal card header, so drop the
   12px top margin the media-modal bar composition adds. */
#job-video-card .uf-media-view {
    margin-top: 0;
}

/* Job detail: scroll-away floating mini player (PiP-style). When the
   source-video card scrolls out of view upward, the SAME <video> node
   is reparented into #job-video-mini-float (fixed corner box) so
   review continues while reading results below; scrolling back docks
   it into the card again. Reparenting never reloads media, so
   playback, playhead and seek state survive. Active for playable
   video only — never images, placeholders, streams or share mode. */
#job-video-mini-float {
    position: fixed;
    right: 16px;
    bottom: 16px;
    width: min(360px, calc(100vw - 32px));
    z-index: 1040; /* below Bootstrap dialogs (native top layer) and toasts */
    background: var(--bs-bg-body, #fff);
    border: 1px solid var(--bs-border-color, rgba(0, 0, 0, 0.15));
    border-radius: 12px;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
    overflow: hidden;
}
#job-video-mini-float #job-video-player {
    width: 100%;
    margin-top: 0;
}
#job-video-mini-bar {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 6px 6px 6px 12px;
}
#job-video-mini-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 0.8125rem;
    font-weight: 600;
}
/* Layout placeholder left in the card while the player floats: same
   16:9 footprint as .uf-media-view (invisible) so the page never
   jumps and the scroll observer target stays stable. */
#job-video-mini-home {
    visibility: hidden;
    background: transparent;
}
@media (max-width: 576px) {
    #job-video-mini-float {
        right: 12px;
        bottom: 12px;
        width: min(260px, calc(100vw - 24px));
    }
}
@media (prefers-reduced-motion: reduce) {
    #job-video-mini-float {
        transition: none;
    }
}

/* Job detail: the source placeholder (file unavailable OR format
   unsupported by the HTML5 player) keeps the player's 16:9 box so the
   card never collapses when there is nothing to play — same footprint
   as .uf-media-view, content vertically centered. The global
   `[hidden] { display: none !important }` restore in app.css keeps the
   hidden toggling intact. */
#job-video-card #job-video-unavailable {
    width: 100%;
    aspect-ratio: 16 / 9;
    justify-content: center;
    border-radius: 10px;
    padding: 20px;
}

/* Job detail: still-image sources show the whole frame (contain, not the
   16:9 cover crop the video player uses) so defects are never cropped out. */
#job-video-card #job-image-view {
    object-fit: contain;
}

/* ---------- Pagination: spaced items ---------- */
/* The kit sets `gap: 3px` on `.pagination`, which reads as stuck together
   next to the reference component. Keep the kit's borderless rounded look,
   just give the items breathing room. */
.pagination { gap: 0.5rem; }
/* Labeled Prev/Next links pair a chevron with text; keep them aligned. */
.pagination .page-link { display: inline-flex; align-items: center; gap: 0.25rem; }
.pagination .page-link svg, .pagination .page-link .lucide { width: 1em; height: 1em; }
/* Disabled bounds read as quiet text, never as lit buttons: Bootstrap fills
   disabled page links with its secondary-bg pill, which is too bright against
   the kit surfaces (notably in dark mode). Transparent ground + tertiary text
   + no pointer response instead. */
.pagination .page-item.disabled .page-link {
    color: var(--uf-text-3);
    background-color: transparent;
    border-color: transparent;
    opacity: 0.6;
    pointer-events: none;
}

/* ---------- Pagination: reference full bar ---------- */
/* Full-bar layout from the shared pager (PortalUtils.renderCanonicalPagination
   full mode): info (items-per-page + showing) left, numbered pager middle,
   Go-to right; wraps into a natural stack on narrow screens. */
.portal-pagination {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem 1rem;
    width: 100%;
}
.portal-pagination__info {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem 0.75rem;
    margin-right: auto;
}
.portal-pagination__label { margin: 0; font-size: 0.8125rem; color: var(--uf-text-2); }
.portal-pagination__label--short { display: none; }
.portal-pagination__select { width: auto; min-width: 4.5rem; }
.portal-pagination__showing { font-size: 0.8125rem; color: var(--uf-text-2); }
.portal-pagination__goto {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.8125rem;
    color: var(--uf-text-2);
}
.portal-pagination__goto label { margin: 0; }
.portal-pagination__input { width: 4.5rem; }
/* Pager controls share the BS6 sm height token, so the Go-to input, the
   page-size select and the Go button match the `.pagination-sm .page-link`
   min-height. (The kit's unlayered `.form-control` padding otherwise wins
   over the layered sm metrics here.) */
.portal-pagination__input.form-control-sm {
    padding: 0.25rem 0.5rem;
    font-size: 0.875rem;
    line-height: 1.5;
    min-height: var(--bs-btn-input-sm-min-height, 2rem);
}
.portal-pagination__select.form-control-sm {
    padding-top: 0.25rem;
    padding-bottom: 0.25rem;
    padding-left: 0.5rem;
    font-size: 0.875rem;
    line-height: 1.5;
    min-height: var(--bs-btn-input-sm-min-height, 2rem);
}
/* Go-to submit is the bar's only true action, so it keeps the visible
   btn-default treatment (surface fill + border + button states) while
   matching the bar's metrics: pagination-sm padding/type and 8px radius, so
   it renders at exactly the boxed input's height (BS6 sm min-height token).
   Page numbers and Prev/Next stay quiet links by design. */
.portal-pagination__go {
    --bs-btn-padding-y: 0.25rem;
    --bs-btn-padding-x: 0.5rem;
    --bs-btn-font-size: 0.875rem;
    --bs-btn-border-radius: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    line-height: 1.5;
    min-height: var(--bs-btn-input-sm-min-height, 2rem);
    font-weight: 500;
}

/* ---------- Pagination: compact phone bar ---------- */
/* Below sm the pager hides every numbered item (`d-none d-sm-block`) while
   Prev/Next keep chevron + text, so the bar sprawls over four rows with no
   page number in sight. On phones the layout is explicit, not flex-wrapped:
   row 1 is the info line (short page-size label + select + "Showing …"
   status), row 2 is the navigation (pager + quick-go). The full
   "Items per page" label swaps for the short "Per page" variant and the
   "Showing …" status drops its unit word, so row 1 fits the ~280px phone
   column. The chevrons are meaningful on their own, so Prev/Next text is
   visually hidden (spans stay for screen readers, links keep their
   aria-labels) and the numbers come back, tightened so a normal window fits
   its row (huge windows wrap inside the pager instead of pushing the page).
   The "Go to" label is dropped — the input keeps its aria-label — and the
   quick-go input shrinks to ~3 digits. No-op at sm and up. */
@media (max-width: 575.98px) {
    .portal-pagination {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: 0.5rem;
        align-items: center;
    }
    .portal-pagination__info {
        grid-column: 1 / -1;
        margin-right: 0;
        gap: 0.5rem;
    }
    .portal-pagination__info .portal-pagination__showing {
        margin-left: auto;
    }
    .portal-pagination ul.pagination {
        grid-column: 1;
        min-width: 0;
        gap: 0.25rem;
    }
    .portal-pagination__goto {
        grid-column: 2;
    }
    .portal-pagination .pagination > .page-item:first-child .page-link > span,
    .portal-pagination .pagination > .page-item:last-child .page-link > span,
    .portal-pagination__info .portal-pagination__label--full {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }
    .portal-pagination__info .portal-pagination__label--short {
        display: block;
    }
    .portal-pagination .pagination > .page-item.d-none.sm\:d-block {
        display: block !important;
    }
    .portal-pagination__goto label {
        display: none;
    }
    .portal-pagination .portal-pagination__select { min-width: 3.5rem; }
    .portal-pagination .portal-pagination__input { width: 2.75rem; }
}

/* White-label brand slot (docs/white-label-design.md §8): fixed 160x32 box
   matching the normalized upload; object-fit fill stretches like the server
   normalization so the preview matches the rendered chrome. */
.brand-logo {
    width: 160px;
    height: 32px;
    object-fit: fill;
    display: block;
}
.uf-brand .brand-logo { margin: 0 auto; }
.redesign-brand .brand-logo { display: inline-block; vertical-align: middle; }

/* Segmented control — unavailable option (kit uf-seg pattern, e.g. a submit
   mode the group's kind policy rejects). The segment stays in the pill so
   the control never breaks shape; it reads muted and cannot be chosen.
   `pointer-events` is restored (Bootstrap kills it on disabled labels) so
   the `title` reason still shows on hover — activating a disabled input is
   a browser no-op, so clicks remain safe. */
/* (Bootstrap 6 nests the input inside label.btn-check, so the v5 sibling
   selectors became :has(). The kit's checked-segment shadow is replicated
   here since the vendored kit still targets the v5 pattern.) */
.uf-seg .btn-check:has(input:checked) { box-shadow: var(--uf-shadow-sm); }
/* BS6 `.btn` base backfill for the segmented control: v6 fills every
   button with `--bs-bg-2` and enforces a 38px `--bs-btn-min-height`
   floor (v5 buttons were transparent with no floor), which turns the
   compact transparent segments into tall gray pills. Restore the kit
   look — transparent rest state, natural height from the kit's own
   padding — while the checked white pill still comes from the kit's
   `--bs-btn-active-*` tokens via `:has()`. */
.uf-seg .uf-seg-btn {
    --bs-btn-bg: transparent;
    --bs-btn-min-height: 0;
}
.uf-seg .btn-check:has(input:disabled) {
  --bs-btn-color: var(--uf-text-3);
  --bs-btn-hover-color: var(--uf-text-3);
  --bs-btn-active-color: var(--uf-text-3);
  color: var(--uf-text-3);
  background: transparent;
  box-shadow: none;
  opacity: 0.6;
  pointer-events: auto;
  cursor: not-allowed;
}
.uf-seg .btn-check:has(input:disabled:checked) {
  background: transparent;
  box-shadow: none;
}

/* ---------- Application Config: storage backend sub-menu ---------- */
/* The Storage Backend select owns two mutually exclusive option groups
   (Local filesystem vs S3/MinIO). The active group renders as an indented
   branch under the selector — a slim accent rail, no enclosing card — so
   it reads as a sub-menu of the backend choice rather than a separate box.
   The inactive group is display:none (JS), so the form never invites
   editing a value the active backend ignores. Inner rows reuse the
   section's label/input columns so every input stays on one vertical line. */
.storage-backend-panels {
    margin-bottom: 1.5rem;
}
.storage-backend-panel {
    margin: 0 0 4px 12px;
    padding: 8px 0 8px 20px;
    border-left: 2px solid var(--uf-primary);
    transition: background-color 0.3s ease;
}
.storage-backend-panel-header {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 2px;
}
.storage-backend-panel-header .lucide {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--uf-text-2);
}
.storage-backend-panel-title {
    font-size: 13px;
    font-weight: 600;
    color: var(--uf-text-1);
    margin-right: auto;
}
/* Switching backends reveals the newly active branch with a short
   fade/slide plus a soft background wash so the changed option set is
   impossible to miss. */
.storage-backend-panel:not(.d-none) {
    animation: storage-panel-in 180ms ease-out;
}
.storage-backend-panel.is-flash {
    background-color: var(--uf-primary-soft);
    border-radius: 0 8px 8px 0;
}
@keyframes storage-panel-in {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}
.storage-backend-hint {
    font-weight: 600;
    color: var(--uf-text-1);
}
@media (prefers-reduced-motion: reduce) {
    .storage-backend-panel {
        transition: none;
    }
    .storage-backend-panel:not(.d-none) {
        animation: none;
    }
    .storage-backend-panel.is-flash {
        background-color: transparent;
    }
}
