/*
 * Ureh Admin UI — the small set of custom rules beyond what Bootstrap /
 * the vendored Jampack bundle already provide. Every rule here uses
 * Bootstrap CSS variables (var(--bs-*)), never a literal hex, so it
 * inherits both the light and dark theme for free — see
 * docs_2/ui-design-system.md.
 */

/* Enterprise Typography System — the single source of truth for every
 * font-family in the Admin UI. Native system-font stacks only: no
 * Google/Adobe/web fonts, no network font request. This file loads
 * after vendors/main.css (see layouts/app.php / auth.php), so — same
 * specificity, later in the cascade — these declarations override that
 * bundle's own --bs-body-font-family/--bs-font-sans-serif/
 * --bs-font-monospace (including its "DM Sans" values) everywhere,
 * without editing the vendor file itself, per guide.md's CSS
 * Architecture section ("never edit main.css directly; override
 * variables here instead"). The one exception is main.css's own Google
 * Fonts @import, which a cascade override can't cancel — see that
 * file's matching comment at the @import site. Icon fonts (Line
 * Awesome, Remixicon, Bootstrap Icons, Font Awesome, Material Icons,
 * etc.) are untouched; they set their own font-family per glyph class
 * and never read these variables. */
:root {
  --font-ui-sans:
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI Variable",
    "Segoe UI",
    Roboto,
    "Helvetica Neue",
    Helvetica,
    Arial,
    sans-serif;

  --font-ui-mono:
    ui-monospace,
    SFMono-Regular,
    "SF Mono",
    Cascadia Code,
    Consolas,
    "Liberation Mono",
    Menlo,
    monospace;

  --bs-font-sans-serif: var(--font-ui-sans);
  --bs-font-monospace: var(--font-ui-mono);
  --bs-body-font-family: var(--font-ui-sans);
}

/* Brand color parity pass — admin-ui and dev-ui share the exact same
 * vendor template but each has its own hardcoded brand color baked into
 * its own copy of vendors/main.css (admin: teal #007D88, dev: slate
 * #3A454D). Per direct instruction to bring admin-ui's brand identity to
 * dev-ui's exact visual reference, every value below is copied verbatim
 * from dev-ui/vendors/main.css's own generated `:root`/[data-bs-theme=
 * dark] blocks (confirmed by direct diff, not recomputed) — this is a
 * pure color-value swap, not a new design; only the properties dev-ui's
 * own markup/CSS actually consumes are overridden (no unused `--bs-teal*`
 * family — confirmed no `.bg-teal`/`.text-teal`/`.btn-teal` utility class
 * appears anywhere in either app's real markup, only `bg-primary-light-N`
 * classes, e.g. the login hero panel — hence the `--bs-bg-primary-*` ramp
 * below). vendors/main.css is never hand-edited directly (guide.md's CSS
 * Architecture convention) — this file loads after it, so an identical
 * `:root`/[data-bs-theme=dark] selector here wins by cascade order alone,
 * same mechanism the font-family override above already uses.
 *
 * One value deliberately NOT ported: dev-ui's light-mode
 * `--bs-soft-light: #3A454D` — every other soft-* token is a genuine
 * light neutral gray (dev's own dark-mode `--bs-soft-light` is a normal
 * dark gray too), so a *light*-mode "soft light" background literally
 * equal to the raw brand hex reads as a generator anomaly specific to
 * dev-ui's own build, not an intentional design choice — admin-ui keeps
 * its own correct neutral gray for this one token rather than
 * reproducing what looks like a dev-ui bug. */
:root {
  --bs-text-primary: #3A454D;
  --bs-primary: #3A454D;
  --bs-primary-rgb: 40, 52, 61;
  --bs-primary-light-rgb: 141, 147, 152;
  --bs-link-color: #3A454D;
  --bs-link-color-rgb: 58, 69, 77;
  --bs-link-hover-color: rgb(61.2, 156.2, 164.56);
  --bs-link-hover-color-rgb: 61, 156, 165;
  --bs-code-color: #3A454D;
  --bs-focus-ring-color: rgba(58, 69, 77, .25);
  --bs-primary-text-emphasis: rgb(40.6, 48.3, 53.9);
  --bs-primary-bg-subtle: rgba(58, 69, 77, .15);
  --bs-primary-border-subtle: rgb(140.74, 147.12, 151.76);
  --bs-soft-primary: rgb(239.24, 240.12, 240.76);
  --hk-select-primary: rgb(239.24, 240.12, 240.76);
  --hk-select-primary-rgb: 239, 240, 241;
  --hk-menu-item-active-bg: rgba(40, 52, 61, .15);
  --hk-menu-item-active-text: #3A454D;
  --bs-bg-primary: #3A454D;
  --bs-bg-primary-light-5: rgb(239.24, 240.12, 240.76);
  --bs-bg-primary-light-4: rgb(211.66, 214.08, 215.84);
  --bs-bg-primary-light-3: rgb(176.2, 180.6, 183.8);
  --bs-bg-primary-light-2: rgb(140.74, 147.12, 151.76);
  --bs-bg-primary-light-1: rgb(105.28, 113.64, 119.72);
  --bs-bg-primary-dark-1: rgb(52.2, 62.1, 69.3);
  --bs-bg-primary-dark-2: rgb(46.4, 55.2, 61.6);
  --bs-bg-primary-dark-3: rgb(40.6, 48.3, 53.9);
  --bs-bg-primary-dark-4: rgb(34.8, 41.4, 46.2);
  --bs-bg-primary-dark-5: rgb(29, 34.5, 38.5);
}

/* Same brand color parity pass, dark theme — values copied verbatim from
 * dev-ui/vendors/main.css's own [data-bs-theme=dark] block. A separate
 * selector block from the existing dark-palette override below (rather
 * than merged into it) so that block's own comment — which correctly
 * documents itself as neutral-surface-only — stays accurate; CSS allows
 * multiple [data-bs-theme=dark] blocks and this one adds new properties
 * without conflicting with anything the other one sets. */
[data-bs-theme="dark"] {
  --bs-primary: #3A454D;
  --bs-primary-rgb: 58, 69, 77;
  --bs-primary-light-rgb: 141, 147, 152;
  --bs-link-color: rgb(140.74, 147.12, 151.76);
  --bs-link-color-rgb: 141, 147, 152;
  --bs-link-hover-color: rgb(176.2, 180.6, 183.8);
  --bs-link-hover-color-rgb: 176, 181, 184;
  --bs-code-color: rgb(176.2, 180.6, 183.8);
  --bs-focus-ring-color: rgba(141, 147, 152, .35);
  --bs-primary-text-emphasis: rgb(176.2, 180.6, 183.8);
  --bs-primary-bg-subtle: rgba(141, 147, 152, .18);
  --bs-primary-border-subtle: rgba(141, 147, 152, .3);
  --bs-soft-primary: rgba(141, 147, 152, .18);
  --hk-select-primary: rgba(141, 147, 152, .18);
  --hk-select-primary-rgb: 141, 147, 152;
  --hk-menu-item-active-bg: rgba(141, 147, 152, .18);
  --hk-menu-item-active-text: rgb(176.2, 180.6, 183.8);
  --bs-bg-primary: #3A454D;
  --bs-bg-primary-light-5: rgb(37.76, 38.64, 41.12);
  --bs-bg-primary-light-4: rgb(40.84, 43.26, 46.58);
  --bs-bg-primary-light-3: rgb(44.8, 49.2, 53.6);
  --bs-bg-primary-light-2: rgb(48.76, 55.14, 60.62);
  --bs-bg-primary-light-1: rgb(52.72, 61.08, 67.64);
  --bs-bg-primary-dark-1: rgb(52.2, 62.1, 69.3);
  --bs-bg-primary-dark-2: rgb(46.4, 55.2, 61.6);
  --bs-bg-primary-dark-3: rgb(40.6, 48.3, 53.9);
  --bs-bg-primary-dark-4: rgb(34.8, 41.4, 46.2);
  --bs-bg-primary-dark-5: rgb(29, 34.5, 38.5);
}

/* Brand rollout (#3A454D) dark-mode fix, ported from dev-ui verbatim:
 * vendors/main.css's `.btn-primary` sets --bs-btn-hover-bg/--bs-btn-
 * active-bg to shades *darker* than --bs-primary — correct in light mode,
 * but #3A454D is already close to this theme's own near-black surfaces
 * (Level 2 card ~#242426), so darkening it further on hover/active makes
 * the button read as *less* visible than its resting state — confirmed
 * live: resting rgb(58,69,77) vs card rgb(36,36,38) is a clearer 22-39
 * per-channel gap than hover's darkened rgb(52,62,69), which closes that
 * gap to 16-31. `.btn-primary` itself has no theme scoping (one rule,
 * both themes), so this is a small, theme-scoped override rather than a
 * change to the base rule. Hover/active now *lighten* instead (the only
 * direction that stays visible against a near-black resting state), and
 * the resting state gets a subtle light border it doesn't have in light
 * mode, so the button reads as a defined surface against the card even
 * before it's interacted with. */
[data-bs-theme="dark"] .btn-primary {
  --bs-btn-border-color: rgba(255, 255, 255, .14);
  --bs-btn-hover-bg: rgb(105.28, 113.64, 119.72);
  --bs-btn-hover-border-color: rgb(105.28, 113.64, 119.72);
  --bs-btn-active-bg: rgb(140.74, 147.12, 151.76);
  --bs-btn-active-border-color: rgb(140.74, 147.12, 151.76);
}

/* Enterprise surface-border refinement (2026-07-23) — light theme only,
 * background/palette untouched. vendors/main.css's --hk-border-primary
 * (rgb(233.66, 233.66, 233.66), ~#e9e9e9) is the one variable behind
 * every major surface edge in the app — .card, .table cell borders,
 * .hk-navbar's bottom border, .hk-menu's (sidebar) right border,
 * .hk-footer's top border, and assorted dividers (75 references) — but
 * at ~92% lightness on a white page it reads as barely-there, which is
 * why white-on-white surfaces (a card sitting on the page, a table
 * sitting in a card) can look soft/washed out rather than deliberately
 * separated. Deepening just this one token to a fully neutral #e0e0e0
 * (no hue shift, so it stays "crisp gray," never "blue-gray") sharpens
 * every one of those edges at once — cards read as intentional surfaces,
 * table rows/columns scan more easily, the sidebar/header/footer read as
 * bounded regions — without adding a single new color anywhere or
 * touching --hk-border-secondary (form-control borders, already a
 * deliberately darker #d8d8d8, left alone) or --hk-border-tertiary
 * (near-invisible dividers, unaffected). Dark theme's own
 * --hk-border-primary (main.css's [data-bs-theme=dark] block, #35393F)
 * is untouched — this selector only matches in light mode. */
[data-bs-theme="light"] {
  --hk-border-primary: #e0e0e0;
}

/* Dark-theme neutral palette (2026-07-27, simplified 2026-07-27) —
 * ChatGPT-desktop-inspired soft charcoal, applied app-wide (sidebar,
 * navbar, footer, cards, tables, forms, dropdowns, modals, pagination —
 * anything that reads one of these tokens) by overriding the same custom
 * properties vendors/main.css's own [data-bs-theme=dark] block already
 * defines, not by adding new component-specific selectors. admin.css
 * loads after vendors/main.css, so this wins at equal specificity/
 * selector with no `!important`, the same mechanism as the light-theme
 * border override directly above.
 *
 * Consolidated to exactly two levels, per direct feedback that four
 * closely-spaced steps still read as "several different grays" rather
 * than one cohesive surface: every application-chrome surface — body,
 * sidebar (--hk-menu-bg), navbar, footer, and the dashboard's own boxed
 * workspace container — is now the *same* `--bs-body-bg` value (Level 1),
 * and every raised surface — cards, tables, modals, dropdowns, widgets —
 * is `--hk-bg-primary` (Level 2, the card color, kept exactly as-is per
 * explicit "I like the current card color, use it as the reference"
 * direction). `.hk-navbar`/`.hk-footer` needed their own small override
 * below (see that rule's comment) since vendors/main.css hardwires both
 * of them to `--hk-bg-primary` directly — that token is now reserved
 * purely for Level 2, so pulling chrome bars back to Level 1 without also
 * dragging cards down with them requires overriding those two selectors
 * directly, not just the shared variable. A third, small "utility" shade
 * (`--bs-tertiary-bg`) remains for hover/active feedback *within* a
 * Level-2 surface (dropdown-item hover, table row hover/active, toggle
 * tracks) — not a competing background level, just enough contrast for
 * those states to stay visible against the card they sit on; see that
 * variable's own comment below for why it can't simply equal Level 2.
 * Borders are translucent white overlays (rgba(255,255,255,X)) rather
 * than a flat hex, so the same border reads correctly on either level.
 * Status/brand colors (--bs-primary/success/warning/danger and every
 * --hk-menu-item-active-* token) are untouched — this only refines
 * neutral surface/border/muted-text tokens. Nothing here touches light
 * theme. */
[data-bs-theme="dark"] {
  /* Level 1 — application background. Every piece of chrome (sidebar,
   * navbar, footer, the dashboard's boxed workspace container) is pinned
   * to this exact same value below rather than getting a shade of its
   * own, so there is only ever one "page" tone in the whole app. */
  --bs-body-bg: #19191a;
  --bs-body-bg-rgb: 25, 25, 26;
  --hk-menu-bg: var(--bs-body-bg);

  /* Level 2 — the one raised-surface tone (cards, tables, modals,
   * dropdowns, widgets). This is the color already in use and explicitly
   * kept as-is — every other token in this file is defined relative to
   * it, not the other way around. */
  --hk-bg-primary: #242426;
  --hk-bg-primary-rgb: 36, 36, 38;

  /* Utility shade for hover/active *states on* a Level-2 surface — a
   * dropdown-item's hover highlight, a table row's hover/active
   * background, a form-switch track. This can't simply equal Level 2
   * (--hk-bg-primary): a hover state that's the exact same color as the
   * card it's inside of would be invisible, and pagination/list-group
   * hover (vendors/main.css) already reads `--bs-tertiary-bg` for
   * exactly this purpose. One small shade, reused everywhere a
   * near-Level-2 highlight is needed, rather than several unrelated
   * grays for the same job — --hk-bg-hover, --hk-bg-secondary and
   * --hk-bg-tertiary below all resolve to it instead of carrying their
   * own separate values. */
  --bs-tertiary-bg: #2a2a2d;
  --bs-tertiary-bg-rgb: 42, 42, 45;
  --hk-bg-hover: var(--bs-tertiary-bg);
  --bs-secondary-bg: var(--bs-tertiary-bg);
  --bs-secondary-bg-rgb: 42, 42, 45;
  --hk-bg-secondary: var(--bs-tertiary-bg);
  --hk-bg-tertiary: var(--bs-tertiary-bg);

  /* Every table (components/table.php's `.table.table-hover`) paints its
   * own background from --bs-table-bg, which vendors/main.css's `.table`
   * sets to var(--bs-body-bg) — inert while page and card were the same
   * shade, but now that cards (Level 2) are visibly lighter than the
   * page (Level 1), that default would paint a visibly *darker*
   * rectangle inside every card — exactly the "patchy" seam this pass is
   * meant to avoid. Transparent lets a table always match whichever
   * surface it's actually placed on instead. */
  --bs-table-bg: transparent;

  /* Borders — translucent white overlays instead of one flat hex, so one
   * border color reads correctly against either level above rather than
   * needing a different shade per surface. */
  --hk-border-primary: rgba(255, 255, 255, .08);
  --bs-border-color: rgba(255, 255, 255, .08);
  --hk-menu-border: rgba(255, 255, 255, .08);
  --hk-border-secondary: rgba(255, 255, 255, .16);
  --hk-border-tertiary: rgba(255, 255, 255, .05);
  --bs-border-color-translucent: rgba(255, 255, 255, .1);

  /* Shadow color (2026-07-27, glow audit): vendors/main.css's own
   * [data-bs-theme=dark] block sets --bs-shadow-color and --hk-shadow-rgb
   * to a *white* RGB triplet (255, 255, 255) — inconsistent with every
   * other shadow token in that same block (--bs-box-shadow/-sm/-lg all
   * stay black-based rgba(0,0,0,...) in dark theme). Every rule that reads
   * either variable renders as a pale halo instead of depth once the
   * surface behind it is dark — and this pair backs far more than the
   * dashboard cards already fixed above: the base `.card` shadow used
   * app-wide (every list/detail page's cards), `.card.card-shadow`,
   * `.dropdown-menu`/`.popover` (account menu, notifications, table row
   * actions, tooltips), the ApexCharts tooltip, `.btn-floating`, and the
   * resizable-panel shadows (several declared `!important`). Overriding
   * the variable here fixes all of them at the shared source in one place
   * — including the `!important` cases, since supplying a different value
   * for a var() doesn't require outranking `!important` on the property,
   * only changing what the variable resolves to. Matches the black already
   * used by --bs-box-shadow-sm etc. so every shadow in dark theme is now
   * the same low-opacity black, never a light color. Light theme's copy of
   * both variables (near-black already) is untouched. */
  --bs-shadow-color: 0, 0, 0;
  --hk-shadow-rgb: 0, 0, 0;

  /* Muted text softened so secondary/tertiary copy (captions, labels,
   * placeholder-weight text) doesn't compete with actual content —
   * primary body/heading text (--bs-body-color, --bs-emphasis-color) is
   * left exactly as-is; it was already a comfortable off-white, never a
   * stark #fff, so it needed no change. */
  --bs-secondary-color: rgba(255, 255, 255, .55);
  --bs-tertiary-color: rgba(255, 255, 255, .35);
}

/* Audit follow-up (2026-07-27): modals and dropdown menus were the one
 * remaining background layer still sitting at the *old*, undifferentiated
 * darkness rather than the new palette above. `.modal-content` reads
 * `--bs-modal-bg`, and `.dropdown-menu` reads `--bs-dropdown-bg` — both
 * are Bootstrap component tokens declared *inside* their own selector
 * (`.modal{--bs-modal-bg:var(--bs-body-bg)}`,
 * `.dropdown-menu{--bs-dropdown-bg:var(--bs-body-bg)}`, vendors/main.css),
 * not at `:root`/`[data-bs-theme]`, so the root-level `--bs-body-bg`
 * override above can't reach them directly — they need their own small
 * override, same as any other component-scoped Bootstrap variable. Before
 * this pass, page and card were the same shade, so a modal or dropdown
 * matching the page looked correct by accident; now that cards are
 * visibly lighter (Level 2), every "Create/Edit" modal and every dropdown
 * in the app (account menu, notifications, table row actions) would still
 * render at page-level (Level 1) darkness — the one place left where the
 * old flat look survives, and, being how most create/edit flows and menus
 * in this app work, arguably the most *visible* one. A floating overlay
 * is conceptually a raised surface like a card, so both are lifted to the
 * same Level-2 tone (`--hk-bg-primary`) rather than getting a level of
 * their own. Scoped to dark theme only — light theme's cards are
 * deliberately flat white and were never part of this complaint. */
[data-bs-theme="dark"] .modal-content {
  --bs-modal-bg: var(--hk-bg-primary);
}

[data-bs-theme="dark"] .dropdown-menu {
  --bs-dropdown-bg: var(--hk-bg-primary);
}

/* Chrome consistency (2026-07-27): vendors/main.css hardwires both the
 * top navbar (`.hk-wrapper .hk-navbar{background:var(--hk-bg-primary)}`)
 * and the simple footer (`[data-footer=simple] .hk-footer{background:
 * var(--hk-bg-primary)}`) directly to the *card* token — reasonable while
 * page and card were one shade, but `--hk-bg-primary` is now reserved
 * purely for Level 2 (see the root block's own comment), so left alone
 * these two chrome bars would keep popping to card-level lightness while
 * the sidebar right next to them (`--hk-menu-bg`, now Level 1) stays
 * page-dark — a visible seam exactly where navbar meets sidebar. Both are
 * chrome, not raised content surfaces, so both get pulled back to
 * Level 1 to match the sidebar. Equal specificity to the vendor rules
 * being overridden (one attribute selector + one class each), so this
 * wins on source order alone — see the light-theme border override at
 * the top of this file for the same mechanism. */
[data-bs-theme="dark"] .hk-navbar {
  background: var(--bs-body-bg);
}

[data-bs-theme="dark"] .hk-footer {
  background: var(--bs-body-bg);
}

/* .hk-pg-wrapper and .hk-pg-body (layouts/app.php — .hk-pg-wrapper is the
 * outer wrapper around .hk-pg-body, which in turn wraps .container-fluid/
 * #uh-page-body; together they're the whole main-content region behind
 * every page's content) have no background rule of their own anywhere in
 * vendors/main.css, so both were relying entirely on inheriting `body`'s
 * own `background-color: var(--bs-body-bg)` — correct in practice, but
 * implicit: nothing here documented that either element was actually part
 * of the theme system, so a real bug elsewhere could hide behind "it's
 * probably just inheriting fine." This makes that explicit for both — same
 * token, same Level-1 page tone, just stated directly on each element
 * rather than left to inheritance. No `!important`, no hardcoded literal
 * color: both now participate in the theme the same way every other
 * surface in this file does. */
.hk-pg-wrapper,
.hk-pg-body {
  background-color: var(--bs-body-bg);
}

/* Financial values (wallet balances, transaction amounts, KPI figures,
 * table numerics) get fixed-width digits so stacked numbers align —
 * harmless no-op on non-numeric text, so applied app-wide rather than
 * requiring every amount call site to opt in. `button` is listed
 * alongside `body` because form controls don't inherit font-variant/
 * font-feature-settings from an ancestor the way normal elements do —
 * Bootstrap's Reboot only forces font-family/font-size/line-height back
 * to `inherit` on button/input/select/textarea, so a bare <button>
 * silently keeps the UA-default `normal` instead of picking up
 * tabular-nums from here. That one font-feature difference is also what
 * was shifting header-icon <button>s (e.g. layouts/header.php's
 * #theme-toggle, the mobile .navbar-toggle) a few px off the baseline
 * that vertical-align:middle computes for the sibling <a>-based icon
 * buttons — confirmed by a live tag-swap test (swapping an <a> icon to
 * <button> reproduced the offset; swapping a <button> icon to <a> fixed
 * it, with no other CSS involved). Listing `button` here fixes both the
 * original numeric-alignment gap and that baseline mismatch in one
 * place, rather than patching #theme-toggle in isolation. */
body,
button {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

/* Icon-button hover blob covering the icon — every `.btn.btn-icon` that
 * also carries a `btn-flush-*` variant (theme toggle, notifications
 * bell, audit log, mobile menu toggle, sidebar collapse toggle —
 * layouts/header.php, layouts/sidebar.php) gets its hover/focus/active
 * feedback from vendors/main.css's `.btn.btn-icon[class*=btn-flush-]`
 * `::before`: a 0×0 circle at the button's center that grows to
 * `top:0;left:0;height:100%;width:100%` on hover (main.css ~116085-
 * 116100), colored per variant — `--bs-bg-gray-light-5` for the
 * `flush-soft-hover` pairing every one of these buttons already uses
 * (~#f7f7f7 in light mode, ~#38403f-ish dark gray in dark mode; plain
 * `--bs-dark` — solid black — for `btn-flush-dark` on its own). Root
 * cause: that `::before` is `position:absolute`, while the icon
 * (`uh_icon()`'s bare `<svg>`, or a feather-icon `<i>` that
 * `feather.replace()` turns into an `<svg>` at runtime) is plain
 * in-flow, non-positioned inline content. Per the CSS painting-order
 * algorithm (CSS 2.1 Appendix E), positioned descendants at the same
 * z-index paint *after* — on top of — non-positioned in-flow content of
 * the same stacking context, regardless of source order, so the
 * fully-grown circle always ends up over the icon: a near-white disc in
 * light mode, a near-black one in dark mode, both fully hiding the
 * glyph rather than sitting behind it as the soft-hover treatment
 * intends. Lifting the icon into its own stacking position above the
 * `::before` (rather than touching main.css's colors/sizing/transition,
 * which stay exactly as designed) fixes every instance at once, in both
 * themes, without a per-page override. */
.btn.btn-icon[class*="btn-flush-"] svg {
  position: relative;
  z-index: 1;
}

/* Disable the vendor's collapsed-sidebar hover flyout (Phase: sidebar
 * persistence follow-up). vendors/main.css's `[data-layout=vertical]
 * .navbar-toggle` click handler (main-DXmtY0ym.js) sets
 * data-layout-style="collapsed" on .hk-wrapper, then — 250ms later —
 * data-hover="active"; every rule below is copied verbatim from
 * main.css's own `@media(min-width:1200px)` block, selector for
 * selector, so cascade order (this file loads after vendors/main.css —
 * see this file's header comment) wins at *equal* specificity, no
 * `!important` needed. Deliberately NOT touching data-hover itself
 * (e.g. via a MutationObserver stripping it): that attribute is also
 * the click handler's own precondition for the *un*-collapse branch
 * ("if collapsed && data-hover==='active', go back to default") — an
 * approach that suppressed data-hover would silently break "click the
 * toggle again to expand." Overriding only the *visual* properties the
 * flyout's :hover rule sets, while leaving that attribute's lifecycle
 * exactly as the vendor drives it, keeps expand-again, active-item
 * highlighting, submenu `.collapse` toggling and every transition
 * untouched — this only cancels the one animation this app doesn't
 * want. `.callout` (one of the vendor's own flyout targets) is skipped:
 * layouts/sidebar.php never renders it, so that vendor rule is still a
 * no-op here. `.brand-img` *used* to be in that same "no-op" category
 * (sidebar.php had no logo markup at all, just plain text) — now that the
 * sidebar has a real logo placeholder (two `.brand-img` children, see that
 * file), its matching flyout rule needs the same cancellation as
 * everything else in this block, added below. */
@media (min-width: 1200px) {
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"][data-hover="active"] .hk-menu:hover {
    width: 72px;
  }
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"][data-hover="active"] .hk-menu:hover .menu-content-wrap .menu-group .nav-header {
    display: none;
  }
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"][data-hover="active"] .hk-menu:hover .menu-content-wrap .menu-group .navbar-nav > .nav-item > .nav-link > *:not(.nav-icon-wrap) {
    visibility: hidden;
  }
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"][data-hover="active"] .hk-menu:hover .menu-content-wrap .menu-group .collapse.show {
    display: none;
  }
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"][data-hover="active"] .hk-menu:hover .menu-content-wrap .menu-gap {
    display: none;
  }
  /* The wordmark stays hidden even while the (now width-pinned-to-72px)
   * flyout is hovered — without this, main.css's own hover rule
   * (`display:inline`) would try to reveal a ~112px-wide wordmark inside a
   * column that never actually widens past 72px, since the rule above
   * already cancels the width expansion that vendor rule assumes. */
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"][data-hover="active"] .hk-menu:hover .menu-header > span .navbar-brand .brand-img:last-child {
    display: none;
  }

  /* Collapsed rail: no horizontal scrolling.
   *
   * The nav labels stay in flow when the rail collapses — main.css hides
   * them with `visibility:hidden`, which removes them visually but keeps
   * their box, so a ~110px label still contributes width inside a 72px
   * column. `.nicescroll-bar` carries `overflow-x:auto`, so that surplus
   * became a real horizontal scrollbar: measured on a 1440px viewport,
   * the collapsed rail scrolled 108px sideways (scrollWidth 179 against
   * clientWidth 71), dragging the icons out of alignment. Expanded, the
   * same container measured 269/269 and scrolled 0, which is why this
   * only ever showed up collapsed.
   *
   * Clipping is safe here specifically BECAUSE of the block above: the
   * hover flyout is deliberately disabled, so the collapsed rail never
   * widens and no label is ever meant to be readable in this state.
   * Nothing visible is cut off — only the invisible overhang that made
   * the rail scrollable.
   *
   * overflow-y is left untouched so long menus still scroll vertically,
   * and this is scoped to the collapsed state so the expanded sidebar,
   * the mobile off-canvas menu (which uses the same attribute value
   * below 1200px with a different meaning) and every tooltip behave
   * exactly as before. */
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"] .hk-menu .nicescroll-bar {
    overflow-x: hidden;
  }

  /* Collapse-toggle visibility & placement (same follow-up).
   * vendors/main.css blurs .navbar-toggle to invisibility whenever
   * collapsed (`filter:blur(100px)`, unconditional — not gated by
   * :hover) and only ever reverses it inside the hover-flyout rule
   * this file already disables above, so with the flyout off the
   * button was left permanently blurred.
   *
   * .menu-header is a single fixed-height flex *row*
   * (display:flex;align-items:center;height:65px;overflow:hidden —
   * main.css ~127465), and the one <span> wrapping brand+toggle inside
   * it is *also* a single flex row (display:flex;justify-content:
   * space-between — ~127480): the two were always meant to sit
   * side-by-side on one line, never stacked, so there's no second row
   * for the toggle to occupy without either widening the (now 72px)
   * header or growing it vertically. Widening isn't an option (that's
   * the whole point of collapsed), so this switches .menu-header and
   * its <span> from a row to a *column* — same elements, same
   * `align-items:center` the vendor rule already sets (now centering
   * the column's cross-axis instead of its row cross-axis, for free),
   * just a different flex-direction — and relaxes the fixed 65px
   * height to `auto` so both rows fit without clipping. Brand stays
   * exactly where the expanded layout puts it (first child, untouched,
   * no display:none); the toggle becomes the second row directly below
   * it, centered in the 72px column the same way every nav icon below
   * it is — reading as the top of the icon rail rather than a
   * replacement for the logo. .hk-menu is itself `display:flex;
   * flex-flow:column` (main.css ~127324) with .menu-header as just its
   * first `flex:0 0 auto` item, so a taller header reflows the
   * scrollable nav list below it automatically — no fixed pixel
   * offsets anywhere in that relationship to break.
   *
   * Un-gated by :hover/data-hover, unlike the flyout block above — this
   * must hold constantly while collapsed, not just flash in on hover. */
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"] .hk-menu .menu-header {
    height: auto;
    padding-left: 0;
    padding-right: 0;
    padding-top: .75rem;
    padding-bottom: .75rem;
  }
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"] .hk-menu .menu-header > span {
    flex-direction: column;
    justify-content: center;
    gap: .5rem;
  }
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"] .hk-menu .menu-header .navbar-toggle {
    filter: none;
  }
}

/* Sidebar logo placeholder (2026-07-27) — sizes the *slot*, not the
 * artwork, so dropping in the real Ureh logo later (a similarly-sized
 * `<img>` or inline SVG in place of the two `.brand-img` spans in
 * layouts/sidebar.php) needs no further changes here. Icon chip ~2rem/32px
 * square (visible in both expanded and collapsed sidebar states — the
 * vendor's own `.brand-img:last-child` rule, and this file's matching
 * hover-flyout override above, are what hide the wordmark on collapse, not
 * this block); wordmark SVG's own `viewBox` fixes its ~112x32 footprint,
 * so this only needs to constrain its rendered height to match the icon. */
.uh-sidebar-brand {
  display: inline-flex;
  align-items: center;
  gap: .625rem;
}

.uh-sidebar-brand-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: .5625rem;
  background: rgba(var(--bs-primary-rgb), .12);
  color: var(--bs-primary);
  flex-shrink: 0;
}

.uh-sidebar-brand-mark {
  display: inline-flex;
  align-items: center;
  height: 2rem;
  color: var(--bs-primary);
}

.uh-sidebar-brand-mark svg {
  display: block;
  height: 100%;
  width: auto;
}

/* Official Ureh brand marks inlined by uh_brand_svg() (sidebar icon +
   wordmark). CSS sizes by height with width:auto so the source aspect
   ratio is never stretched. The collapsed rail shows the icon; the
   expanded rail shows the wordmark (the last .brand-img is hidden on
   collapse by the vendor rule). */
.uh-sidebar-brand-icon svg {
  display: block;
  width: 1.375rem;
  height: 1.375rem;
}

.uh-sidebar-brand-mark .uh-sidebar-brand-svg {
  display: block;
  height: 100%;
  width: auto;
}



/* Lightweight, reusable hover tooltip — assets/js/components/
 * sidebar-tooltip.js is the only thing that toggles `.uh-tooltip--visible`;
 * generic, not sidebar-specific (currently only wired to the collapsed
 * sidebar's nav-links, per the flyout-disable block above, but reusable
 * anywhere else a collapsed/icon-only control needs an on-hover label).
 * Single shared DOM node (see that file), repositioned per-target rather
 * than one node per icon, so this stays cheap regardless of sidebar
 * length.
 *
 * Built on vendors/main.css's own Bootstrap tooltip classes/tokens
 * (`.tooltip`/`.tooltip-inner`, ~main.css:5168) rather than new colors —
 * shipped in every page already, fully theme-aware, but unused anywhere
 * in this app until now (no `data-bs-toggle="tooltip"` exists). A
 * second, more specific rule further down that same file (~main.css:
 * 120983 — this app's own "Enterprise" template layer, so it wins the
 * cascade over Bootstrap's base version) is what actually supplies the
 * look: `background:var(--bs-foreground)` / `color:var(--hk-bg-primary)`
 * — a dark-slate-on-light chip in light mode, a white-on-near-black chip
 * in dark mode (both tokens flip in the `[data-bs-theme=dark]` block, so
 * this needs zero dark-mode-specific rules of its own) — plus
 * `border-radius:.5rem`. Its own `.tooltip-arrow{display:none}` (this
 * app's template doesn't use the classic CSS-triangle arrow anywhere)
 * is left alone — this component doesn't use that element at all,
 * building its own callout-style diamond pointer instead (below) via a
 * plain `::before` on `.tooltip-inner`, so there's no conflict with
 * that vendor rule either way. Class names only, no Bootstrap Tooltip
 * JS/Popper dependency: this component positions itself with a plain
 * getBoundingClientRect() call, since it only ever needs to sit beside
 * a fixed 72px-wide rail — Popper's dynamic collision/placement logic
 * has nothing to solve here.
 *
 * What this file adds on top of vendor's bare .tooltip/.tooltip-inner:
 * `position:fixed` (vendor's version assumes Popper sets
 * `position:absolute` itself; this component has no Popper instance, so
 * it needs its own fixed-position placement instead), slightly more
 * generous padding (`.375rem`/`.75rem` — this file's own already-
 * established spacing increments, e.g. `.uh-dashboard-kpi .card-body`
 * above), a soft shadow (`--bs-box-shadow-sm`, the same subtle-
 * elevation token `.dropdown-menu` builds its own shadow from), the
 * fade+slide transition (`prefers-reduced-motion` respected, the same
 * convention main.css's own `.fade` utility already follows elsewhere
 * in this app), and the diamond pointer itself: an 8px square,
 * `background:inherit` (copies `.tooltip-inner`'s own computed
 * background — the same theme-aware `--bs-foreground` token, so it's
 * always in sync with the box in both themes with no color of its own
 * to keep in sync by hand), rotated 45deg and pulled half-outside the
 * box's left edge — a callout pointer (Linear/Notion/Raycast-style)
 * rather than the vendor's classic CSS-triangle arrow. */
.uh-tooltip.tooltip {
  position: fixed;
  z-index: 1071;
  opacity: 0;
  transform: translateY(-50%) translateX(-6px);
  transition: opacity .15s ease, transform .15s ease;
  pointer-events: none;
}
.uh-tooltip.tooltip.uh-tooltip--visible {
  opacity: 1;
  transform: translateY(-50%) translateX(0);
}
.uh-tooltip .tooltip-inner {
  position: relative;
  padding: .375rem .75rem;
  box-shadow: var(--bs-box-shadow-sm);
}
.uh-tooltip .tooltip-inner::before {
  content: "";
  position: absolute;
  top: 50%;
  left: -4px;
  width: 8px;
  height: 8px;
  background: inherit;
  border-radius: 1px;
  transform: translateY(-50%) rotate(45deg);
}
@media (prefers-reduced-motion: reduce) {
  .uh-tooltip.tooltip {
    transition: none;
  }
}

/* Table Responsiveness & Header UI Refinement pass — docs/ui-table-audit.md.
 * Reusable column-width hooks so Status/Amount/Date/Reference/Actions read
 * as the same width across every table in the app rather than each table's
 * own content organically sizing them. Applied to <th> (components/table.php)
 * and mirrored onto the matching <td> (assets/js/components/table.js) since
 * table-layout stays the Bootstrap default (auto) — a plain width hint, not
 * a hard constraint, so long content still isn't clipped without warning.
 * Amount also right-aligns, which reads naturally alongside the app-wide
 * tabular-nums rule above. */
.uh-col-status { width: 110px; }
.uh-col-amount { width: 130px; text-align: right; }
.uh-col-date { width: 130px; }
.uh-col-reference { width: 170px; }
.uh-col-actions { width: 90px; }
/* Row-selection checkbox column (Reconciliation workspace) — narrow,
 * centered, never a wrap/scroll driver. Same "width hint, not a hard
 * constraint" contract as the hooks above. */
.uh-col-select { width: 40px; text-align: center; }

/* Bulk action toolbar (Reconciliation workspace). Reads its surface and
 * border from the same theme tokens the rest of the app uses, so it
 * follows light/dark automatically with no per-mode overrides of its
 * own. */
.uh-recon-bulkbar {
  background-color: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
}

/* Truncation for the small set of genuinely-unbounded free-text columns
 * (Description, Message, Reason, User Agent, URL, Value, ...) — paired
 * with Bootstrap's own .text-truncate (already compiled into main.css:
 * overflow:hidden;text-overflow:ellipsis;white-space:nowrap), which needs
 * a max-width to actually trigger. Each call site also sets title="" on
 * the same element for a native hover tooltip, so the full value is never
 * actually lost, just not shown inline. */
.uh-table-truncate {
  max-width: 260px;
  display: inline-block;
  vertical-align: bottom;
}

/* Consistent gap between a row's primary action and its "⋯" overflow
 * menu (assets/js/components/table.js's actionsMenu()) — previously two
 * inline elements with no explicit spacing, relying on incidental
 * whitespace in the template string. */
.uh-row-actions {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  white-space: nowrap;
}

/* Row-actions overflow dropdown ("⋯", actionsMenu()) clipped by its own
 * table wrapper — components/table.php always renders the table inside
 * .table-responsive (vendors/main.css: overflow-x:auto, overflow-y left
 * at its visible default), but a browser computes an unset overflow-y to
 * auto as soon as overflow-x is anything other than visible — so any
 * dropdown-menu that would extend below the wrapper's own bottom edge
 * (any row near the end of a short table, confirmed live on
 * clients/profile.php's KYC Documents table) got clipped mid-menu instead
 * of floating above the fold. overflow-x stays auto (wide tables still
 * scroll horizontally); only overflow-y is freed so the dropdown can
 * escape vertically. */
.table-responsive {
  overflow-y: visible;
}

/* Row-actions overflow toggle ("⋯") — the same flat, washed-out hover
 * block dev-ui's own dev.css already documents and fixes for
 * [data-action="mark-read"]/[data-action="replay"] (vendors/main.css's
 * flush-soft-hover :before fill reads as a filled rectangle rather than
 * a deliberate hover state at small icon-button sizes, confirmed live).
 * This toggle has no unique data-action of its own (it opens a menu, it
 * isn't itself a row action), so it's scoped to .uh-row-actions'
 * dropdown-toggle instead of one data-action value — every actionsMenu()
 * overflow toggle across admin-ui picks this up from one rule, not a
 * per-page fix. */
.uh-row-actions .dropdown-toggle.flush-soft-hover:hover,
.uh-row-actions .dropdown-toggle.flush-soft-hover:focus {
  background-color: transparent !important;
  border-color: transparent !important;
}

.uh-row-actions .dropdown-toggle.flush-soft-hover:hover:before,
.uh-row-actions .dropdown-toggle.flush-soft-hover:focus:before {
  background: transparent !important;
}

/* Row-actions overflow menu items (Replacement/Delete/Reset/... —
 * actionsMenu()'s own dropdown-item markup) at Bootstrap's stock 1rem
 * font-size / 1rem-x .25rem-y padding read oversized next to this app's
 * own compact 14px/12px scale everywhere else (buttons, table cells,
 * card headers). Set once via the dropdown's own --bs-dropdown-* custom
 * properties (Bootstrap's own supported override point, not a specificity
 * fight against vendors/main.css) so every actionsMenu() overflow menu
 * app-wide picks up the tighter sizing, not just one page's table. */
.uh-row-actions .dropdown-menu {
  --bs-dropdown-min-width: 8.5rem;
  --bs-dropdown-font-size: .8125rem;
  --bs-dropdown-item-padding-x: .75rem;
  --bs-dropdown-item-padding-y: .375rem;
}

/* Remove (clients/profile.php's KYC Documents row actions — cancels a
 * document request with no file yet) — Bootstrap's stock .dropdown-item
 * hover fill (--bs-dropdown-link-hover-bg, a solid tertiary-bg block) is
 * the right call for a normal menu choice, but this is the one item in
 * the menu that's purely a "never mind" — a filled hover block on it
 * reads as if it's being highlighted for selection rather than backed
 * out of. `key: 'remove'` is unique to this one action across admin-ui
 * (confirmed via a repo-wide grep), so scoping by data-action here can't
 * affect any other page's dropdown item. No !important needed — this
 * selector already outweighs vendors/main.css's own `.dropdown-item:hover`
 * on specificity alone. */
.uh-row-actions [data-action="remove"].dropdown-item:hover,
.uh-row-actions [data-action="remove"].dropdown-item:focus {
  background-color: transparent;
}

/* Remove — same action, the OTHER shape actionsMenu() can render it in:
 * a requirement with no file yet has no other action to pair it with
 * (documentRowActions()), so it becomes the row's sole *primary* button
 * instead of a dropdown item — a plain .flush-soft-hover icon button,
 * confirmed live to have no LABEL_ICON entry of its own so it renders as
 * the bare text "Remove". Same flat washed-out hover-fill dev-ui's own
 * dev.css already documents and fixes for [data-action="mark-read"]/
 * [data-action="replay"] — this mirrors that exact pattern (including the
 * :before pseudo-element fill and the !important needed to beat it),
 * scoped to data-action="remove" for the same reason the dropdown-item
 * rule above is. */
[data-action="remove"].flush-soft-hover:hover,
[data-action="remove"].flush-soft-hover:focus {
  background-color: transparent !important;
  border-color: transparent !important;
}

[data-action="remove"].flush-soft-hover:hover:before,
[data-action="remove"].flush-soft-hover:focus:before {
  background: transparent !important;
}

/* Whole-row navigation (usability pass) — cursor affordance only; hover
 * highlight already comes from Bootstrap's own .table-hover, already on
 * every table via components/table.php, so no new hover style is added
 * here. */
.uh-row-clickable {
  cursor: pointer;
}

/* Filter-bar control widths — every list page's search/filter row
 * (e.g. transactions/index.php, clients/index.php) previously hardcoded
 * its own style="width:Npx" per control with no shared scale (220px,
 * 260px, 180px, 140px, 120px, 150px, 160px all appeared for
 * differently-sized versions of the same kind of control). One scale,
 * reused everywhere; still plain widths (not min-width) so controls keep
 * wrapping naturally on narrow/mobile viewports exactly as before. */
.uh-filter-xs { width: 120px; }
.uh-filter-sm { width: 150px; }
.uh-filter-md { width: 180px; }
.uh-filter-lg { width: 220px; }

/* components/form.php's uh_form_select() own width rule, moved from an
 * inline style to a class for the same reason as the filter widths above
 * — see that function's docblock for why min-width (not a fixed width)
 * is deliberate here. */
.uh-filter-select {
  width: auto;
  min-width: 170px;
}

/* A filter <select> that its page has deliberately WRAPPED in one of the
 * width classes above fills that wrapper instead of sizing itself.
 *
 * .uh-filter-select's own width:auto sizes a <select> to its widest
 * OPTION, which is invisible on most list pages (short option text never
 * reaches the 170px floor) but breaks down once the options are long:
 * Support Inquiries' Country filter carries all 243 ISO country names,
 * and "British Indian Ocean Territory" rendered it at 228px beside two
 * 170px siblings. Measured in a real browser, not inferred.
 *
 * Scoped to the wrapped case on purpose. All 30 existing uh_form_select()
 * call sites render the select as a DIRECT flex child of the filter row
 * with no wrapper, so none of them match this selector and none change
 * width — verified before adding it. Wrapping is opt-in, and a page that
 * opts in is asking for a specific width by definition. min-width:0 is
 * what actually lets the wrapper win; without it the 170px floor would
 * still override a narrower wrapper. */
.uh-filter-xs > .uh-filter-select,
.uh-filter-sm > .uh-filter-select,
.uh-filter-md > .uh-filter-select,
.uh-filter-lg > .uh-filter-select {
  width: 100%;
  min-width: 0;
}

/* Opt-in filter row that mixes LABELLED controls (uh_form_field(), which
 * renders a `.form-group` with a label above the input) with UNLABELLED
 * ones (uh_form_select(), a bare <select>) on the same line.
 *
 * .form-group carries margin-bottom:1rem (vendors/main.css). With the row
 * bottom-aligned — which is what puts a label-less select on the same
 * baseline as a labelled input — that margin lifts every labelled control
 * exactly 16px above its unlabelled neighbours. Measured, not assumed.
 *
 * A separate class rather than a rule on .uh-filter-* itself, because
 * transactions/index.php also wraps uh_form_field()s in those width
 * classes and its own filter row is entirely labelled, so it is already
 * self-consistent and must not shift. Only a row that actually mixes the
 * two kinds needs this, and it says so by name. */
.uh-filter-bar .form-group {
  margin-bottom: 0;
}

/* Same reason, second source: uh_form_field() also reserves an empty
 * `.invalid-feedback` slot below its input, which carries margin-top:
 * .25rem (vendors/main.css) even while empty — 4px that likewise lifts a
 * labelled control off the row's shared bottom edge. A filter bar never
 * renders a validation message into that slot (these are filters, not a
 * submitted form), so the reserved space has nothing to reserve here.
 * Only the margin is zeroed, not the element: if a future filter ever
 * does write into it, the message still renders, just flush. */
.uh-filter-bar .uh-field-error {
  margin-top: 0;
}

/* Account dropdown (layouts/header.php) — scoped so these refinements
 * never leak into the header's other dropdown (navbar-search), which has
 * its own, unrelated markup. Brings the dropdown's text down to the
 * app-wide 14px scale (it was the last surviving 16px text in the
 * authenticated shell — --bs-dropdown-font-size defaults to 1rem) and
 * tightens item/divider spacing to match the rest of the compact shell,
 * all via the same --bs-dropdown-* custom properties main.css's own
 * .dropdown-menu rule already reads, not by fighting its specificity. */
.uh-account-dropdown {
  --bs-dropdown-font-size: .875rem;
  --bs-dropdown-item-padding-x: .75rem;
  --bs-dropdown-item-padding-y: .4rem;
  --bs-dropdown-divider-margin-y: .375rem;
  min-width: 230px;
}

/* display:flex + align-items:center replaces the per-icon position:relative;
 * top:2px/-2px offset hacks main.css's own .dropdown-icon rules use to
 * approximate vertical centering — flex centers the icon and label
 * against each other exactly, at any font-size, so the top overrides
 * directly below zero them out rather than compounding with flex. */
.uh-account-dropdown .dropdown-item {
  display: flex;
  align-items: center;
  border-radius: var(--bs-border-radius-sm);
  transition: background-color .15s ease-in-out, color .15s ease-in-out;
}

.uh-account-dropdown .dropdown-icon,
.uh-account-dropdown .dropdown-icon.feather-icon {
  top: 0;
  width: 18px;
  margin-right: .5rem;
}

.uh-account-dropdown .dropdown-header {
  padding-top: .5rem;
  padding-bottom: .25rem;
}

/* Phase F4-02: the vendored bundle's own :focus-visible rules
   (vendors/main.css) only cover .btn/.nav-link/.btn-link/.icon-link-hover —
   not .dropdown-item (F4-01's rebuilt account dropdown). A real,
   self-introduced gap, closed here rather than in the vendor bundle. */
.dropdown-item:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: -2px;
}

/* Phase F4-02: the skeleton->content swap (assets/js/components/loader.js's
   swapSkeleton()) fades content in rather than popping it, guarded the same
   way as the scroll-behavior rule above. */
.uh-fade-in {
  animation: uh-fade-in .2s ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
  .uh-fade-in {
    animation: none;
  }
}

@keyframes uh-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

.uh-file-drop {
  cursor: pointer;
  border-color: var(--bs-border-color) !important;
  transition: border-color .15s ease-in-out, background-color .15s ease-in-out;
}

.uh-file-drop:hover,
.uh-file-drop:focus-visible {
  border-color: var(--bs-primary) !important;
  background-color: var(--bs-primary-bg-subtle);
}

.uh-chart-summary {
  border-bottom: 1px solid var(--bs-border-color);
  padding-bottom: .5rem;
      font-size: 0.875rem !important; /* Adjust pixel/rem size as needed */
}

.uh-empty-state {
  color: var(--bs-secondary-color);
}

/* Compact, consistent form controls + denser typography across the
 * authenticated app shell — scoped off .hk-pg-auth (layouts/auth.php's
 * own wrapper class) so login/OTP is never touched, per that page being
 * out of scope. Values below are the vendor bundle's own .form-control-sm/
 * .form-select-sm numbers (vendors/main.css) applied as the app-wide
 * default, rather than requiring every call site to add -sm — this is
 * also what fixes filter bars mixing a plain .form-control search box
 * (previously 16px/38px tall) with a .form-select-sm dropdown (14px/31px)
 * side by side.
 *
 * Border/focus/hover treatment ported from dev-ui's own later polish pass
 * on this exact shared shell class: border tightens to the brand primary
 * color + a soft rgba ring on focus, a lighter border on hover — the
 * "enterprise-SaaS-style focus ring" already used on this app's own login
 * page, extended to every other authenticated page's inputs instead of
 * the vendor bundle's stock, non-brand #86b7fe/#0d6efd40 default, so both
 * areas share one visual language instead of two. Padding widened
 * slightly alongside it (4px/8px -> 6px/10px at the same .875rem font
 * size) so the text sits in a comfortably roomy box rather than reading
 * as cramped.
 */
.hk-wrapper:not(.hk-pg-auth) .form-control {
  min-height: calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));
  padding-top: .375rem;
  padding-bottom: .375rem;
  padding-left: .625rem;
  font-size: .875rem;
  border-color: var(--hk-border-primary);
  transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out, background-color .15s ease-in-out;
}

.hk-wrapper:not(.hk-pg-auth) .form-select {
  padding-top: .375rem;
  padding-bottom: .375rem;
  padding-left: .625rem;
  font-size: .875rem;
  border-color: var(--hk-border-primary);
  transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out, background-color .15s ease-in-out;
}

.hk-wrapper:not(.hk-pg-auth) .form-control:hover:not(:disabled):not(:focus),
.hk-wrapper:not(.hk-pg-auth) .form-select:hover:not(:disabled):not(:focus) {
  border-color: var(--hk-border-secondary);
}

.hk-wrapper:not(.hk-pg-auth) .form-control:focus,
.hk-wrapper:not(.hk-pg-auth) .form-select:focus {
  border-color: var(--bs-primary);
  box-shadow: 0 0 0 .2rem rgba(var(--bs-primary-rgb), .15);
}

.hk-wrapper:not(.hk-pg-auth) .form-control::placeholder {
  color: var(--bs-tertiary-color);
}

.hk-wrapper:not(.hk-pg-auth) .form-control:disabled,
.hk-wrapper:not(.hk-pg-auth) .form-select:disabled {
  background-color: var(--bs-tertiary-bg);
  opacity: .65;
}

/* Labels a touch of weight and the app's own standard muted tone — the
 * same "small, medium-weight, muted caption above a clearer value"
 * hierarchy already established for .uh-entity-header-label/
 * .uh-dashboard-section-title elsewhere in this app, applied to every
 * ordinary form label too. */
.hk-wrapper:not(.hk-pg-auth) .form-label {
  font-size: .875rem;
  font-weight: 500;
  color: var(--bs-secondary-color);
}

/* Sidebar nav text was rendering at the body's default 16px — every other
 * "small" text in the app (breadcrumb items, table headers, filter
 * selects) already sits at 14px, so this brings the sidebar in line with
 * that established scale rather than introducing a new size. */
.hk-menu .nav-link-text {
  font-size: .875rem;
}

/* Table body content (16px) was noticeably larger than its own header
 * row (14px, components/table.php's fs-8 header cells) — matching it
 * keeps rows visually calmer without touching header/badge sizing. */
.hk-wrapper:not(.hk-pg-auth) .table td {
  font-size: .875rem;
}

/* Portal-wide typography pass, ported from dev-ui — the one outlier this
 * app's own already-compact scale still had: every page's own title
 * (layouts/app.php's shared `<h4 class="mb-1">`) rendered at the vendor
 * bundle's raw h4 size (24px at desktop, ~21-22px on narrow viewports),
 * against a page built almost entirely out of the 14px tier established
 * above (breadcrumbs, card headers, sidebar, table, form controls) — a
 * page title nearly double its own breadcrumb's size read as oversized/
 * unbalanced rather than as deliberate hierarchy. `#uh-page-body` scoping
 * (rather than a bare `h4.mb-1`) keeps this from ever matching an
 * unrelated h4 a page's own $content might render. Breadcrumbs (14px)
 * move down one tier to the same 12px "metadata" size already used for
 * page subtitles/muted captions elsewhere on this same header. Card
 * headers, dashboard section titles, table/form text, and sidebar nav
 * are untouched — already at or below this same 14px/12px scale. */
#uh-page-body > div.mb-4 > h4.mb-1 {
  font-size: 1.25rem;
}

.hk-pg-header .breadcrumb {
  font-size: .75rem;
}

/* Dashboard redesign (docs/dashboard-redesign-plan.md) — scoped to
 * dashboard/index.php's own markup only (uh-dashboard-* prefix), so none
 * of this can leak onto or conflict with any other page's cards. */

/* Section headings (Executive Overview, User Overview, ...) — a
 * distinct tier from a card's own <h6> title, so "this is a page
 * section" reads differently from "this is one card's title". */
.uh-dashboard-section-title {
  font-size: .75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--bs-secondary-color);
  margin-bottom: .75rem;
 
}

/* Visual-polish pass (2026-07-27): every card on this page previously
 * carried whatever the vendor bundle happened to assign it — Quick
 * Actions (plain `.card`, no `card-border`) kept Bootstrap's own default
 * `box-shadow:0 2px 2px rgba(...,.1)`, while every KPI/data card
 * (`.card-border`, vendors/main.css) explicitly zeroes that shadow to
 * `none` — two different elevation treatments on one page — and both sat
 * at the vendor's flat `.5rem` corner radius, one step smaller than this
 * page's own `.75rem` workspace frame around them. One rule, scoped to
 * `.uh-dashboard-section .card` (every card on this page lives inside a
 * `.uh-dashboard-section` wrapper — Quick Actions included), gives all of
 * them the same quiet elevation and a radius that actually matches the
 * frame they sit in. `rgba(var(--bs-shadow-color),...)` reuses the exact
 * token the vendor's own `.card` shadow already reads (main.css) — that
 * variable flips from a dark to a light RGB triplet in
 * `[data-bs-theme=dark]`, which is exactly why dark theme needs its own
 * override below (a light-RGB shadow reads as a glow, not depth, on a
 * dark card). admin.css loads after vendors/main.css, so this wins over
 * both vendor rules at equal specificity without `!important`.
 *
 * White-surface revision (2026-07-27, follow-up): the workspace frame
 * behind these cards no longer carries any background tint in light
 * theme (see `.uh-dashboard-workspace` below), so this shadow is the only
 * thing left doing the work of separating a card from its surroundings.
 * A third, wider/softer layer was added — still each individually
 * low-opacity — so depth still reads clearly on a flat white canvas
 * rather than depending on a tinted background to make the edge visible. */
.uh-dashboard-section .card {
  border-radius: .75rem;
  box-shadow:
    0 1px 2px rgba(var(--bs-shadow-color), .04),
    0 2px 4px rgba(var(--bs-shadow-color), .05),
    0 10px 20px rgba(var(--bs-shadow-color), .05);
}

/* Dark-theme refinement (2026-07-27, second follow-up): the shadow above
 * reads `--bs-shadow-color`, which flips to a *white* RGB triplet in dark
 * theme (vendors/main.css) — so on a dark card that same "subtle shadow"
 * recipe renders as a pale glow/halo around every edge instead of depth,
 * the opposite of the calm, integrated look enterprise dark dashboards
 * (Linear, Vercel, GitHub Enterprise, Datadog) go for. Dark mode already
 * gets its separation from the workspace's own tertiary-tint background
 * (`.uh-dashboard-workspace` below, left as-is for dark theme) plus each
 * card's existing 1px border (--hk-border-primary) — a shadow on top of
 * that is redundant and works against "integrated, not floating". Higher
 * specificity than the base rule above (attribute selector + two classes
 * beats two classes), so this wins regardless of source order with no
 * `!important` needed; the light theme is untouched by this selector. */
[data-bs-theme="dark"] .uh-dashboard-section .card {
  box-shadow: none;
}

.uh-dashboard-section .card .card-header:first-child {
  border-radius: calc(.75rem - 1px) calc(.75rem - 1px) 0 0;
}

/* Alignment/typography fix for the Active/Pending/Suspended and
 * Today/Successful/Pending/Failed/Needs Reconciliation pills (User &
 * Financial Overview) — `.badge-status` has no layout rule of its own
 * anywhere in vendors/main.css (only its `.badge-label` descendant does),
 * so the dot + label + value previously aligned on plain inline baseline,
 * sitting the small round dot noticeably low against the text next to it.
 * `badge-status` is used exclusively on this page (no other admin-ui page
 * references it), so this is scoped here rather than risking a global
 * badge behavior change. Label muted to secondary-color to match the rest
 * of the page's label/value hierarchy (label quiet, value — already
 * `fw-semibold` in the markup — the thing that's supposed to draw the
 * eye); dot/value colors are untouched. */
.badge-status {
  display: inline-flex;
  align-items: center;
}

.badge-status .badge-label {
  color: var(--bs-secondary-color);
}

/* The one KPI-tile template every tile in every section uses (icon +
 * label header row, then a full-width value, then an optional
 * caption/note line) — replaces the old page's two different ad hoc
 * tile treatments (fs-6 fw-bold vs fs-4 fw-medium) with a single
 * consistent one.
 *
 * UX pass (2026-07-23): the previous version sat the icon *beside* the
 * label+value stack in one flex row, so a large icon permanently ate
 * into the value's own width — on a real Naira balance
 * ("₦1,505,650.98") that meant the number wrapped mid-digit onto a
 * second line, which read as broken rather than dense. The fix is
 * structural, not a smaller font: the icon now sits in its own small
 * header row next to the label only (`.uh-dashboard-kpi-head`), and the
 * value is a block-level element with the *entire* card's content width
 * to itself, no icon competing for the space. Icon shrunk 40px -> 28px
 * to match its new, smaller role (a label-row accent, not something
 * sized to match the metric itself). */
.uh-dashboard-kpi-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--bs-border-radius);
  background-color: var(--bs-primary-bg-subtle);
  /* --bs-primary-text-emphasis, not --bs-primary itself: confirmed against
   * vendors/main.css that dark mode's plain --bs-primary (a darkened
   * teal, rgb(0,100,109)) sits too close in luminance to this chip's own
   * dark-mode background to stay clearly legible, while
   * --bs-primary-text-emphasis (a lighter, more saturated teal,
   * rgb(107,180,186)) is exactly the token Bootstrap's own subtle-color
   * system computes for "readable text/icon on a *-bg-subtle surface" in
   * both themes — the correct token for this job regardless of hue. */
  color: var(--bs-primary-text-emphasis);
  flex-shrink: 0;
}

.uh-dashboard-kpi-icon--lg {
  width: 36px;
  height: 36px;
  background-color: var(--bs-primary);
  color: #fff;
}

/* Wallet Balance hero (visual-parity pass, ported from dev-ui's own
 * Stripe/Mercury/Brex-style balance card) — a larger, solid-filled icon
 * chip (.uh-dashboard-kpi-icon--lg above) and a faint radial-gradient
 * glow, scoped to the one Wallet Balance tile only. `#uh-kpi-wallet-total`'s
 * own font-size override (below) is an ID selector and keeps winning over
 * anything scoped here, so no value font-size rule is duplicated in this
 * block — only the glow/positioning plumbing that ID rule doesn't cover. */
.uh-dashboard-wallet-hero {
  position: relative;
  overflow: hidden;
}

.uh-dashboard-wallet-hero-glow {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: radial-gradient(circle at 100% 0%, rgba(var(--bs-primary-rgb), .08), transparent 55%);
  pointer-events: none;
}

.uh-dashboard-wallet-hero .card-body {
  position: relative;
  z-index: 1;
}

/* Skeleton sizing for the balance figure — Bootstrap's own .placeholder
 * (vendors/main.css) has no intrinsic width, so this just gives it one
 * roughly matching a real Naira amount. Lives *inside* #uh-kpi-wallet-total
 * as its initial content; the existing, unmodified setText() call
 * replaces it via .textContent the instant real data arrives. */
.uh-dashboard-wallet-skeleton {
  display: block;
  width: 9rem;
  height: 1.9rem;
  border-radius: .375rem;
}

/* Generic KPI-value skeleton — same recipe as the Wallet hero's own
 * .uh-dashboard-wallet-skeleton above, sized for the shared 1.5rem/fs-6
 * .uh-dashboard-kpi-value scale every other KPI tile on this page renders
 * at, so any tile can adopt it instead of a bare "—" as its initial
 * (pre-fetch) state. */
.uh-dashboard-kpi-value-skeleton {
  display: block;
  width: 3.5rem;
  height: 1.6rem;
  border-radius: .375rem;
}

/* Smaller sibling for the inline stat-pill counts (badge-status rows,
 * status-strip pills) that sit next to a label at body text size rather
 * than a full KPI tile's own 1.5rem value. */
.uh-dashboard-kpi-value-skeleton--sm {
  display: inline-block;
  width: 2rem;
  height: .875rem;
  border-radius: .25rem;
  vertical-align: middle;
}

/* Tabular figures + a touch of negative tracking — a deliberate, small
 * "considered numeric typography" detail real fintech balance displays
 * use (Stripe/Mercury): digits align to a fixed width (no jitter as the
 * value updates) and read slightly denser at this size. Applied directly
 * on a value element (not parent-scoped) so any monetary/numeric KPI on
 * this page can opt in. */
.uh-kpi-value-tabular {
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
}

/* Aspect-ratio pass (2026-07-23): Bootstrap's global `.card .card-body`
 * default (1.25rem/20px on every side) is tuned for content-heavy cards
 * (tables, charts) — on a KPI tile that's just an icon+label+value, that
 * much *vertical* padding on top of the content stack made the card read
 * as nearly square instead of the wide, flat "summary tile" shape
 * enterprise dashboards use (Stripe/Vercel/Linear). Horizontal padding
 * is left at a full 1rem so the card doesn't feel pinched side-to-side —
 * width was explicitly not the problem, only height. Scoped to
 * `.uh-dashboard-kpi`'s own card-body only, so every other card on the
 * page (Clients, Financial Summary, Transaction Volume, ...) keeps the
 * app-wide default padding untouched. */
.uh-dashboard-kpi .card-body {
  padding: .75rem 1rem;
}

/* Bug fix (Enterprise Detail Page pass): the icon-beside-value tile
 * layout transactions/detail.php's headline band uses (icon + a plain
 * wrapping <div> holding label/value, side by side in one flex row) is
 * older than the icon-above-value `.uh-dashboard-kpi-head`/-body restructure
 * dashboard/index.php's own tiles moved to (see that pass's comment above)
 * — transaction-detail.php's tiles were never migrated to it, so they
 * still have this gap: a flex item with no min-width sizes to its
 * content's *unwrapped* intrinsic width first, so `.uh-dashboard-kpi-value`'s
 * own `overflow-wrap: break-word` never gets a chance to apply — a long
 * reference/identifier pushes the tile (and the card around it) wider
 * than its column instead of wrapping, exactly the reported overflow bug.
 * `.uh-dashboard-kpi-content` on that wrapping <div> gives the fix a real
 * class instead of a per-tile inline style, reusable anywhere else this
 * exact tile shape appears (wallets/overview.php's balance tiles use it
 * too). flex: 1 1 auto so it still claims the row's remaining width
 * (matching its previous unstyled-div behavior) once it's allowed to
 * shrink. */
.uh-dashboard-kpi-content {
  min-width: 0;
  flex: 1 1 auto;
}

/* Quick Actions row (visual-parity pass, ported from dev-ui's own
 * enterprise redesign of the same section): replaces the previous flat
 * wrap of plain buttons with a horizontal list of full-width action
 * rows — icon chip (.uh-dashboard-kpi-icon, reused verbatim, not a
 * second chip shape), label + one-line description, trailing chevron.
 * `flex: 1 1 240px` fills the row evenly and reflows to 2-, then
 * 1-per-row as the viewport narrows, entirely from content width rather
 * than a breakpoint table. */
.uh-dashboard-quickaction-list {
  display: flex;
  flex-wrap: wrap;
  gap: .625rem;
}

.uh-dashboard-quickaction-item {
  display: flex;
  align-items: center;
  gap: .75rem;
  flex: 1 1 240px;
  padding: .75rem .875rem;
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  text-decoration: none;
  color: var(--bs-body-color);
  transition: background-color .15s ease-in-out, border-color .15s ease-in-out, transform .15s ease-in-out;
}

.uh-dashboard-quickaction-item:hover,
.uh-dashboard-quickaction-item:focus-visible {
  background-color: var(--bs-primary-bg-subtle);
  border-color: var(--bs-primary);
  color: var(--bs-body-color);
  transform: translateY(-1px);
}

.uh-dashboard-quickaction-item:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 1px;
}

.uh-dashboard-quickaction-icon {
  flex-shrink: 0;
}

.uh-dashboard-quickaction-text {
  display: flex;
  flex-direction: column;
  gap: .0625rem;
  min-width: 0;
  flex: 1 1 auto;
}

.uh-dashboard-quickaction-label {
  font-size: .8125rem;
  font-weight: 600;
  line-height: 1.2;
}

.uh-dashboard-quickaction-desc {
  font-size: .75rem;
  color: var(--bs-secondary-color);
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Hidden until hover/focus, then slides in from the left — the same
 * "this row reveals its own affordance on interaction" convention as
 * .table-hover elsewhere in this app, applied here with a directional
 * cue since this row's whole purpose is navigation. */
.uh-dashboard-quickaction-arrow {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--bs-secondary-color);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity .15s ease-in-out, transform .15s ease-in-out, color .15s ease-in-out;
}

.uh-dashboard-quickaction-item:hover .uh-dashboard-quickaction-arrow,
.uh-dashboard-quickaction-item:focus-visible .uh-dashboard-quickaction-arrow {
  opacity: 1;
  transform: translateX(0);
  color: var(--bs-primary-text-emphasis);
}

@media (prefers-reduced-motion: reduce) {
  .uh-dashboard-quickaction-item,
  .uh-dashboard-quickaction-arrow {
    transition: none;
  }
}

/* Dashboard Refresh control, visual-parity pass ported from dev-ui — the
 * "live" dot reuses this app's own halo-dot recipe (same success color +
 * soft ring already used for status indicators elsewhere) — one
 * consistent "status indicator" language, not a second one invented for
 * this control. */
.uh-dashboard-refresh-status {
  display: inline-flex;
  align-items: center;
  gap: .4375rem;
}

.uh-dashboard-refresh-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background-color: var(--bs-success);
  box-shadow: 0 0 0 3px rgba(var(--bs-success-rgb), .15);
  flex-shrink: 0;
}

/* A slight press-down on click on top of whatever hover/focus treatment
 * .btn-outline-light already provides everywhere else in this app —
 * refines, doesn't replace, the existing button styling. */
.uh-dashboard-refresh-btn {
  display: inline-flex;
  align-items: center;
  transition: transform .1s ease-in-out;
}

.uh-dashboard-refresh-btn:active {
  transform: scale(.96);
}

/* Loading state: the existing refresh icon spins in place while
 * assets/js/pages/dashboard.js's loadDashboard() has a request in
 * flight. `disabled` (set by the same JS) already gets Bootstrap's own
 * dimmed/no-pointer-events treatment for free. */
.uh-dashboard-refresh-btn svg {
  transition: transform .15s ease-in-out;
}

.uh-dashboard-refresh-btn.uh-btn-loading {
  cursor: progress;
}

.uh-dashboard-refresh-btn.uh-btn-loading svg {
  animation: uh-icon-spin .7s linear infinite;
}

@keyframes uh-icon-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .uh-dashboard-refresh-btn,
  .uh-dashboard-refresh-btn svg {
    transition: none;
  }

  .uh-dashboard-refresh-btn.uh-btn-loading svg {
    animation: none;
  }
}

/* The tile's own flex-column stack: head row, value, optional
 * caption/note — one `gap` controls the rhythm between all of them so
 * no individual line needs its own top/bottom margin. Tightened from
 * .375rem — the label/value/caption relationship reads more like one
 * cohesive metric when the lines sit closer together, rather than each
 * floating with its own breathing room. */
.uh-dashboard-kpi-body {
  display: flex;
  flex-direction: column;
  gap: .25rem;
}

.uh-dashboard-kpi-head {
  display: flex;
  align-items: center;
  gap: .5rem;
}

/* The label's own margin-bottom (below) exists for the one remaining
 * place it's used standalone, stacked directly above a value with no
 * icon (User Overview's "Total Clients" mini-stat) — zeroed here since
 * inside the head row it sits beside the icon instead, and the row's
 * own `gap` already provides the spacing down to the value line. */
.uh-dashboard-kpi-head .uh-dashboard-kpi-label {
  margin-bottom: 0;
}

.uh-dashboard-kpi-label {
  font-size: .75rem;
  text-transform: uppercase;
  letter-spacing: .02em;
  color: var(--bs-secondary-color);
  margin-bottom: .125rem;
  overflow-wrap: anywhere;
text-transform: capitalize;
}

/* Restored to the full 1.5rem size (a previous pass had dropped this to
 * 1.25rem as a stopgap overflow fix) — now that the value owns the
 * tile's entire width instead of sharing a row with a 40px icon, it no
 * longer needs to give up size to fit; it goes back to being the clear
 * focal point of the tile. `overflow-wrap: break-word` is kept only as
 * a last-resort safety net for a pathological case (a huge number on a
 * very narrow phone viewport) — normal layouts never reach it, since
 * the head-row restructure is what actually prevents wrapping now, not
 * this property. `word-break: break-word` (more aggressive, breaks
 * anywhere) was removed for the same reason it caused the reported
 * mid-digit wrap in the first place. line-height tightened 1.25 -> 1.1
 * (aspect-ratio pass, 2026-07-23): at a bold 1.5rem this is still
 * comfortable — no ascender/descender clipping — but without the extra
 * quarter-line of empty space a body-text line-height convention adds
 * on top of a large display number, which was a real contributor to the
 * card's too-tall, too-square footprint. */
.uh-dashboard-kpi-value {
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--bs-body-color);
  line-height: 1.1;
  overflow-wrap: break-word;
}

/* Semantic KPI icon tints, ported from dev-ui — background + icon color
 * only, both already theme-aware via Bootstrap's own --bs-success/
 * --bs-warning/--bs-danger subtle-bg custom properties. Used below for
 * Success Rate's check icon (--success) — the same "severity cue"
 * convention this app's own statusBadge()/Redis-Status badge already
 * establish, applied to a KPI tile's own icon chip instead of a badge. */
.uh-dashboard-kpi-icon--success {
  background-color: var(--bs-success-bg-subtle);
  color: var(--bs-success);
}

.uh-dashboard-kpi-icon--warning {
  background-color: var(--bs-warning-bg-subtle);
  color: var(--bs-warning);
}

.uh-dashboard-kpi-icon--danger {
  background-color: var(--bs-danger-bg-subtle);
  color: var(--bs-danger);
}

/* Compact KPI row, visual-parity pass ported from dev-ui — scoped to
 * .uh-dashboard-txn-kpi (NOT the bare .uh-dashboard-kpi/-icon/-value
 * classes, which are shared app-wide and must stay untouched) so none of
 * this ripples onto any other KPI tile shape in the app. One flex ROW
 * (icon + text stack) instead of three stacked rows (head-row, value,
 * caption) is the actual mechanism that shortens the card — applied to
 * Executive Overview's non-hero tiles (Total Clients, Transactions
 * Today, Success Rate), which read as tall, square cards without it. */
.uh-dashboard-txn-kpi {
  border-radius: .625rem .15rem .625rem .625rem;
}

.uh-dashboard-txn-kpi .uh-dashboard-kpi-body {
  flex-direction: row;
  align-items: center;
  gap: .5rem;
}

.uh-dashboard-txn-kpi-text {
  display: flex;
  flex-direction: column;
  gap: .0625rem;
  min-width: 0;
}

.uh-dashboard-txn-kpi-text .uh-dashboard-kpi-label {
  margin-bottom: 0;
}

.uh-dashboard-txn-kpi .card-body {
  padding: .55rem .75rem;
}

.uh-dashboard-txn-kpi .uh-dashboard-kpi-icon {
  width: 21px;
  height: 21px;
  border-radius: .4rem .15rem .4rem .4rem;
  box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), .07);
}

.uh-dashboard-txn-kpi .uh-dashboard-kpi-icon--success {
  box-shadow: 0 0 0 3px rgba(var(--bs-success-rgb), .08);
}

.uh-dashboard-txn-kpi .uh-dashboard-kpi-icon--danger {
  box-shadow: 0 0 0 3px rgba(var(--bs-danger-rgb), .08);
}

.uh-dashboard-txn-kpi .uh-dashboard-kpi-value {
  font-size: 1.1875rem;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
}

/* Consistency fix (2026-07-24): Wallet Balance is a full Naira amount
 * (typically 12-14 characters, e.g. "₦1,505,650.98") — the longest
 * value anywhere in Executive Overview by a clear margin, and the only
 * one that could still wrap onto a second line at the tightest card
 * width, which made this one card render taller than its 3 siblings
 * even with `h-100` restored (h-100 matches the *tallest* card in the
 * row — if Wallet Balance's own content grows to two lines, it becomes
 * the tallest, and pulls the others down with it). A 10% reduction
 * (1.5rem -> 1.35rem) is barely perceptible sitting next to the other
 * three values at full size, but removes just enough width pressure to
 * keep a full amount on one line in the vast majority of cases.
 * Deliberately scoped to this one id — every other KPI value on the
 * page (Total Clients, Transactions Today, Success Rate, and everything
 * in User/Platform Overview) is untouched and stays at the full
 * 1.5rem. */
#uh-kpi-wallet-total {
  font-size: 1.35rem;
}

/* A third microcopy line under a *real* (non-placeholder) KPI value —
 * Platform Overview's Active Services/Total Providers use this to
 * explain what the number means, which also happens to give them the
 * same 3-line height as their placeholder siblings in the same row
 * (uh-dashboard-kpi--placeholder below), so a real tile and a
 * not-yet-available tile never sit at visibly different heights next to
 * each other. Not italic, unlike the placeholder note — italics are
 * reserved for signaling "this isn't real data yet". Matches
 * .uh-dashboard-placeholder-note (both .75rem, Bootstrap's fs-8 step) —
 * one shared "fine print" size for both. line-height set explicitly
 * (aspect-ratio pass, 2026-07-23) — this line previously inherited the
 * body's own 1.5 line-height, noticeably loose for a single small
 * caption line and, compounded with the tile's other spacing, part of
 * why 3-line tiles read as too tall. */
.uh-dashboard-kpi-caption {
  font-size: .75rem;
  line-height: 1.3;
  color: var(--bs-secondary-color);
  overflow-wrap: anywhere;
}

/* Platform Overview compact rows — visual-parity pass. Row mechanics
 * (padding, border-between-rows, hover tint) are the same recipe dev-ui's
 * own API Health list (.uh-dashboard-health-row) uses for the identical
 * "several small operational facts inside one card" problem; the row
 * *contents* reuse this app's own existing .uh-dashboard-kpi-icon/-label/
 * -caption/-value sub-parts rather than inventing a second icon/text
 * language, since Platform Overview's own values (counts, an environment
 * name, status badges) aren't a homogeneous up/down health set the way
 * dev-ui's API Health rows are — no direct 1:1 widget to copy, so this
 * composes the two apps' own existing pieces instead of forcing a
 * mismatched semantic (e.g. a health dot next to "Total Staff: 3", which
 * has no boolean health of its own) onto this section's real data. */
.uh-dashboard-stat-row {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: .625rem .5rem;
  border-radius: var(--bs-border-radius);
  transition: background-color .15s ease-in-out;
}

.uh-dashboard-stat-row:hover {
  background-color: var(--bs-secondary-bg);
}

@media (prefers-reduced-motion: reduce) {
  .uh-dashboard-stat-row {
    transition: none;
  }
}

.uh-dashboard-stat-row:not(:first-child) {
  border-top: 1px solid var(--bs-border-color);
}

.uh-dashboard-stat-row-text {
  display: flex;
  flex-direction: column;
  gap: .0625rem;
  min-width: 0;
  flex: 1 1 auto;
}

.uh-dashboard-stat-row-text .uh-dashboard-kpi-label {
  margin-bottom: 0;
}

.uh-dashboard-stat-row-value {
  flex-shrink: 0;
  font-size: 1rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--bs-body-color);
  text-align: right;
}

/* Visual-refinement pass: User Overview/Financial Summary's stat pills
 * (dashboard/index.php's own comment on each grid explains why they
 * moved off a flex-wrap row) sit inside a 2-column Bootstrap grid — but
 * vendors/main.css has no base `.badge-status{...}` display rule at all
 * (confirmed by grep), so the element defaults to a plain inline <span>.
 * An inline span has no box model of its own to constrain its content
 * to its parent `.col`'s width, so a longer pill (e.g. "Needs
 * Reconciliation") overflowed straight into the neighboring column
 * instead of wrapping — confirmed live. Scoped to this one grid context
 * (not a blanket `.badge-status` override, which is a shared vendor
 * class used elsewhere in this app with its own, different existing
 * layouts that must stay untouched): `flex` + `flex-wrap` lets a long
 * label wrap onto its own second line within its column instead of
 * escaping it. */
.uh-badge-pill-grid .badge-status {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: .25rem;
  min-width: 0;
}

/* Notifications, ported verbatim from dev-ui's own dashboard notification
 * widget (dashboard/index.php's own comment on this section covers the
 * data-source reasoning). Rows are an icon + text-stack flex row (the
 * same shape Quick Actions/Platform Overview's own stat rows already
 * use), a transparent-by-default border that only turns visible on
 * hover — rows read as a plain list at rest, not a stack of boxes, but
 * still get real depth when interactive — the same hover recipe Quick
 * Actions already established (border-color + bg tint + a 1px lift). */
.uh-dashboard-notification-list {
  display: flex;
  flex-direction: column;
  gap: .375rem;
}

.uh-dashboard-notification-item {
  display: flex;
  align-items: flex-start;
  gap: .625rem;
  padding: .5rem .625rem;
  border: 1px solid transparent;
  border-radius: var(--bs-border-radius);
  color: inherit;
  text-decoration: none;
  transition: background-color .15s ease-in-out, border-color .15s ease-in-out, transform .15s ease-in-out;
}

.uh-dashboard-notification-item:hover,
.uh-dashboard-notification-item:focus-visible {
  background-color: var(--bs-primary-bg-subtle);
  border-color: var(--bs-primary);
  color: inherit;
  transform: translateY(-1px);
}

.uh-dashboard-notification-item:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 1px;
}

@media (prefers-reduced-motion: reduce) {
  .uh-dashboard-notification-item {
    transition: none;
  }
}

.uh-dashboard-notification-item-icon {
  flex-shrink: 0;
}

.uh-dashboard-notification-item-body {
  flex: 1 1 auto;
  min-width: 0;
}

.uh-dashboard-notification-item-title {
  display: inline-flex;
  /* flex-start, not center: a long title wraps onto several lines inside
   * this flex container, and `align-items: center` was centering the dot
   * against that *entire* multi-line block — visually stranding it next
   * to a middle line instead of the title's own first word. flex-start
   * keeps it pinned to the top, beside line one, regardless of how many
   * lines the title wraps to. */
  align-items: flex-start;
  font-size: .8125rem;
  font-weight: 500;
  color: var(--bs-body-color);
  overflow-wrap: anywhere;
}

/* Unread indicator: a small dot + bold title, "elegant, not heavy" — the
 * background tint is a much fainter hand-picked opacity than the hover
 * state above, so hover still reads as a clearly stronger step up from
 * an unread row's own resting state. */
.uh-dashboard-notification-item--unread {
  background-color: rgba(var(--bs-primary-rgb), .05);
}

.uh-dashboard-notification-item--unread .uh-dashboard-notification-item-title {
  font-weight: 700;
}

.uh-dashboard-notification-item-dot {
  width: 6px;
  height: 6px;
  /* Nudges the dot down from the pure flex-start (text top) to sit level
   * with the first line's own x-height instead of its ascender line. */
  margin-top: .375rem;
  margin-right: .375rem;
  border-radius: 50%;
  background-color: var(--bs-primary);
  flex-shrink: 0;
}

.uh-dashboard-notification-item-date {
  flex-shrink: 0;
  font-size: .75rem;
  color: var(--bs-secondary-color);
}

.uh-dashboard-notification-item-message {
  margin: .125rem 0 0;
  font-size: .75rem;
  color: var(--bs-secondary-color);
  overflow-wrap: anywhere;
}

/* Loading skeleton — dashboard/index.php's own initial markup inside
 * #uh-dashboard-notifications-list, sized to roughly match a real row's
 * height so the swap to real content (or the empty state) doesn't jump.
 * populateNotifications() always replaces this container's entire
 * innerHTML on its first call (success or empty), same mechanism every
 * other skeleton on this page already relies on. */
.uh-dashboard-notification-skeleton-row {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: .5rem .625rem;
}

.uh-dashboard-notification-skeleton-icon {
  width: 28px;
  height: 28px;
  border-radius: var(--bs-border-radius);
  flex-shrink: 0;
}

.uh-dashboard-notification-skeleton-lines {
  flex: 1 1 auto;
  min-width: 0;
}

/* Dark-mode contrast fix, same pattern as .uh-dashboard-kpi-icon's own
 * comment: vendors/main.css's `.badge-soft-primary` reads `color:
 * var(--bs-primary)` directly — confirmed live (this section's own
 * unread-count badge) that dark mode's plain --bs-primary is too close
 * in luminance to --bs-soft-primary's own translucent background to stay
 * legible. --bs-primary-text-emphasis is the correct token for "readable
 * text on a *-bg-subtle-family surface" in both themes, same as every
 * other icon-chip fix already applied elsewhere in this file — light
 * theme is untouched (its own --bs-primary already has enough contrast
 * against --bs-soft-primary there). */
[data-bs-theme="dark"] .badge.badge-soft-primary {
  color: var(--bs-primary-text-emphasis);
}

/* Dark-mode contrast fix, same pattern as .badge-soft-primary's own
 * comment just above: vendors/main.css's `.btn-outline-primary` resting
 * state reads `color`/`border-color: var(--bs-primary)` directly —
 * confirmed live (clients/profile.php's Rotate Secret / Request More
 * Information / Request Document buttons) that dark mode's plain
 * --bs-primary is a near-black slate, essentially unreadable against a
 * dark card background even though the button is fully enabled. Scoped
 * to the resting state only — :hover/:focus/:active already swap to
 * white text on a solid --bs-primary fill (vendors/main.css), which has
 * plenty of contrast on its own and is left untouched. Light theme is
 * untouched too (its own --bs-primary already has enough contrast there). */
[data-bs-theme="dark"] .btn-outline-primary {
  color: var(--bs-primary-text-emphasis);
  border-color: var(--bs-primary-text-emphasis);
}

/* Placeholder tiles (Staff, Revenue, API Environment, Redis Status,
 * Queue Status — no backend field exists for any of these yet) get a
 * visibly muted icon/value plus a small italic note, so they read as
 * *intentionally* empty rather than broken. */
.uh-dashboard-kpi--placeholder .uh-dashboard-kpi-icon {
  background-color: var(--bs-secondary-bg);
  color: var(--bs-secondary-color);
}

.uh-dashboard-kpi--placeholder .uh-dashboard-kpi-value {
  color: var(--bs-secondary-color);
}

.uh-dashboard-placeholder-note {
  font-size: .75rem;
  line-height: 1.3;
  color: var(--bs-secondary-color);
  font-style: italic;
  overflow-wrap: anywhere;
}

/* A plain 1px rule separating a placeholder tile from the stat strip
 * below it in Financial Overview's combined card — var(--bs-border-color)
 * so it inherits both themes for free, same as every other rule here. */
.uh-dashboard-divider {
  border-top: 1px solid var(--bs-border-color);
}

/* Charts are supporting detail in the redesigned layout, not the
 * card's main subject (their JS chart:{height:...} config was already
 * reduced to match — see dashboard.js's populateClientChart()/
 * populateMonthlyChart() comments) — this just stops either chart's
 * container from claiming more width than its card intends on very wide
 * viewports. */
.uh-dashboard-chart-support {
  max-width: 100%;
}

/* Layout refactor (2026-07-23) — boxed two-panel workspace. Purely
 * structural: no widget markup, color, typography or spacing scale
 * changed, only the container the existing sections now sit inside (see
 * dashboard/index.php's matching comment). One bordered, rounded frame
 * (the "workspace") holds two independently-scrolling panels instead of
 * the page itself being one long scroll — the Stripe/Linear/Vercel
 * "boxed application view" feel asked for. Mobile-first: a single
 * stacked column (both panels full width, workspace height auto, page
 * scrolls normally) is the base rule; the side-by-side split with fixed,
 * independently-scrolling panels only turns on at lg+, matching every
 * other lg breakpoint already used across this page's own grid (row
 * g-3 col-lg-*). */
/* Canvas-contrast pass (2026-07-27): the comment above already describes
 * this as a "bordered, rounded frame", but border was actually `none` — so
 * the frame had no visible presence at all beyond the panel-divider line
 * between its two columns. A real hairline border is what actually
 * delivers the "boxed application view" the surrounding comments describe.
 * `overflow: hidden` clips the two panels' background to the frame's own
 * rounded corners; it doesn't affect `.uh-dashboard-panel`'s independent
 * `overflow-y: auto` scrolling below, since that's the child's own
 * overflow context, not this element's.
 *
 * White-surface revision (2026-07-27, follow-up): an earlier version of
 * this pass filled the frame with a visible tint so the white cards
 * inside would read as elevated above it — reverted in light theme (see
 * the `[data-bs-theme="light"]` override directly below) per explicit
 * direction that every card must sit on a true white surface with zero
 * background tint, separation coming only from the border + each card's
 * own shadow (`.uh-dashboard-section .card` below, strengthened slightly
 * to carry that depth cue on its own).
 *
 * Two-level consolidation (2026-07-27, second follow-up): this is a
 * layout container, not a raised content surface, so in dark theme it
 * now reads `--bs-body-bg` directly (Level 1 — the same app-background
 * token as `body`, the sidebar, the navbar and the footer) instead of
 * `--bs-tertiary-bg`, which was repurposed to a Level-2 hover/utility
 * shade in the root dark-theme block and is no longer close to Level 1.
 * The border above still gives the frame its own visible edge; the cards
 * inside (Level 2, `--hk-bg-primary`) are what actually needs to read as
 * elevated, not this container itself. */
.uh-dashboard-workspace {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  border: 1px solid var(--bs-border-color);
  border-radius: .75rem;
  background-color: var(--bs-body-bg);
  overflow: hidden;
}

/* Light theme only: pure white, matching every card's own background
 * (--hk-bg-primary) exactly, so the frame itself carries no tint at
 * all — separation comes entirely from the border above and the cards'
 * own shadows, not from a background color. */
[data-bs-theme="light"] .uh-dashboard-workspace {
  background-color: var(--hk-bg-primary);
}

.uh-dashboard-panel {
  min-width: 0;
  padding: 1.5rem;

}

/* dashboard.js's no-permission fallback (contentEl.innerHTML = ...)
 * replaces the two panels below this container with one plain card,
 * bypassing both .uh-dashboard-panel elements entirely — without this,
 * that card would only fill the grid's first column at lg+, leaving a
 * blank gap where the right panel used to be. Spans the full workspace
 * and picks up the same padding a panel would have had. */
.uh-dashboard-workspace > :not(.uh-dashboard-panel) {
  grid-column: 1 / -1;
  padding: 1.5rem;
}

.uh-dashboard-panel-left {
  border-bottom: 1px solid var(--bs-border-color);
}

@media (min-width: 992px) {
  /* 7fr/3fr reads as the requested ~65-70% / ~30-35% split while still
   * letting each panel's own width be a simple fraction of the
   * workspace rather than a hardcoded percentage. */
  .uh-dashboard-workspace {
    grid-template-columns: 7fr 3fr;
    /* Header simplification pass (2026-07-23): dashboard/index.php now
     * sets $uhShowBreadcrumb = false (layouts/app.php), so the shared
     * breadcrumb bar no longer renders above this workspace at all —
     * recomputed from scratch rather than just subtracting an estimate
     * of its height from the previous 21rem, to avoid compounding
     * earlier approximation error. Itemized against layouts/app.php +
     * vendors/main.css's own rules (still an estimate — no chrome height
     * here is CSS-readable without JS): fixed navbar 65px + `#uh-page-
     * body`'s own unconditional `mt-4` 24px + this page's own title
     * block (~51px content + `mt-2`/`mb-2` 16px combined) + the Quick
     * Actions card (~16px header padding + ~18px title text + ~40px body
     * padding + ~31px button row + `mb-3` 16px) ≈ 279px, i.e. ~17.5rem.
     * If this still looks short in a real browser, this is the value to
     * shrink further before touching anything else. */
    height: calc(100vh - 17.5rem);
  }

  .uh-dashboard-panel-left {
    border-bottom: none;
    border-right: 1px solid var(--bs-border-color);
  }

  .uh-dashboard-panel {
    height: 100%;
    overflow-y: auto;
    
  }
}

/* Sidebar footer nav (Enterprise UX pass) — layouts/sidebar.php's
 * .hk-menu now has a third flex child after .nicescroll-bar (System API
 * Management / Audit Logs / Profile), pinned to the bottom by the same
 * flex-column layout vendors/main.css already gives .hk-menu itself; this
 * rule only adds the visual separator the vendor bundle has no opinion
 * on. Written against the same attribute-qualified selector vendors/
 * main.css uses for .menu-content-wrap so specificity is equal-or-higher
 * regardless of cascade order (this file loads after the vendor bundle,
 * but that shouldn't be the only reason it wins). */
.hk-wrapper[data-layout=vertical] .hk-menu .uh-sidebar-footer {
  padding-top: .75rem;
  padding-bottom: .75rem;
  border-top: 1px solid var(--hk-border-primary);
  flex: 0 0 auto;
}

/* Profile row: same .nav-link the two links above it use (so collapsed
 * mode's existing `>*:not(.nav-icon-wrap){visibility:hidden}` rule hides
 * this row's text/chevron for free, exactly like every other nav item),
 * plus a two-line name/role stack in place of a single label. No `gap`
 * here (a previous pass added one) — the two link rows above get their
 * icon-to-text spacing entirely from .nav-icon-wrap's own vendor
 * `margin-right: .875rem`; adding a second spacing source only on this
 * row pushed its text ~8px further right than the other two, one of the
 * two causes of the reported misalignment (see .uh-sidebar-footer
 * .nav-icon-wrap below for the other). The chevron needs no gap either —
 * `margin-left: auto` on .uh-sidebar-profile-chevron already pushes it
 * to the row's end on its own. */

/* Support Tickets sidebar badge — .nav-icon-wrap has no `position` of its
 * own in vendors/main.css (just `display: flex`), so the badge's own
 * `.position-top-end-overflow-1` (`position: absolute`, the same class
 * the header bell badge already uses) would anchor to the nearest
 * positioned ancestor instead of this icon. `position: relative` here is
 * a pure no-op for every other nav item (nothing else inside .nav-icon-wrap
 * is absolutely positioned) — safe to apply to every icon wrap, not just
 * this one item's, so any future nav badge needs no CSS of its own either.
 * vendors/main.css's own `.nav-icon-wrap>*:not(.badge){font-size:1.5rem}`
 * rule already anticipates a `.badge` living in here (excluded from the
 * icon glyph's oversized font-size), confirming this is the template's
 * own intended slot for a nav badge, not a new pattern. */
.nav-icon-wrap {
  position: relative;
}

/* Root cause of the misalignment: vendors/main.css's .nav-icon-wrap has
 * no fixed size — `display: flex` with no width/height, so its rendered
 * box is exactly as wide as whatever it wraps. System API Management and
 * Audit Logs wrap a 20x20 uh_icon() SVG, so their wrap is 20px wide; the
 * Profile row instead wraps a 32x32 .avatar-xs (vendors/main.css's own
 * `.avatar.avatar-xs{width:2rem;height:2rem}`), so *its* wrap was 32px
 * wide — 12px more, shifting the label after it and making the avatar
 * itself look off-axis. Fixing the wrapper to the same fixed 20x20 slot
 * every icon in this footer already renders at (rather than special-
 * casing the profile row with a margin/offset) is the actual fix; the
 * avatar is then explicitly sized to fill that same slot below, instead
 * of overflowing or shrinking unpredictably as a flex child would. */
.uh-sidebar-footer .nav-icon-wrap {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
}

.uh-sidebar-footer .nav-icon-wrap .avatar {
  width: 1.25rem;
  height: 1.25rem;
}

.uh-sidebar-footer .nav-icon-wrap .avatar .initial-wrap {
  font-size: .5rem;
}

/* Header parity pass — the account-menu toggle now borrows the same
 * .btn-icon treatment every other header icon (theme toggle, notification
 * bell) already uses, for consistent sizing/hover feedback. That brings
 * an unwanted side effect along with it though: vendors/main.css's
 * `.btn > span:not(.badge):not(.sr-only):not([class^=spinner])
 * {width:100%;height:100%}` — written for a `.icon` wrapper span meant to
 * fill its button — also matches `.avatar`, since it's the toggle's only
 * direct-child span, and outranks `.avatar.avatar-xs`'s own `width:2rem`
 * on specificity. Left alone that stretches the 32px avatar circle out to
 * fill the full 38px button, losing the circular crop and the ring of
 * padding every other header icon has around it. `!important` here
 * restores avatar-xs's intended 32px — same fix, same reasoning as
 * `.uh-sidebar-footer .nav-icon-wrap .avatar` above for the identical
 * avatar-in-a-fixed-box scenario in the sidebar footer. */
.uh-account-toggle > .avatar.avatar-xs {
  width: 2rem !important;
  height: 2rem !important;
}

/* Header notification counter, ported verbatim from dev-ui's own
 * identical fix — header.php's `.uh-notif-badge` class has been reserved
 * on the bell icon's badge since this app's own Phase 1 but never
 * actually had CSS behind it — the `badge-xs` class that used to sit
 * alongside it in the markup doesn't exist anywhere in the vendored
 * bundle, so the badge was rendering at Bootstrap's full default
 * `.badge` size (`.65em .35em` padding, `.75em` font) on top of the
 * header's 18px icons — oversized enough to visibly unbalance the
 * header, and never a true circle/pill at that size. Sized here instead:
 * a small, fixed-height pill just large enough for one or two digits,
 * with a border matching the page background (`--bs-body-bg`, already
 * theme-aware) so it reads as a distinct badge sitting ON the icon
 * rather than a shape merging into it.
 *
 * Position: vendors/main.css's `position-top-end-overflow-1` (still on
 * the element, still driving the `translate(50%,-50%)` corner pull) sets
 * `top`/`right` as a percentage of the *button's* 38px box, not the 18px
 * bell glyph centered inside it — that lands the badge nearer the
 * button's own corner than the bell's. Overridden below in px, measured
 * against the glyph: an 18px icon centered in a 38px box sits
 * (38-18)/2 = 10px in from each edge, so anchoring the (pre-translate)
 * badge position at that same 10px and letting the vendor's
 * translate(50%,-50%) pull it outward from there lands the badge dead on
 * the glyph's top-right corner. `#uh-notif-badge` (id) beats the
 * vendor's single-class selector on specificity, so this wins regardless
 * of stylesheet order. */
.uh-notif-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 14px;
  height: 14px;
  padding: 0 3px;
  font-size: .5625rem;
  line-height: 1;
  border: 1.5px solid var(--bs-body-bg);
}

#uh-notif-badge.uh-notif-badge {
  top: 10px !important;
  right: 10px !important;
}

.uh-sidebar-profile-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  white-space: normal;
  line-height: 1.2;
}

.uh-sidebar-profile-name {
  font-size: .875rem;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.uh-sidebar-profile-role {
  font-size: .75rem;
  color: var(--bs-secondary-color);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.uh-sidebar-profile-role:empty {
  display: none;
}

.uh-sidebar-profile-chevron {
  margin-left: auto;
  flex-shrink: 0;
  color: var(--bs-secondary-color);
  display: flex;
  transition: transform .15s ease-in-out;
}

/* Dropup + Bootstrap's own aria-expanded toggle (no new JS): the chevron
 * points up while closed (hinting the menu opens upward, this row being
 * the last thing in the sidebar) and flips to point at the row it
 * belongs to once open. */
.uh-sidebar-profile-item .nav-link[aria-expanded="true"] .uh-sidebar-profile-chevron {
  transform: rotate(180deg);
}

.uh-sidebar-account-dropdown {
  min-width: 240px;
}

/* Enterprise Detail Page pass — shared across every profile/detail page
 * (clients/profile.php, transactions/detail.php, staff/detail.php,
 * providers/detail.php, products/detail.php, wallets/overview.php,
 * staff/role-detail.php), unlike the system/index.php redesign's
 * deliberately page-scoped uh-system-hub-* rules: this pass explicitly
 * asked for one reusable design language across many pages, so these
 * classes are named generically and meant to be reused by any future
 * detail page too. */

/* Root-cause fix for "long identifier overflows its card": uh_detail_list()
 * (components/detail-list.php) is the one shared component every detail
 * page above renders its label/value rows through, and its value column
 * had no overflow protection at all — a long, space-less token (a
 * transaction reference, a UUID, an idempotency key, a callback URL) set
 * as plain text or <code> doesn't wrap by default, so it pushes past the
 * column/card edge instead. Fixing it once here fixes every current and
 * future caller, rather than patching each page's markup individually.
 * overflow-wrap: anywhere (not the more conservative break-word) because
 * these are frequently *fully* unbroken tokens (no hyphens/underscores
 * for break-word to hook into) — anywhere is the one value guaranteed to
 * never overflow regardless of token shape. min-width: 0 is required for
 * this to take effect at all inside a Bootstrap .row/.col flex/grid
 * context, which otherwise sizes the column to its content's intrinsic
 * (unwrapped) width first. */
/* Content-formatting audit: this rule used to also carry
 * `text-transform: capitalize !important`, blanket-applied to every value
 * this shared component ever renders (dev-ui's own copy of this same rule
 * carried the identical bug and was fixed the same way earlier — see
 * dev-ui/assets/css/dev.css's matching comment). That title-cased every
 * word of any genuine free-text value passed through (e.g. a webhook
 * delivery's "Last Error"/payload text, a security/audit event detail),
 * without touching the underlying data — a display-layer bug, not a data
 * one. Any specific value that IS a short, enum-like word wanting
 * title-casing opts in directly at its own render call via Bootstrap's
 * `.text-capitalize` utility instead (the same "opt in on the one value
 * that needs it" convention `.uh-text-url` below already established for
 * opting URLs *out*). */
.uh-detail-list .row > [class*="col-"] {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Data/value typography: the shared component's value column (col-sm-8)
 * had no font-size override at all, inheriting the ambient 1rem/16px body
 * default — visibly larger than both its own .75rem label directly above
 * it and this app's own established "compact data" tier (.875rem) already
 * used everywhere else a label/value pair appears (.form-control inputs,
 * .uh-entity-header-meta). Row padding tightens to .4375rem ("compact but
 * comfortable," matching the value-column resize) — `!important` is
 * required here since the row markup carries Bootstrap's own `.py-2`
 * utility class (components/detail-list.php), which vendors/main.css
 * compiles with `!important` on both properties; a plain override loses
 * to that regardless of selector specificity. */
.uh-detail-list .row {
  padding-top: .4375rem !important;
  padding-bottom: .4375rem !important;
}

.uh-detail-list .col-sm-8 {
  font-size: .875rem;
}

/* URL/domain values must never be visually capitalized — the capitalize
 * rule above (added for enum-like values: Product Network, Service Type,
 * ...) otherwise turns a stored URL like "https://example.com" into
 * "https://Example.Com" purely at render time (the value itself is
 * never altered — this is a display-layer bug, not a data one).
 * text-transform is inherited, so setting a *different* value directly on
 * the element itself (no !important needed) always wins over whatever the
 * ancestor .uh-detail-list col computed, regardless of that rule's own
 * specificity/!important. Apply directly to whichever span/code actually
 * renders a URL (clients/profile.php's KYC Website row, system/environment.php's
 * URL row, dev-ui's own webhook URL row, and any future one) rather than
 * removing the shared rule, which real enum-value callers still rely on. */
.uh-text-url {
  text-transform: none;
}

/* A handful of pages render a full sentence or multi-word phrase inside a
 * `.badge` pill instead of plain text (e.g. an audit/activity event
 * label). These were never affected by .uh-detail-list's own (now-removed)
 * blanket capitalize rule above — the culprit here is vendors/main.css's
 * own base `.badge{text-transform:capitalize}`, which applies to every
 * badge in the app regardless of this app's own classes, and which this
 * project's own conventions don't allow hand-editing. Every *other* badge
 * in the app (statusBadge() and friends) genuinely wants that — they hold
 * a single short enum word. Same opt-out shape as .uh-text-url above,
 * generalized under its own name since "this badge holds a sentence" is a
 * distinct reason from "this value is a URL." No !important needed — this
 * file loads after vendors/main.css, so a plain override already wins at
 * equal specificity via source order alone. */
.uh-badge-sentence {
  text-transform: none;
}

/* Entity header band — icon + primary name/value + status badge(s) +
 * a muted secondary-facts line, sitting above the existing two-column
 * detail/actions row on every page listed above. Deliberately built from
 * each page's *existing* element ids relocated into this markup, not new
 * ids — see the per-page comments at each call site for exactly which
 * ids moved. */
.uh-entity-header {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  padding: 1.25rem;
  margin-bottom: 1.5rem;
  background-color: var(--bs-body-bg);
  border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: var(--bs-border-radius);
}

.uh-entity-header-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: var(--bs-border-radius);
  background-color: var(--bs-primary-bg-subtle);
  /* --bs-primary-text-emphasis — see .uh-dashboard-kpi-icon's own comment. */
  color: var(--bs-primary-text-emphasis);
  flex-shrink: 0;
}

/* Notification Details page — category-tinted icon-chip variants, ported
 * from dev-ui's own dev.css verbatim. Reuses Bootstrap's own subtle-bg/
 * text-emphasis token pairs, the exact same ones the category badge (JS-
 * set badge-soft-{variant}) already colors itself with, so the icon and
 * its badge always visually agree and both stay theme-aware automatically
 * — no new colors introduced. Modifier classes added alongside the base
 * class above (not a variant prop on it), so every other page's plain,
 * uncategorized entity-header icon (Client, Staff, Transaction, ...) is
 * unaffected. */
.uh-entity-header-icon--danger {
  background-color: var(--bs-danger-bg-subtle);
  color: var(--bs-danger-text-emphasis);
}

.uh-entity-header-icon--success {
  background-color: var(--bs-success-bg-subtle);
  color: var(--bs-success-text-emphasis);
}

.uh-entity-header-icon--info {
  background-color: var(--bs-info-bg-subtle);
  color: var(--bs-info-text-emphasis);
}

.uh-entity-header-icon--secondary {
  background-color: var(--bs-secondary-bg-subtle);
  color: var(--bs-secondary-text-emphasis);
}

.uh-entity-header-body {
  min-width: 0;
  flex: 1 1 auto;
}

/* Notification Details page — the message reads as an actual delivered
 * message rather than one more labeled card field: its own gently
 * distinct surface (a tertiary-bg fill, no border on top of it — the
 * card already has one, and doubling up would look like two nested
 * boxes) with roomier padding and a readable measure (max-width) rather
 * than stretching full card width on a wide desktop viewport. Ported
 * from dev-ui's dev.css verbatim (its own .uh-notification-message-body/
 * -actions), which itself already established this treatment as this
 * app-family's "prose/message-body content" tier, distinct from the
 * .875rem-scale label/value data rows .uh-detail-list uses. */
.uh-notification-message-body {
  padding: 1.25rem;
  border-radius: var(--bs-border-radius);
  background-color: var(--bs-tertiary-bg);
  max-width: 640px;
  white-space: pre-wrap;
  word-break: break-word;
  font-size: .9375rem;
  line-height: 1.6;
}

/* The "Mark as Read" action sits below the message as its own row, not
 * appended to the message text — a top border/spacing break keeps it
 * reading as "an action about this message," not part of the message's
 * own content. Hidden via the same d-none toggle the button itself would
 * otherwise carry directly (assets/js/pages/system-notification-detail.js),
 * moved to this wrapper so the divider disappears along with the button
 * once a notification is already read, instead of leaving an empty
 * bordered gap. */
.uh-notification-message-actions {
  margin-top: 1.25rem;
  padding-top: 1.25rem;
  border-top: var(--bs-border-width) solid var(--bs-border-color);
}

.uh-entity-header-title {
  font-size: 1.1rem;
  font-weight: 700;
  overflow-wrap: anywhere;
}

.uh-entity-header-badges {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin-top: .375rem;
}

/* Information-hierarchy pass — every value in the entity header (the
 * title, each status badge, each meta fact) previously had no label
 * identifying what it represented, relying solely on position/badge
 * color. This wraps a badge (or the title) with a small label above it,
 * reusing the exact same small/muted/uppercase recipe
 * .uh-dashboard-kpi-label already established for "label above value"
 * elsewhere in the app (transaction/wallet summary tiles) — not a new
 * visual language, just this component's own version of it. */
.uh-entity-header-field {
  display: flex;
  flex-direction: column;
  gap: .125rem;
}

.uh-entity-header-label {
  font-size: .6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .02em;
  color: var(--bs-secondary-color);
}

.uh-entity-header-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .375rem;
  margin-top: .5rem;
  font-size: .875rem;
  color: var(--bs-secondary-color);
  overflow-wrap: anywhere;
}

.uh-entity-header-meta .uh-entity-header-label {
  color: inherit;
}

.uh-entity-header-meta .uh-entity-header-meta-sep {
  color: var(--bs-border-color);
}

/* Support Ticket conversation thread (support-tickets/ticket.php +
 * assets/js/pages/support-ticket-detail.js) — ported verbatim from
 * dev-ui/assets/css/dev.css's own identical rules (that file's own
 * comment explains the design: a chat/comment-style stacked thread,
 * reusing this app's existing avatar-chip language and Bootstrap
 * subtle-bg/border tokens rather than a new color). Kept byte-identical
 * so a ticket's thread reads the same on both sides of the same
 * conversation. */
.uh-ticket-message {
  display: flex;
  gap: .75rem;
  align-items: flex-start;
  margin-top: 1rem;
}

.uh-ticket-message:first-child {
  margin-top: 0;
}

.uh-ticket-message--grouped {
  margin-top: .375rem;
}

.uh-ticket-message-avatar {
  flex-shrink: 0;
  margin-top: .125rem;
}

.uh-ticket-message-avatar-spacer {
  display: inline-block;
  width: 2rem;
  flex-shrink: 0;
}

.uh-ticket-message-body {
  flex: 1 1 auto;
  min-width: 0;
  padding: .625rem .875rem;
  border-radius: var(--bs-border-radius);
  background-color: var(--bs-tertiary-bg);
}

.uh-ticket-message--staff .uh-ticket-message-body {
  background-color: var(--bs-primary-bg-subtle);
  border: 1px solid var(--bs-primary-border-subtle);
}

.uh-ticket-message-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: .75rem;
  margin-bottom: .25rem;
}

.uh-ticket-message-author {
  font-weight: 600;
  font-size: .8125rem;
}

.uh-ticket-message-time {
  font-size: .75rem;
  color: var(--bs-secondary-color);
  flex-shrink: 0;
  white-space: nowrap;
}

.uh-ticket-message-text {
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
  font-size: .9375rem;
  line-height: 1.5;
}

/* Action-card icon badge — the same 36px colored-square recipe as
 * system/index.php's own hub cards (uh-system-hub-icon), generalized
 * into a shared, non-page-scoped class since this pass reuses it across
 * every detail page's action panel, not just one page.
 * --bs-primary-text-emphasis — see .uh-dashboard-kpi-icon's own comment
 * (dark-mode --bs-primary is a near-black slate, unreadable against this
 * chip's own --bs-primary-bg-subtle background; confirmed live on
 * clients/profile.php's Verification card, same latent bug already fixed
 * on .uh-dashboard-kpi-icon/.uh-entity-header-icon/.uh-system-hub-icon). */
.uh-action-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--bs-border-radius);
  background-color: var(--bs-primary-bg-subtle);
  color: var(--bs-primary-text-emphasis);
  flex-shrink: 0;
}

/* A <pre> JSON/config dump's own overflow-safety — transactions/detail.php
 * already referenced this class name on its Provider Response block but
 * relied entirely on a duplicated inline style for the actual behavior;
 * this gives the class real content so any future <pre> dump (this pass
 * also applies it to system/security.php's Rate Limit Defaults block,
 * which had no wrap protection at all) gets it by adding one class,
 * rather than copying the same four properties inline again. */
.uh-json-block {
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
  max-height: 420px;
  overflow: auto;
}

/* System Administration hub redesign (system/index.php) — scoped to this
 * page's own markup only (uh-system-hub-* prefix), same isolation
 * approach as the dashboard's uh-dashboard-* rules above (see that
 * block's own comment). Deliberately not reusing .uh-dashboard-section-
 * title/.uh-dashboard-kpi-icon directly even though the recipe is
 * identical — those are explicitly scoped to dashboard/index.php's own
 * markup, and duplicating the ~10 lines here is cheaper than either
 * loosening that scoping comment or coupling two unrelated pages to one
 * shared class. */
.uh-system-hub-section-title {
  font-size: .75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--bs-secondary-color);
  margin-bottom: .75rem;
}

.uh-system-hub-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--bs-border-radius);
  background-color: var(--bs-primary-bg-subtle);
  /* --bs-primary-text-emphasis — see .uh-dashboard-kpi-icon's own comment. */
  color: var(--bs-primary-text-emphasis);
  flex-shrink: 0;
}

/* Breadcrumb separator fix — the vendor template's own
 * .breadcrumb-item+.breadcrumb-item:before rule (vendors/main.css)
 * renders its divider through an icon font, `font-family:
 * dripicons-v2!important; content:"V"` — that font's "V" character maps
 * to a chevron glyph in the vendor's own theme, but this app never loads
 * dripicons-v2 (every icon here is an inline SVG via uh_icon(), no icon
 * font anywhere else in the bundle), so the browser falls back to
 * literally rendering the capital letter "V", which reads as a
 * dropdown-style arrow next to every breadcrumb crumb. This app's own
 * admin.css loads after vendors/main.css (layouts/app.php, layouts/
 * auth.php), so the same selector here wins by cascade order and resets
 * the divider to a plain "/" — Bootstrap's own original default the
 * vendor rule was overriding — with no icon-font dependency. */
.breadcrumb-item + .breadcrumb-item::before {
  font-family: inherit;
  content: "/";
}

/* Global navigation preloader — visual redesign pass. Architecture
 * unchanged (assets/js/utils/preloader.js still owns click interception,
 * the sessionStorage continuation handoff between pages, and the
 * is-active/is-continuing/is-done class sequence; only what those classes
 * *look like* changed here, from a top progress bar to a centered,
 * three-dot "flowing pulse" — the same restrained motif Stripe/Linear/
 * Raycast use for an inline "working on it" cue rather than a determinate
 * progress metaphor, which suits this MPA better: the browser's own
 * navigation timing was never something we could measure precisely
 * enough to justify a bar that claims to show real progress.
 *
 * Markup: #uh-preloader (layouts/app.php / layouts/auth.php) now wraps
 * three small .uh-preloader-dot spans instead of standing alone with a
 * ::before bar — still just 4 total nodes (1 wrapper + 3 dots), no
 * heavier than the previous single-pseudo-element bar in any way that
 * matters (no images, no SVG, nothing that blocks or delays render).
 *
 * `background-color: var(--bs-primary)` on the dots is the exact same
 * accent every other themed element in this file already reads
 * (`.uh-dashboard-kpi-icon`, `.uh-entity-header-icon`, ...) — change that
 * one Bootstrap variable anywhere upstream and this repaints with it, in
 * both themes, automatically.
 *
 * Only `transform` (scale) and `opacity` are ever animated — both
 * GPU-compositable, neither triggers layout or repaints surrounding
 * content. `animation-play-state` (paused by default, running only while
 * a state class is present) means the three dots never tick a single
 * animation frame while the loader is hidden — the idle-page cost is
 * exactly zero, same as the bar it replaces. `pointer-events: none` plus
 * a z-index above the navbar/sidebar's own 1030-1083 range (see
 * vendors/main.css) means it can never intercept a click or shift any
 * real layout — a fixed-position 33x7px cluster is all that ever paints. */
#uh-preloader {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 6px;
  opacity: 0;
  z-index: 1200;
  pointer-events: none;
  transition: opacity .25s ease-out;
  will-change: opacity;
}

.uh-preloader-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background-color: var(--bs-primary);
  opacity: .35;
  transform: scale(.6);
  animation: uh-preloader-pulse 1.3s ease-in-out infinite;
  animation-play-state: paused;
  will-change: transform, opacity;
}

.uh-preloader-dot:nth-child(2) {
  animation-delay: .15s;
}

.uh-preloader-dot:nth-child(3) {
  animation-delay: .3s;
}

/* Calm, deliberate "breathe" — a slow scale+fade per dot, staggered so
 * the sequence reads as one smooth left-to-right wave rather than three
 * dots bouncing in place (bounce/translate motion is what reads as
 * playful; a shared scale+opacity curve at this speed reads as
 * sophisticated, closer to a heartbeat than a spinner). */
@keyframes uh-preloader-pulse {
  0%, 80%, 100% { transform: scale(.6); opacity: .35; }
  40% { transform: scale(1); opacity: 1; }
}

/* Triggered by a qualifying <a> click, on the page being left, and by the
 * destination page's own completion — both just fade the whole cluster
 * in/out; the dots keep pulsing the entire time they're visible (paused
 * again only once fully hidden), so the motion never abruptly starts or
 * stops mid-cycle. */
#uh-preloader.is-active,
#uh-preloader.is-continuing {
  opacity: 1;
}

#uh-preloader.is-active .uh-preloader-dot,
#uh-preloader.is-continuing .uh-preloader-dot,
#uh-preloader.is-done .uh-preloader-dot {
  animation-play-state: running;
}

/* The destination page's own first paint, when it's the continuation of
 * a cluster already started on the previous page — appears at full
 * opacity instantly, transition disabled, so there is no fade-in restart
 * and no gap between "left the old page" and "arrived at the new one." */
#uh-preloader.is-continuing {
  transition: none;
}

/* Destination page has finished loading — a slightly slower fade-out
 * than the fade-in, so the loader visibly settles rather than just
 * vanishing mid-pulse. */
#uh-preloader.is-done {
  opacity: 0;
  transition: opacity .3s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .uh-preloader-dot {
    animation: none;
    opacity: .85;
    transform: scale(.85);
  }
}

/* ==========================================================================
 * Enterprise auth redesign (2026-07-27, revised same day) — login.php only.
 * Pure CSS/JS-hook addition: no PHP auth logic, form action, id, name, or
 * validation touched — see login.php/login.js for the (also purely
 * additive) markup/JS this pairs with. Everything below is scoped under
 * `.hk-pg-auth`/`.uh-auth-*`, so none of it can leak onto the authenticated
 * app shell. This page was previously untouched by admin.css entirely (see
 * the compact-form-control pass above, which explicitly carves
 * `.hk-pg-auth` out) — a blank canvas, not a component being reworked out
 * from under existing overrides.
 *
 * Revision note: the first pass wrapped the form in an elevated
 * `.uh-auth-card` (background/border/shadow/padding box). Direct feedback
 * asked for that removed — an open, integrated layout instead, in the spirit
 * of an API-platform login (Stripe/Postman/Kong) rather than a boxed
 * consumer-app form. The card rule and its dark/mobile overrides are gone;
 * input/button styling below now targets `.hk-pg-auth` directly (the same
 * scoping convention the compact-form-control pass above already
 * establishes) instead of a wrapper div that no longer exists. Typography,
 * the password toggle, and the OTP field treatment from that first pass are
 * unchanged — none of that was card-dependent.
 * ========================================================================== */

/* Background: a faint two-point brand-tinted radial wash behind the whole
 * split layout, rather than a flat page color — reads as "considered" the
 * way Linear/Vercel auth pages do, without competing with the form itself
 * (each glow tops out at 5% opacity and fades to nothing by mid-viewport). */
.hk-pg-auth {
  background:
    radial-gradient(circle at 12% 18%, rgba(var(--bs-primary-rgb), .05), transparent 55%),
    radial-gradient(circle at 88% 82%, rgba(var(--bs-primary-rgb), .04), transparent 50%),
    var(--bs-body-bg);
}

/* A single quiet rule beneath the brand mark stands in for the elevation
 * the card used to provide — enough separation to feel intentional without
 * boxing the form in. Faded at both ends rather than a hard-edged line, so
 * it reads as a subtle break rather than a form-builder-style divider. */
.uh-auth-divider {
  height: 1px;
  width: 100%;
  max-width: 18rem;
  margin-left: auto;
  margin-right: auto;
  background: linear-gradient(to right, transparent, var(--hk-border-primary), transparent);
}

/* Brand lockup: small icon mark + the existing text logotype, tightened
 * and center-balanced rather than a single plain line of bold text. */
.uh-auth-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
}

.uh-auth-brand-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: .5rem;
  background: rgba(var(--bs-primary-rgb), .12);
  /* --bs-primary-text-emphasis, not --bs-primary — see
   * .uh-dashboard-kpi-icon's own comment: the brand color is now dark
   * enough in dark mode (post color-parity pass) that plain --bs-primary
   * loses contrast against this chip's own tinted background. */
  color: var(--bs-primary-text-emphasis);
  flex-shrink: 0;
}

.uh-auth-brand-mark .uh-auth-brand-svg {
  display: block;
  width: 1.4rem;
  height: 1.4rem;
}

.uh-auth-hero-badge .uh-auth-hero-svg {
  display: block;
  width: 1.75rem;
  height: 1.75rem;
}

/* Compact Ureh icon on a clean white chip (production branding pass).
   The dashboard header brand icon sits on white for clear contrast, and
   the auth badges get the same treatment so the unchanged slate + green
   mark reads on any surface in either theme (the neutral ink is no
   longer force-whitened in dark mode). Only the container changes — the
   icon artwork is untouched. */
.uh-sidebar-brand-icon,
.uh-auth-brand-mark,
.uh-auth-hero-badge {
  background: #ffffff;
  border: 1px solid rgba(0, 0, 0, .08);
}

:root[data-bs-theme="dark"] .uh-auth-hero-badge {
  border-color: rgba(0, 0, 0, .08);
}

/* Sidebar brand is icon-only now (the wordmark was removed). The vendor
   collapse rule hides `.navbar-brand .brand-img:last-child` to drop the
   wordmark on the 72px rail — but with a single child the icon IS the
   last child, so it was hidden along with it. Keep the icon visible in
   every collapsed state (the base rail and the width-pinned hover
   flyout) so the brand still shows when the sidebar is collapsed.
   Specificity matches the vendor rules; this file loads after
   vendors/main.css, so it wins the tie. */
@media (min-width: 1200px) {
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"] .hk-menu .menu-header > span .navbar-brand .brand-img.uh-sidebar-brand-icon,
  .hk-wrapper[data-layout="vertical"][data-layout-style="collapsed"][data-hover="active"] .hk-menu:hover .menu-header > span .navbar-brand .brand-img.uh-sidebar-brand-icon {
    display: inline-flex;
  }
}


.uh-auth-brand .navbar-brand {
  letter-spacing: -.01em;
}

/* Typography: stronger title, calmer subtitle line-height than the vendor
 * default (which is body copy's 1.5 — fine for paragraphs, slightly loose
 * for a single centered sentence under a heading). */
.uh-auth-title {
  font-size: 1.375rem;
  font-weight: 700;
  letter-spacing: -.01em;
  margin-bottom: .375rem;
}

.uh-auth-subtitle {
  font-size: .875rem;
  line-height: 1.45;
}

.uh-auth-label {
  font-size: .875rem;
  font-weight: 600;
  letter-spacing: .01em;
  color: var(--bs-secondary-color);
}

/* Inputs. `.hk-pg-auth` was already explicitly excluded from the app-wide
 * compact `.form-control` sizing near the top of this file, so this is a
 * clean surface — a larger click target and an enterprise-SaaS-style focus
 * ring, not a size fight with that other rule.
 *
 * font-size set explicitly to this app's own established compact-input
 * tier (.875rem/14px, matching .uh-auth-label above and the app-shell
 * .form-control block further up this file) — at the vendor bundle's
 * un-overridden base (1rem/16px), auth-page input text read noticeably
 * larger than every other input in the app, including its own label.
 * min-height/padding trimmed slightly to match (2.75rem/.625rem-.875rem
 * -> 2.5rem/.5rem-.875rem) so the now-smaller text still sits in a
 * comfortably proportioned box instead of floating in space sized for the
 * old, larger text — still a noticeably roomier click target than the
 * dense app-shell tier's own box (this remains a public, first-impression
 * page), just no longer oversized. */
.hk-pg-auth .form-control {
  min-height: 2.5rem;
  padding-top: .5rem;
  padding-bottom: .5rem;
  padding-left: .875rem;
  padding-right: .875rem;
  font-size: .875rem;
  border-radius: .625rem;
  border-color: var(--hk-border-primary);
  transition: border-color .18s ease, box-shadow .18s ease, background-color .18s ease;
}

.hk-pg-auth .form-control:hover:not(:disabled):not(:focus) {
  border-color: var(--hk-border-secondary);
}

.hk-pg-auth .form-control:focus {
  border-color: var(--bs-primary);
  box-shadow: 0 0 0 .2rem rgba(var(--bs-primary-rgb), .15);
}

.hk-pg-auth .form-control::placeholder {
  color: var(--bs-tertiary-color);
}

.hk-pg-auth .form-control:disabled {
  background: var(--bs-tertiary-bg);
  opacity: .65;
  cursor: not-allowed;
}

/* Verification-code input: wider letter-spacing and a centered, slightly
 * larger monospace-adjacent size — the common "this is a code, not a
 * sentence" treatment (Stripe/Auth0 OTP fields). */
.uh-auth-otp-input {
  text-align: center;
  font-size: 1.125rem;
  letter-spacing: .35em;
  text-indent: .35em;
}

/* Password field + its inline visibility toggle. The toggle is an
 * absolutely-positioned button *inside* the same wrapper as the input
 * (not a sibling row), so it can never affect layout/height — exactly the
 * "does not affect layout" requirement. */
.uh-password-field {
  position: relative;
}

.uh-password-field .form-control {
  padding-right: 2.75rem;
}

.uh-password-toggle {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 2.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  padding: 0;
  color: var(--bs-tertiary-color);
  cursor: pointer;
  border-radius: 0 .625rem .625rem 0;
  transition: color .18s ease, background-color .18s ease;
}

.uh-password-toggle:hover {
  color: var(--bs-secondary-color);
  background-color: var(--bs-tertiary-bg);
}

.uh-password-toggle:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: -2px;
}

/* Two stacked icons, cross-faded by `aria-pressed` (set in login.js) rather
 * than swapped via innerHTML — keeps the toggle a pure CSS state change. */
.uh-password-toggle-icon {
  display: inline-flex;
  transition: opacity .18s ease;
}

.uh-password-toggle-icon--hide {
  display: none;
}

.uh-password-toggle[aria-pressed="true"] .uh-password-toggle-icon--show {
  display: none;
}

.uh-password-toggle[aria-pressed="true"] .uh-password-toggle-icon--hide {
  display: inline-flex;
}

/* Password Strength — live requirement checklist + strength meter for a
 * "new password" field (components/password-strength.js), ported from
 * dev-ui/assets/css/dev.css verbatim. Collapsed to just the meter's
 * resting 2px track until the field has content
 * (.uh-password-strength--visible, set by JS on first keystroke), so an
 * empty form doesn't open with a wall of gray checklist text. */
.uh-password-strength {
  margin-top: .625rem;
}

.uh-password-strength-meter {
  height: 3px;
  border-radius: 2px;
  background-color: var(--bs-tertiary-bg);
  overflow: hidden;
}

.uh-password-strength-meter-bar {
  display: block;
  height: 100%;
  width: 0%;
  border-radius: inherit;
  background-color: var(--bs-danger);
  transition: width .2s ease, background-color .2s ease;
}

.uh-password-strength-meter-bar[data-strength="3"] {
  background-color: var(--bs-warning);
}

.uh-password-strength-meter-bar[data-strength="4"],
.uh-password-strength-meter-bar[data-strength="5"] {
  background-color: var(--bs-success);
}

.uh-password-strength-list {
  list-style: none;
  margin: .625rem 0 0;
  padding: 0;
  display: none;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .375rem .75rem;
}

.uh-password-strength--visible .uh-password-strength-list {
  display: grid;
}

.uh-password-strength-item {
  display: flex;
  align-items: center;
  gap: .4rem;
  font-size: .75rem;
  color: var(--bs-tertiary-color);
  transition: color .18s ease;
}

.uh-password-strength-icon-dot {
  flex: none;
  width: .4rem;
  height: .4rem;
  border-radius: 50%;
  background-color: var(--bs-tertiary-color);
  transition: background-color .18s ease;
}

.uh-password-strength-icon-check {
  display: none;
  flex: none;
  color: var(--bs-success);
}

.uh-password-strength-item.is-met {
  color: var(--bs-success);
}

.uh-password-strength-item.is-met .uh-password-strength-icon-dot {
  display: none;
}

.uh-password-strength-item.is-met .uh-password-strength-icon-check {
  display: inline-flex;
}

.uh-password-strength-status {
  margin: .5rem 0 0;
  font-size: .75rem;
  font-weight: 600;
  color: var(--bs-tertiary-color);
  min-height: 1em;
}

.uh-password-strength-status--strong {
  color: var(--bs-success);
}

.uh-password-strength-status--strong::before {
  content: '✓ ';
}

/* Primary button: same vendor .btn-primary recipe underneath, refined with
 * enterprise radius, a touch more weight/tracking on the (already
 * uppercase) label, and calm hover/active motion instead of the vendor
 * bundle's flat instant-swap. */
.hk-pg-auth .btn-primary {
  position: relative;
  border-radius: .625rem;
  padding-top: .75rem;
  padding-bottom: .75rem;
  font-weight: 600;
  letter-spacing: .03em;
  transition: transform .18s ease, box-shadow .18s ease, filter .18s ease;
}

.hk-pg-auth .btn-primary:hover:not(:disabled) {
  filter: brightness(1.06);
  box-shadow: 0 6px 16px rgba(var(--bs-primary-rgb), .28);
}

.hk-pg-auth .btn-primary:active:not(:disabled) {
  transform: translateY(1px);
  filter: brightness(.98);
}

.hk-pg-auth .btn-primary:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 2px;
}

/* Loading state: guardSubmit() (components/form.js) already toggles this
 * exact `.disabled` class on the submit button for the duration of the
 * request — this only styles that existing state, no JS change needed.
 * Label is hidden (not removed) so button width never shifts. */
.hk-pg-auth .btn-primary.disabled {
  color: transparent;
  filter: none;
}

.hk-pg-auth .btn-primary.disabled::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1rem;
  height: 1rem;
  margin: -.5rem 0 0 -.5rem;
  border: 2px solid rgba(255, 255, 255, .35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: uh-auth-spin .6s linear infinite;
}

@keyframes uh-auth-spin {
  to { transform: rotate(360deg); }
}

.btn-flush-primary {
  transition: color .18s ease, background-color .18s ease;
}

/* Hero (right-hand informational panel) — "infrastructure mesh" visual
 * pass ported verbatim from dev-ui/assets/css/dev.css, replacing the
 * previous single-SVG line/node illustration. Three coordinated,
 * still entirely pure-CSS/inline-SVG layers instead of one:
 *   1. .uh-auth-hero-mesh — a soft, slowly-drifting color wash with its
 *      own depth vignette and [data-bs-theme=dark] tuning.
 *   2. .uh-auth-hero-grid — a very faint engineering/blueprint grid,
 *      masked to fade out near the copy and concentrate toward the
 *      panel's edges.
 *   3. .uh-auth-hero-network — a small hand-placed inline SVG (login.php):
 *      two loose node clusters joined by one long connection, evoking
 *      distributed API infrastructure, at low opacity in the panel's
 *      quiet lower-right. Uses stroke/fill="currentColor" so its color
 *      tracks theme automatically via the `color:` set below.
 * All three are zero network weight and infinitely crisp at any
 * resolution, same as the illustration they replace. */
.uh-auth-hero {
  z-index: 0;
  overflow: hidden;
}

.uh-auth-hero .auth-content > .row {
  width: 100%;
}

.uh-auth-hero-mesh {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  background:
    radial-gradient(ellipse 90% 80% at 50% 50%, transparent 55%, rgba(0, 0, 0, .05) 100%),
    radial-gradient(circle at 50% 65%, rgba(var(--bs-primary-light-rgb), .16), transparent 55%);
}

.uh-auth-hero-mesh::before,
.uh-auth-hero-mesh::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  filter: blur(60px);
}

.uh-auth-hero-mesh::before {
  top: -12%;
  left: -8%;
  width: 52%;
  height: 42%;
  background: rgba(var(--bs-primary-rgb), .20);
}

.uh-auth-hero-mesh::after {
  right: -12%;
  bottom: -15%;
  width: 58%;
  height: 50%;
  background: rgba(var(--bs-info-rgb), .10);
}

[data-bs-theme="dark"] .uh-auth-hero-mesh {
  background:
    radial-gradient(ellipse 90% 80% at 50% 50%, transparent 45%, rgba(0, 0, 0, .22) 100%),
    radial-gradient(circle at 50% 65%, rgba(var(--bs-primary-light-rgb), .10), transparent 55%);
}

[data-bs-theme="dark"] .uh-auth-hero-mesh::before {
  background: rgba(var(--bs-primary-rgb), .38);
}

[data-bs-theme="dark"] .uh-auth-hero-mesh::after {
  background: rgba(var(--bs-info-rgb), .16);
}

@media (prefers-reduced-motion: no-preference) {
  .uh-auth-hero-mesh::before {
    animation: uh-hero-mesh-drift-a 20s ease-in-out infinite;
  }

  .uh-auth-hero-mesh::after {
    animation: uh-hero-mesh-drift-b 26s ease-in-out infinite;
  }
}

@keyframes uh-hero-mesh-drift-a {
  0%, 100% { transform: translate(0, 0); }
  50% { transform: translate(4%, 6%); }
}

@keyframes uh-hero-mesh-drift-b {
  0%, 100% { transform: translate(0, 0); }
  50% { transform: translate(-5%, -4%); }
}

.uh-auth-hero-grid {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(rgba(var(--bs-primary-rgb), .05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(var(--bs-primary-rgb), .05) 1px, transparent 1px);
  background-size: 40px 40px;
  mask-image: radial-gradient(ellipse 68% 62% at 42% 46%, rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 1) 88%);
  -webkit-mask-image: radial-gradient(ellipse 68% 62% at 42% 46%, rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 1) 88%);
}

[data-bs-theme="dark"] .uh-auth-hero-grid {
  background-image:
    linear-gradient(rgba(255, 255, 255, .06) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, .06) 1px, transparent 1px);
}

.uh-auth-hero-network {
  position: absolute;
  z-index: -1;
  right: -8%;
  bottom: -12%;
  width: 58%;
  aspect-ratio: 1;
  color: rgba(var(--bs-primary-rgb), .4);
  pointer-events: none;
}

.uh-auth-hero-network svg {
  display: block;
  width: 100%;
  height: 100%;
}

[data-bs-theme="dark"] .uh-auth-hero-network {
  color: rgba(var(--bs-primary-light-rgb), .55);
}

.uh-auth-hero-glow {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(circle at 50% 30%, rgba(var(--bs-primary-rgb), .08), transparent 60%);
  pointer-events: none;
}

[data-bs-theme="dark"] .uh-auth-hero-glow {
  background: radial-gradient(circle at 50% 30%, rgba(var(--bs-primary-rgb), .16), transparent 60%);
}

.uh-auth-hero-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: .75rem;
  background: rgba(var(--bs-primary-rgb), .12);
  /* --bs-primary-text-emphasis, not --bs-primary — see
   * .uh-dashboard-kpi-icon's own comment (dark-mode contrast). */
  color: var(--bs-primary-text-emphasis);
  margin-bottom: 1rem;
}

.uh-auth-hero-title {
  font-size: 2rem;
  line-height: 1.2;
  font-weight: 700;
  letter-spacing: -.02em;
  margin-bottom: 2rem;
  text-wrap: balance;
}

.uh-auth-hero-text {
  font-size: .9375rem;
  line-height: 1.65;
  max-width: 28rem;
}

/* Admin Portal Mail module (mail/index.php, mail/history.php).
 * All colors use existing Bootstrap CSS custom properties (--bs-*), so
 * these inherit light/dark theming from the shared token layer with no
 * per-theme overrides — the same discipline every other admin.css block
 * follows. */
.uh-mail-recipient-option {
  cursor: pointer;
  border-color: var(--bs-border-color) !important;
  transition: border-color .15s ease, background-color .15s ease;
}
.uh-mail-recipient-option:hover {
  background-color: var(--bs-tertiary-bg);
}
.uh-mail-recipient-option:has(input:checked) {
  border-color: var(--bs-primary) !important;
  background-color: var(--bs-tertiary-bg);
}

.uh-mail-preview-frame {
  background-color: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: .5rem;
  padding: 1.25rem 1.5rem;
  max-height: 60vh;
  overflow-y: auto;
}
.uh-mail-preview-body {
  font-size: .95rem;
  line-height: 1.65;
  color: var(--bs-body-color);
  word-break: break-word;
}
.uh-mail-preview-body p {
  margin: 0 0 1rem;
}
.uh-mail-preview-body p:last-child {
  margin-bottom: 0;
}

.uh-mail-chip-remove {
  font-size: .6rem;
  padding: .15rem;
  margin-left: .1rem;
}

/* WYSIWYG rich editor (Mail composer — components/rich-editor.js).
 * All colors are existing --bs-* tokens so it themes in light/dark with
 * no per-theme overrides. */
.uh-rte {
  border: 1px solid var(--bs-border-color);
  border-radius: .5rem;
  background-color: var(--bs-body-bg);
  overflow: hidden;
}
.uh-rte-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .25rem;
  padding: .4rem .5rem;
  border-bottom: 1px solid var(--bs-border-color);
  background-color: var(--bs-tertiary-bg);
}
.uh-rte-group {
  display: inline-flex;
  align-items: center;
  gap: .15rem;
  padding-right: .35rem;
  margin-right: .1rem;
  border-right: 1px solid var(--bs-border-color);
}
.uh-rte-group:last-child {
  border-right: 0;
  padding-right: 0;
  margin-right: 0;
}
.uh-rte-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.9rem;
  height: 1.9rem;
  padding: 0 .35rem;
  border: 0;
  border-radius: .35rem;
  background: transparent;
  color: var(--bs-body-color);
  cursor: pointer;
  transition: background-color .12s ease, color .12s ease;
}
.uh-rte-btn:hover {
  background-color: var(--bs-secondary-bg);
}
.uh-rte-btn.is-active {
  background-color: var(--bs-primary);
  color: #fff;
}
.uh-rte-btn-text {
  font-size: .8rem;
  font-weight: 600;
}
.uh-rte-surface {
  min-height: 16rem;
  max-height: 32rem;
  overflow-y: auto;
  padding: .9rem 1rem;
  font-size: .95rem;
  line-height: 1.6;
  color: var(--bs-body-color);
  outline: none;
}
.uh-rte-surface:empty::before {
  content: attr(data-placeholder);
  color: var(--bs-secondary-color);
  pointer-events: none;
}
.uh-rte-surface:focus {
  box-shadow: inset 0 0 0 2px rgba(var(--bs-primary-rgb), .25);
}
/* Editor + email-body content typography (shared by the editor surface,
 * the composer preview, and the history detail body). */
.uh-rte-surface h1, .uh-mail-preview-body h1, .uh-mail-detail-body h1 { font-size: 1.5rem; font-weight: 700; margin: .2rem 0 .6rem; }
.uh-rte-surface h2, .uh-mail-preview-body h2, .uh-mail-detail-body h2 { font-size: 1.25rem; font-weight: 700; margin: .2rem 0 .5rem; }
.uh-rte-surface h3, .uh-mail-preview-body h3, .uh-mail-detail-body h3 { font-size: 1.1rem; font-weight: 600; margin: .2rem 0 .5rem; }
.uh-rte-surface p, .uh-mail-preview-body p, .uh-mail-detail-body p { margin: 0 0 .75rem; }
.uh-rte-surface ul, .uh-rte-surface ol,
.uh-mail-preview-body ul, .uh-mail-preview-body ol,
.uh-mail-detail-body ul, .uh-mail-detail-body ol { margin: 0 0 .75rem; padding-left: 1.5rem; }
.uh-rte-surface blockquote, .uh-mail-preview-body blockquote, .uh-mail-detail-body blockquote {
  margin: 0 0 .75rem;
  padding-left: .9rem;
  border-left: 3px solid var(--bs-border-color);
  color: var(--bs-secondary-color);
}
.uh-rte-surface a, .uh-mail-preview-body a, .uh-mail-detail-body a { color: var(--bs-primary); }

/* History detail body panel */
.uh-mail-detail-body {
  background-color: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: .5rem;
  padding: 1rem 1.25rem;
  font-size: .95rem;
  line-height: 1.6;
  color: var(--bs-body-color);
  word-break: break-word;
}

/* Clearer clickable-row interaction state (Mail History + any rowAction
 * table). .uh-row-clickable already sets cursor:pointer; add a hover
 * background so a clickable row reads as interactive. */
.uh-row-clickable:hover > td {
  background-color: var(--bs-tertiary-bg);
}

/* Searchable Select / combobox (components/searchable-select.js) — used
 * by the commission-rule scope selectors. Colors are existing --bs-*
 * tokens, so it inherits light/dark theming with no per-theme block. */
.uh-combobox {
  position: relative;
}
.uh-combobox-menu {
  position: absolute;
  z-index: 1056; /* above the modal body */
  top: calc(100% + 2px);
  left: 0;
  right: 0;
  max-height: 15rem;
  overflow-y: auto;
  background-color: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: .5rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .12);
  padding: .25rem;
}
.uh-combobox-option {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  padding: .45rem .6rem;
  border-radius: .35rem;
  cursor: pointer;
}
.uh-combobox-option:hover,
.uh-combobox-option.is-active {
  background-color: var(--bs-tertiary-bg);
}
.uh-combobox-option-label {
  font-size: .9rem;
  color: var(--bs-body-color);
}
.uh-combobox-option-sub {
  font-size: .75rem;
  color: var(--bs-secondary-color);
}
.uh-combobox-empty,
.uh-combobox-loading,
.uh-combobox-error {
  padding: .6rem;
  font-size: .8rem;
  color: var(--bs-secondary-color);
  text-align: center;
}
.uh-combobox-error {
  color: var(--bs-danger);
}

/* ---------------------------------------------------------------------
 * Support Inquiries — the public contact form's submissions
 * (support-inquiries/inquiry.php)
 * ------------------------------------------------------------------- */

/* The submitter's own message body. Same recipe as
   .uh-ticket-message-text above (the equivalent block on the client
   support ticket thread): pre-wrap is what preserves the paragraph
   breaks a person typed, without the message ever being rendered as
   markup — support-inquiry-detail.js writes it with textContent, never
   innerHTML, precisely because this is unauthenticated public input.
   word-break guards against a pasted, unbroken URL or token widening
   the card past its column. */
.uh-inquiry-message {
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
  font-size: .9375rem;
  line-height: 1.6;
}

/* ---------------------------------------------------------------------
 * Blog editor (blog/post.php)
 * ------------------------------------------------------------------- */

/* The article surface needs far more room than the Mail composer's,
   which .uh-rte-surface is sized for. A tall min-height with vertical
   growth means a long article does not force the author to write through
   a letterbox, while still scrolling the page rather than the editor. */
.uh-rte-surface--tall {
  min-height: 32rem;
  max-height: none;
}

/* Featured image preview. object-fit keeps a portrait upload from
   stretching the sidebar card out of shape. */
.uh-post-image {
  margin-bottom: .875rem;
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  overflow: hidden;
  background: var(--bs-tertiary-bg);
}

.uh-post-image img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* Code blocks and images inside the editor surface, so what the author
   sees while writing matches how the article renders on the public site
   rather than appearing as unstyled text. */
.uh-rte-surface pre {
  background: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  padding: .75rem .875rem;
  overflow-x: auto;
  font-family: var(--font-ui-mono, monospace);
  font-size: .8125rem;
}

.uh-rte-surface figure {
  margin: 1rem 0;
}

.uh-rte-surface img {
  max-width: 100%;
  height: auto;
  border-radius: var(--bs-border-radius);
  display: block;
}

.uh-rte-surface blockquote {
  margin: 1rem 0;
  padding-left: 1rem;
  border-left: 3px solid var(--bs-border-color);
  color: var(--bs-secondary-color);
}

/* ---------------------------------------------------------------------
 * Changelog (admin-ui/changelog/)
 * ------------------------------------------------------------------- */

/* The summary under each entry title in the list.
 *
 * Not .uh-table-truncate: that class caps width but does not clamp, so a
 * 400 character summary wrapped to five lines and made every row tall
 * enough that only four entries fitted on screen. A two line clamp keeps
 * rows even and still shows enough to tell entries apart. */
.uh-cl-summary-cell {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 30rem;
  line-height: 1.45;
}
