/* ─────────────────────────────────────────────────────────────────────────
   HRMS Design System — custom properties & components
───────────────────────────────────────────────────────────────────────── */
:root {
  --primary:       #4f46e5;
  --primary-dark:  #3730a3;
  --primary-light: #eef2ff;
  --accent:        #0ea5e9;
  --success:       #10b981;
  --warning:       #f59e0b;
  --danger:        #ef4444;
  --info:          #0ea5e9;
  --bg:            #f1f5f9;
  --surface:       #ffffff;
  --text:          #1e293b;
  --muted:         #617187;
  /* Bumped from #e2e8f0 — that read as nearly invisible against --surface
     (#fff)/--bg (#f1f5f9), especially on cards and form inputs where a
     visible edge actually matters (WCAG 1.4.11 non-text contrast wants
     ~3:1 against adjacent colors; #e2e8f0 was closer to 1.2:1). */
  --border:        #cbd5e1;
  /* Control boundaries, held to WCAG 1.4.11's 3:1 against --surface.
     --border above stays deliberately subtle: it draws dividers and card
     edges, which are decoration, not "information required to identify a
     UI component". Darkening that one to 3:1 would coarsen every hairline in
     the product to satisfy a rule it isn't subject to. See
     apps/system_settings/preset_audit.py for the measurements. */
  --border-strong: #8e959e;
  --radius:        .75rem;
  --shadow-sm:     0 1px 3px 0 rgb(0 0 0/.08), 0 1px 2px -1px rgb(0 0 0/.08);
  --shadow:        0 4px 6px -1px rgb(0 0 0/.08), 0 2px 4px -2px rgb(0 0 0/.08);
  --shadow-lg:     0 10px 15px -3px rgb(0 0 0/.08), 0 4px 6px -4px rgb(0 0 0/.08);

  /* ── Accessible text variants of the semantic colours ───────────────────
     Added 2026-08-04 after auditing all 21 appearance presets against WCAG
     2.2 (apps/system_settings/preset_audit.py). The audit found 81 failures
     across 188 checks with NOT ONE preset fully clean, and the same three
     offenders in every single one:

         --success #10b981 as text on white   2.54:1   (needs 4.5)
         --warning #f59e0b as text on white   2.15:1
         --danger  #ef4444 as text on white   3.76:1

     Worse in the real rendering context: the status badge draws text in the
     tone over a 12% tint OF THAT TONE, so the actual measured ratios were
     2.27 / 1.96 / 3.23 -- warning text was under 2:1 on a shipped component.

     The vivid values stay as-is because they are correct for FILLS (meter
     bars, badge tints, icon chips), where 1.4.11's 3:1 non-text rule applies,
     not 1.4.3's 4.5:1. Darkening them globally would have muddied every fill
     in the product to fix a text problem.

     So: two roles, two tokens. Use --*-text wherever the colour is the TEXT,
     and the plain token wherever it is a fill. Values were solved against the
     actual tinted background, not against pure --surface, and verified by
     apps/system_settings/tests/test_preset_contrast.py. */
  /* Primary-family text sitting ON a primary tint (not on a plain surface).
     --primary itself is a fill/link colour: at .12-.18 alpha over white the
     tint lifts the background enough that --primary lands at 3.8-4.0:1,
     under AA. Same split, and same reason, as --success-text vs --success. */
  --primary-text: #4f46e5;
  /* The text drawn ON a --primary FILL. Third role of the same hue, and the
     one that was missing.

     --primary is the fill and the link colour; --primary-text is the accent
     at a value safe as text on a SURFACE. Neither answers "what colour is the
     label inside a solid primary button", and Bootstrap simply hardcodes
     white. On the 21 light-ish presets white is right (4.83–21:1). On the two
     presets whose accent LIGHTENS for a dark page it is not: the `dark`
     preset's #818CF8 measured 2.98:1 and glassmorphism's #38BDF8 2.14:1 —
     i.e. the product's primary call to action failed AA on every page for
     anyone using either.

     Darkening --primary instead was rejected for the reason glassmorphism's
     own (now-removed) one-off already recorded: the same token is excellent
     link text on a dark page, and dulling the accent everywhere to satisfy
     buttons trades a real gain for a real loss. Splitting the role costs one
     token and changes nothing for the 21 presets that inherit the default.

     Checked by apps/system_settings/preset_audit.py so a new preset cannot
     quietly reintroduce it. */
  --on-primary:   #ffffff;
  /* Darkened 2026-08-18. These pass comfortably as text on --surface (white),
     which is the only ground the audit used to check -- but the pattern that
     actually ships is a STATUS CHIP: a ~14% tint of the vivid counterpart,
     painted on the page canvas. Two grounds stack under the text, and
     unwrapping the marketing section panels moved those chips off --surface
     onto --bg (#ECF0F1 on Flat), darkening the composite a second time.
     Measured live: success 3.82:1, danger 3.88:1, warning 3.98:1, info 4.13:1
     -- all under AA while "success text on surface" reported a healthy pass.

     Values below are the least-dark ones that clear 4.5:1 on tint-over---bg
     for ALL 21 light presets, not just the default. `skeuomorphism` sets the
     bar: its canvas is a dim beige (#D9D2C5, luminance .65) rather than the
     near-white the others use, so a tint composited on it starts far darker.
     One global set rather than per-preset overrides -- the spread between
     presets is a few percent, and four tokens in one place beat 32 scattered
     ones. Each still GAINS contrast on white (7.1-7.8:1), so no existing
     usage gets worse; they read as deep green/amber/red/blue, not as black.
     Guarded now by the "* text on its own tint" pairs in preset_audit.py. */
  --success-text: #085f43;
  --warning-text: #794f05;
  --danger-text:  #962b2b;
  --info-text:    #085e85;

  /* ── Spacing scale ──────────────────────────────────────────────────────
     Added 2026-08-04. There was NO spacing token of any kind before this —
     every margin and padding in the app was an ad-hoc rem value, which is
     the single biggest reason pages don't line up with each other and why
     "compact" could not be applied globally (see
     docs/frontend-design-system.md §2, gap #1).

     A 4px base on a roughly-1.5x ramp, chosen to match the values already
     most common in the codebase rather than an invented geometric scale —
     so adopting it is mostly relabelling what pages already do, not
     restyling them.

     These are DECLARATIVE only: nothing is retrofitted here. Templates
     adopt them as they migrate (implementation order step 3), which is what
     keeps this change zero-risk. */
  --space-1: .25rem;   /*  4px */
  --space-2: .5rem;    /*  8px */
  --space-3: .75rem;   /* 12px */
  --space-4: 1rem;     /* 16px */
  --space-5: 1.5rem;   /* 24px */
  --space-6: 2rem;     /* 32px */
  --space-7: 3rem;     /* 48px */
  --space-8: 4rem;     /* 64px */

  /* ── Radius scale ───────────────────────────────────────────────────────
     --radius is the authoritative MEDIUM step: it predates this scale, is
     used in ~40 places, and every appearance preset overrides it. The whole
     ramp is derived from it so a preset drives all of them at once.

     --radius-xs/-sm/-lg are NOT declared here. They read var(--radius), and
     a custom property is substituted against the element that DECLARES it —
     from :root they would resolve against :root's own default-preset .75rem
     and freeze there, which is exactly the bug that kept every Bootstrap
     component on the default geometry under all 22 presets. They live in the
     derived block below, which names :root AND the element the presets
     declare --radius on. --radius-md and --radius-pill stay here: the first
     is a bare alias (no arithmetic, so substitution position does not matter
     for what it resolves to at the point of USE), the second is a constant.

     --radius-sm was .375rem and --radius-lg 1.25rem as fixed literals before
     that block existed; they are now .7x and 1.35x, chosen to reproduce the
     .5rem/1rem the components using them had written by hand. */
  --radius-md:   var(--radius);
  --radius-pill: 999px;

  /* ── Z-index scale ──────────────────────────────────────────────────────
     Stacking was previously guesswork — literal z-index values appear
     scattered across page-local <style> blocks with no shared ordering, so
     "which thing wins" was decided by whoever wrote the bigger number.

     Values deliberately match Bootstrap 5's own scale (dropdown 1000, sticky
     1020, fixed 1030, backdrop 1040, modal 1050, popover 1070, tooltip 1080)
     so our components and Bootstrap's stack together predictably instead of
     fighting. --z-toast sits above modal on purpose: a toast confirming an
     action taken INSIDE a modal must not appear behind it. */
  --z-base:     0;
  --z-raised:   10;
  --z-dropdown: 1000;
  --z-sticky:   1020;
  --z-fixed:    1030;
  --z-backdrop: 1040;
  --z-modal:    1050;
  --z-popover:  1070;
  --z-toast:    1090;

  /* ── Portal appearance system ──────────────────────────────────────────
     The sidebar palette for the "Simple" preset, which is the only preset
     that does not state one of its own.

     These used to be the baseline under a SECOND control -- a "sidebar
     accent" (data-portal-theme) offering six alternative sidebar palettes,
     layered on top of whichever preset was chosen. It existed because the
     presets were largely colour swaps of each other and needed something to
     make a choice feel like it had done anything. It was also almost always
     inert: every preset except Simple sets these same tokens with a higher
     specificity, so the accent silently lost. Both production tenants were
     on a non-default accent and neither could see it.

     The presets now differ structurally, so the accent was removed rather
     than fixed. Every preset below overrides these tokens. */
  --sidebar-bg:                  #1e293b;
  --sidebar-text:                #94a3b8;
  --sidebar-text-hover:          #e2e8f0;
  --sidebar-text-active:         #c7d2fe;
  --sidebar-icon-active:         #a5b4fc;
  --sidebar-hover-bg:            rgba(255,255,255,.05);
  --sidebar-active-bg:           rgba(99,102,241,.14);
  --sidebar-active-border:       #818cf8;
  --sidebar-section-label:       #8e9aaa;
  --sidebar-section-label-hover: #cbd5e1;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.04);
  --sidebar-group-accent:        rgba(148,163,184,.16);
  --sidebar-border:              rgba(255,255,255,.06);

  --portal-font-family: 'Inter', system-ui, -apple-system, sans-serif;
  --portal-font-size:   .9375rem;
  --portal-line-height: 1.6;
  --sidebar-font-size:  .875rem;

  /* ── Type scale (Phase 4 of the UI audit) ───────────────────────────────
     Templates carried 1,295 inline `font-size` declarations spanning 121
     distinct values across 186 files. Those were not 121 intentions -- they
     clustered tightly as near-duplicates of about eight sizes (.7/.72/.75/
     .76 all meaning "small", .78/.8/.82 all meaning "dense UI"). Each step
     below is the modal value of one real observed cluster, so adopting the
     scale is a rounding operation rather than a redesign.

     Deliberately rem, not em. em would let the tenant's --portal-font-size
     flow through automatically, which is tempting, but --portal-font-size
     is set on `body`, not `html` -- so an em step resolves against its
     INHERITED size and compounds whenever one sized element nests inside
     another (a .fs-xs span inside a .fs-sm cell would land at .75 x .875,
     not .75). Across 1,184 existing call sites that is a guaranteed silent
     shrink. rem keeps every step absolute and identical to what ships now.

     Per-theme font sizing is therefore done the way every other token here
     varies: a preset block overrides these values directly, exactly as the
     density presets already override --portal-font-size further down this
     file. That is a real mechanism, not a deferral.

     Two steps are ANCHORED and must not move: --fs-xs (.75rem) and --fs-md
     (.875rem) are the literal values the pre-existing .fs-xs and .fs-sm
     utility classes already render at, across 439 and 745 template usages.
     Those classes are redefined below in terms of these tokens so every one
     of those call sites keeps its exact rendered size. (.fs-sm maps to
     --fs-md, not --fs-sm: a deliberate mismatch, because renaming the class
     would mean editing 745 call sites for no visual gain. The historical
     class name stays and the token naming is left honest.) */
  --fs-4xs:  .6rem;    /* absorbs .48 .55 .58 .6   — micro labels, chips  */
  --fs-3xs:  .65rem;   /* absorbs .62 .65 .68      — small labels         */
  --fs-2xs:  .7rem;    /* absorbs .7  .72          — meta, timestamps     */
  --fs-xs:   .75rem;   /* ANCHOR = .fs-xs          — secondary text       */
  --fs-sm:   .8rem;    /* absorbs .78 .8  .82      — dense UI             */
  --fs-md:   .875rem;  /* ANCHOR = .fs-sm          — table cells          */
  --fs-base: .9375rem; /* absorbs .9  .9375        — body                 */
  --fs-lg:   1.125rem; /* absorbs 1.1 1.15 1.125   — card titles          */
  --fs-xl:   1.5rem;   /* absorbs 1.4 1.5          — section headings     */
  --fs-2xl:  2rem;     /* absorbs 1.75 1.8 2       — page titles, stats   */

  --motion-fast: .15s;
  --motion-med:  .2s;
  --motion-slow: .3s;

  /* ── Preset "personality" tokens — Default (Indigo Tech) baseline ────────
     border-width, shadow-* and heading-font are what actually make each of
     the 6 Appearance presets FEEL different rather than just re-tinted.
     Every preset below overrides a subset of these; whatever it doesn't
     override falls back to this Default fingerprint: soft ambient shadows,
     a modest .75rem radius, 1px hairline borders, Inter throughout.
     Caution: never let an asterisk sit immediately before a slash anywhere
     in this comment block. That exact two-character pair is the CSS
     comment terminator, so it ends this comment wherever it appears — an
     earlier revision wrote "shadow" then a star then a slash then
     "heading-font" as a run-together shorthand, which closed this comment
     mid-sentence and silently deleted the --border-width declaration and
     its --bs-border-width mirror below. Every .form-control/.form-select
     sitewide rendered with a 0-width, style-none border as a result —
     invisible field outlines on every non-dark preset (dark preset hid it
     behind its own background-color contrast instead of a real border). */
  --border-width:           1px;
  --heading-font-family:    var(--portal-font-family);
  --letter-spacing-heading: -.02em;
  /* Standard "material" ease — brisk, neutral. Each preset below swaps
     this for a curve that fits its own personality (see hover transitions
     on .card/.feature-card/.msgc-row, which read this token). */
  --motion-easing: cubic-bezier(.4, 0, .2, 1);

  /* Mirrored onto Bootstrap 5.3's own root variables so every stock
     Bootstrap component (buttons, inputs, cards, tables, modals, dropdowns,
     accordions...) inherits each preset's identity automatically, without a
     bespoke override per component. Custom-property values resolve lazily,
     so a preset only ever needs to redeclare the left-hand tokens
     (--radius, --shadow, --border-width, --portal-font-family...) — these
     mirrors recompute on their own. */
  --bs-border-radius-pill: 999px;
}

/* ── The derived layer: every token that is COMPUTED from --radius, --shadow,
   --border-width or --portal-font-family ────────────────────────────────────

   Declared on :root AND on the element each preset declares those four on,
   and that duplication is the whole point.

   A custom property's var() references are substituted where the property is
   DECLARED, not where it is used. These mirrors used to sit on :root alone,
   so `--bs-border-radius: var(--radius)` resolved against :ROOT's --radius --
   the Default fingerprint's .75rem -- and froze there. Every preset declares
   --radius/--shadow on `body:is(.portal-page,…)`, which is BELOW :root, so no
   preset's radius or shadow has ever reached a single Bootstrap component.
   Measured on production with Flat Design selected: body's --radius read `0`
   and --shadow read `none`, while --bs-border-radius on that same body still
   read `.75rem`. Every card, button, input, modal and dropdown in the product
   was drawing the default preset's geometry whatever was selected.

   Same defect, same fix, and the same reasoning already written out at length
   for --portal-muted in portal-compact.css. See [[public-site-follows-ui-preset]].

   --radius-xs/-sm/-lg are the product's own scale, factored out of ~40 literal
   radii scattered through this file. The multipliers are chosen so the Default
   preset is byte-for-byte unchanged: .75rem * .7 = .525rem against the .5/.55rem
   those rules wrote by hand. */
:root,
body:is(.portal-page,.public-cms-page,.themed-page),
body.auth-page {
  --radius-xs: calc(var(--radius) * .45);
  --radius-sm: calc(var(--radius) * .7);
  --radius-lg: calc(var(--radius) * 1.35);

  /* The brand fill for the product's few "hero" surfaces (the employee
     dashboard's avatar ring and attendance card). Both wrote
     `linear-gradient(135deg, var(--primary) 0%, #7c3aed 100%)` — half themed,
     half a fixed violet, so on every preset whose accent is not violet the
     card ended somewhere the palette never goes. Under Flat the gradient is
     wrong twice over: the style has no gradients at all. Flat overrides this
     to a flat fill in its own block below; any other preset can too. */
  --brand-gradient: linear-gradient(135deg, var(--primary) 0%,
                    color-mix(in srgb, var(--primary) 45%, var(--accent)) 100%);

  --bs-border-radius:      var(--radius);
  --bs-border-radius-sm:   calc(var(--radius) * .66);
  --bs-border-radius-lg:   calc(var(--radius) * 1.34);
  --bs-border-radius-xl:   calc(var(--radius) * 1.75);
  --bs-border-width:       var(--border-width);
  --bs-body-font-family:   var(--portal-font-family);
  --bs-box-shadow:         var(--shadow);
  --bs-box-shadow-sm:      var(--shadow-sm);
  --bs-box-shadow-lg:      var(--shadow-lg);

  --bs-card-border-color:        var(--border);
  --bs-card-border-radius:       var(--radius);
  --bs-card-inner-border-radius: var(--radius-sm);
  --bs-card-cap-bg:              transparent;
}

/* ── Buttons follow the active theme ───────────────────────────────────────
   Bootstrap's compiled CSS bakes .btn-primary/.btn-success/etc.'s colors in
   as literal hex at build time — they don't actually read --bs-primary (or
   any other custom property) at runtime, so re-declaring that variable per
   preset silently changed nothing. Every "Publish"/"Save"/primary-action
   button sitewide stayed Bootstrap's stock blue/green/red regardless of
   which Appearance preset or accent theme was selected. These overrides use
   plain color properties (not Bootstrap's internal variable chain) so they
   win by cascade order alone — site.css always loads after the Bootstrap
   CDN link — and they reference this file's own theme tokens, so they
   already track every preset above and any future one added. Outline
   variants keep proper text/hover contrast the same way Bootstrap's own
   outline buttons do (filled on hover). */
/* --on-primary, not white: see its definition in :root. --primary-hover falls
   back to --primary-dark, so the 21 presets that do not set it are byte-for-
   byte unchanged; only the two whose accent lightens for a dark page need a
   hover that lightens too (darkening #818CF8 lands on #6366F1, where NEITHER
   white nor dark text clears AA — 4.47 and 4.22). */
.btn-primary {
  background-color: var(--primary); border-color: var(--primary);
  color: var(--on-primary);
}
.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-check:checked + .btn-primary {
  background-color: var(--primary-hover, var(--primary-dark));
  border-color: var(--primary-hover, var(--primary-dark));
  color: var(--on-primary);
}
/* NB: a SECOND `.btn-outline-primary` rule exists further down this file (search
   for "the two tokens answer different questions"). Same specificity, later in
   the cascade, so that one is what actually ships -- keep the two in step. */
.btn-outline-primary {
  color: var(--primary-text, var(--primary)); border-color: var(--primary);
}
.btn-outline-primary:hover, .btn-outline-primary:active {
  background-color: var(--primary); border-color: var(--primary);
  color: var(--on-primary);
}
/* Filled buttons carry WHITE text (Bootstrap's own rule), so the fill has to
   be dark enough for white to clear AA -- the raw hue is not. Measured in the
   browser: .btn-success was 2.54:1, .btn-info 2.77:1, .btn-danger 3.76:1, in
   every preset. (.btn-warning is fine and deliberately untouched: Bootstrap
   gives it DARK text, so darkening its fill would make it worse.)

   Derived with color-mix rather than a fixed hex so each button still tracks
   whatever hue the active preset defines. 70% is the weakest darkening that
   clears 4.5:1 for all 21 presets -- worst case 4.82:1 on default/success.

   NOT --success-text and friends: those are the *text-on-tint* variants, and
   on the dark preset they are LIGHTENED (#f36d6d, #15a8ea), which would make
   a white-text button worse rather than better. */
.btn-success {
  background-color: color-mix(in srgb, var(--success) 70%, black);
  border-color: color-mix(in srgb, var(--success) 70%, black);
}
.btn-success:hover, .btn-success:focus, .btn-success:active {
  background-color: color-mix(in srgb, var(--success) 58%, black); border-color: color-mix(in srgb, var(--success) 58%, black);
}
.btn-outline-success { color: var(--success-text, var(--success)); border-color: var(--success); }
.btn-outline-success:hover, .btn-outline-success:active {
  background-color: color-mix(in srgb, var(--success) 70%, black); border-color: color-mix(in srgb, var(--success) 70%, black); color: #fff;
}
.btn-danger {
  background-color: color-mix(in srgb, var(--danger) 70%, black);
  border-color: color-mix(in srgb, var(--danger) 70%, black);
}
.btn-danger:hover, .btn-danger:focus, .btn-danger:active {
  background-color: color-mix(in srgb, var(--danger) 85%, black); border-color: color-mix(in srgb, var(--danger) 85%, black);
}
.btn-outline-danger { color: var(--danger-text, var(--danger)); border-color: var(--danger); }
.btn-outline-danger:hover, .btn-outline-danger:active {
  background-color: color-mix(in srgb, var(--danger) 70%, black); border-color: color-mix(in srgb, var(--danger) 70%, black); color: #fff;
}
.btn-warning { background-color: var(--warning); border-color: var(--warning); color: #1c1917; }
.btn-warning:hover, .btn-warning:focus, .btn-warning:active {
  background-color: color-mix(in srgb, var(--warning) 85%, black); border-color: color-mix(in srgb, var(--warning) 85%, black); color: #1c1917;
}
.btn-outline-warning { color: var(--warning-text, var(--warning)); border-color: var(--warning); }
.btn-outline-warning:hover, .btn-outline-warning:active {
  background-color: var(--warning); border-color: var(--warning); color: #1c1917;
}

.btn-info {
  background-color: color-mix(in srgb, var(--info) 70%, black);
  border-color: color-mix(in srgb, var(--info) 70%, black);
  color: #fff;
}
.btn-outline-info { color: var(--info-text, var(--info)); border-color: var(--info); }
.btn-outline-info:hover, .btn-outline-info:active {
  background-color: color-mix(in srgb, var(--info) 70%, black); border-color: color-mix(in srgb, var(--info) 70%, black); color: #fff;
}
.btn-outline-secondary { color: var(--muted); border-color: var(--border); }
.btn-outline-secondary:hover, .btn-outline-secondary:active {
  background-color: var(--muted); border-color: var(--muted); color: #fff;
}
.btn-check:focus + .btn, .btn:focus {
  box-shadow: 0 0 0 .25rem color-mix(in srgb, var(--primary) 25%, transparent);
}


/* Every --border/--bs-border-color value in the preset blocks below was
   recomputed to clear the WCAG 1.4.11 non-text-contrast minimum (3:1)
   against that preset's own --surface/--bg — the original values (picked
   for a "subtle" look) landed between 1.4:1 and 2.5:1, so card edges, form
   fields, and table rows were effectively borderless on every non-dark
   preset. Each value keeps its preset's hue, just darkened until contrast
   hits ~3.6:1. */
/* ══════════════════════════════════════════════════════════════════════════
   UI PRESET THEMES  —  full-portal colour system
   Set via html[data-portal-preset="X"] from PlatformSettings.portal_ui_preset.
   Each preset overrides ALL colour tokens (body, surface, primary, sidebar).
   These come AFTER portal-theme presets so they win the cascade.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Preset: Light — airy & minimal ────────────────────────────────────
   Identity: generous 1.25rem radius, near-invisible ambient shadows, wide
   line-height and slightly loosened heading tracking — the "breathing
   room" preset. Border stays a crisp 1px so cards/inputs never look like
   they're floating with no edge at all. */
html[data-portal-preset="light"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="light"] body.auth-page {
  --bg:            #ECEEF1;
  --surface:       #F5F7FA;
  --text:          #2C3B52;
  --muted:         #5c6e82;
  /* Darkened again + widened to 1.5px — #B7C2D4 at 1px still read as
     "barely there" against this preset's near-white input/card
     backgrounds per direct feedback; #8CA0B8 (closer to Tailwind
     slate-400) is unambiguously a visible edge without looking heavy. */
  --border:        #738397;
  --border-strong: #888f98;
  --primary:       #5048E5;
  --primary-dark:  #3730C8;
  --primary-light: #EEEEFF;

  --radius:      1.25rem;
  --border-width: 1.5px;
  --shadow-sm: 0 1px 2px rgba(44,59,82,.04);
  --shadow:    0 2px 10px -2px rgba(44,59,82,.07);
  --shadow-lg: 0 10px 28px -6px rgba(44,59,82,.10);
  --portal-line-height:     1.7;
  --letter-spacing-heading: -.01em;
  --motion-easing: ease-out;

  --bs-body-bg:          #ECEEF1;
  --bs-body-color:       #2C3B52;
  --bs-secondary-color:  #5c6e82;  /* was #6A7E96: failed AA on this preset's own surface */
  --bs-border-color:     #738397;
  --bs-tertiary-bg:      #F5F7FA;
  --bs-link-color:       #5048E5;
  --bs-link-hover-color: #3730C8;

  --sidebar-bg:                  #F0F2F5;
  --sidebar-text:                #4E6176;
  --sidebar-text-hover:          #1E2A38;
  --sidebar-text-active:         #4338CA;
  --sidebar-icon-active:         #5048E5;
  --sidebar-hover-bg:            rgba(80,72,229,.07);
  --sidebar-active-bg:           rgba(80,72,229,.10);
  --sidebar-active-border:       #5048E5;
  --sidebar-section-label:       #5e6b7b;
  --sidebar-section-label-hover: #4E6176;
  --sidebar-toggle-hover-bg:     rgba(80,72,229,.05);
  --sidebar-group-accent:        rgba(80,72,229,.12);
  --sidebar-border:              rgba(0,0,0,.08);
}

/* ── Preset: Dark — full dark portal ──────────────────────────────────── */
/* Bootstrap dark mode activates automatically via data-bs-theme="dark"
   (set server-side + enforced by the anti-flash script in base.html).
   These tokens cover HRMS-specific non-Bootstrap elements.               */
html[data-portal-preset="dark"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="dark"] body.auth-page {
  --bg:            #0F1117;
  --surface:       #1A1D27;
  --text:          #DDE3EF;
  --muted:         #8895A8;
  --border:        rgba(255,255,255,.40);
  /* Accessible text variants must be restated here, not only under
     [data-bs-theme="dark"]. These are TWO independent mechanisms that both
     produce dark surfaces: data-bs-theme is the light/dark toggle, while
     data-portal-preset="dark" is one of the 14 appearance presets. A user on
     this preset with the toggle still on "light" gets a dark --surface but
     would otherwise inherit the LIGHT-optimised --*-text values from :root,
     which are darkened for white backgrounds and therefore fail AA here.
     Caught by apps/system_settings/tests/test_preset_contrast.py. */
  --primary-text: #a5b4fc;
  --success-text: var(--success);
  --warning-text: var(--warning);
  --info-text:    #15a8ea;
  --danger-text:  #f36d6d;
  --primary:       #818CF8;
  --primary-dark:  #6366F1;
  --primary-light: rgba(99,102,241,.18);

  /* Identity: an accent-colored halo instead of a neutral black shadow —
     the signature "glow" that reads as depth on a dark surface, where a
     plain gray drop-shadow would be nearly invisible. */
  --radius:      .625rem;
  --border-width: 1px;
  --shadow-sm: 0 0 0 1px rgba(129,140,248,.08), 0 1px 2px rgba(0,0,0,.45);
  --shadow:    0 0 0 1px rgba(129,140,248,.10), 0 8px 20px -4px rgba(129,140,248,.25), 0 4px 10px rgba(0,0,0,.45);
  --shadow-lg: 0 0 0 1px rgba(129,140,248,.14), 0 16px 40px -8px rgba(129,140,248,.35), 0 8px 20px rgba(0,0,0,.55);
  --letter-spacing-heading: -.025em;
  --motion-easing: cubic-bezier(.16, 1, .3, 1);

  --bs-body-bg:          #0F1117;
  --bs-body-color:       #DDE3EF;
  --bs-secondary-color:  #8895A8;
  --bs-border-color:     rgba(255,255,255,.40);
  --bs-tertiary-bg:      #1A1D27;
  --bs-secondary-bg:     #1E2230;
  --bs-link-color:       #818CF8;
  --bs-link-hover-color: #A5B4FC;

  /* This preset lightens the accent for a dark page, so white button text
     lands at 2.98:1. See --on-primary in :root. --primary-hover lightens
     rather than darkens for the same reason: --primary-dark (#6366F1) is a
     dead end here, failing under white (4.47) AND dark (4.22) text. */
  --on-primary:                  #0F1117;   /* 6.33:1 on --primary */
  --primary-hover:               #A5B4FC;   /* 9.47:1 under --on-primary */

  --sidebar-bg:                  #111318;
  --sidebar-text:                #8895A8;
  --sidebar-text-hover:          #DDE3EF;
  --sidebar-text-active:         #C7D2FE;
  --sidebar-icon-active:         #A5B4FC;
  --sidebar-hover-bg:            rgba(255,255,255,.05);
  --sidebar-active-bg:           rgba(129,140,248,.14);
  --sidebar-active-border:       #818CF8;
  --sidebar-section-label:       #798497;
  --sidebar-section-label-hover: #8895A8;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.04);
  --sidebar-group-accent:        rgba(165,180,252,.12);
  --sidebar-border:              rgba(255,255,255,.07);
}

/* ── Preset: Classic — traditional corporate navy ─────────────────────── */
html[data-portal-preset="classic"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="classic"] body.auth-page {
  --bg:            #EDF1F5;
  --surface:       #FFFFFF;
  --text:          #1A2940;
  --muted:         #566980;
  --border:        #7B8897;
  --primary:       #1B4B8A;
  --primary-dark:  #0E2F5C;
  --primary-light: #E8EFF8;

  /* Identity: sharp corners, a thicker defined border, and hard offset
     shadows (no blur) instead of soft ambient ones — precise and formal.
     Serif headings (body text stays sans for readability) are the single
     biggest differentiator: a traditional/corporate signal no other
     preset uses. */
  --radius:      .25rem;
  --border-width: 1.5px;
  --shadow-sm: 0 1px 0 rgba(26,41,64,.14);
  --shadow:    0 2px 0 rgba(26,41,64,.10), 0 1px 3px rgba(26,41,64,.16);
  --shadow-lg: 0 4px 0 rgba(26,41,64,.08), 0 2px 10px rgba(26,41,64,.20);
  --portal-font-size:       .875rem;
  --portal-line-height:     1.55;
  --heading-font-family:    Georgia, 'Times New Roman', serif;
  --letter-spacing-heading: 0;
  --motion-easing: ease;

  --bs-body-bg:          #EDF1F5;
  --bs-body-color:       #1A2940;
  --bs-secondary-color:  #566980;
  --bs-border-color:     #7B8897;
  --bs-tertiary-bg:      #F0F4F8;
  --bs-link-color:       #1B4B8A;
  --bs-link-hover-color: #0E2F5C;

  --sidebar-bg:                  #1A2940;
  --sidebar-text:                #88A4C0;
  --sidebar-text-hover:          #E8EFF8;
  --sidebar-text-active:         #B8D4F0;
  --sidebar-icon-active:         #88C0F0;
  --sidebar-hover-bg:            rgba(255,255,255,.06);
  --sidebar-active-bg:           rgba(43,122,228,.18);
  --sidebar-active-border:       #2A7AE4;
  --sidebar-section-label:       #8b9baa;
  --sidebar-section-label-hover: #88A4C0;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.04);
  --sidebar-group-accent:        rgba(136,192,240,.18);
  --sidebar-border:              rgba(255,255,255,.08);
}

/* ── Preset: Modern — contemporary violet ─────────────────────────────── */
html[data-portal-preset="modern"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="modern"] body.auth-page {
  --bg:            #F5F5FB;
  --surface:       #FFFFFF;
  --text:          #1D1932;
  --muted:         #726c8e;
  --border:        #8B82A3;
  --primary:       #7C3AED;
  --primary-dark:  #5B21B6;
  --primary-light: #F3EEFF;

  /* Identity: large pill-like radius + large diffused colour-tinted
     shadows (floaty, contemporary) and Poppins — a bold geometric sans
     already loaded site-wide — instead of Inter, so headings/buttons
     read as a distinct typeface, not just a re-tinted Default. */
  --radius:      1.5rem;
  --border-width: 1px;
  --shadow-sm: 0 2px 8px -2px rgba(124,58,237,.10);
  --shadow:    0 10px 26px -6px rgba(124,58,237,.16), 0 2px 6px rgba(29,25,50,.06);
  --shadow-lg: 0 22px 52px -12px rgba(124,58,237,.24), 0 8px 18px rgba(29,25,50,.08);
  --portal-font-family:     'Poppins', system-ui, -apple-system, sans-serif;
  --heading-font-family:    'Poppins', system-ui, -apple-system, sans-serif;
  --letter-spacing-heading: -.03em;
  --motion-easing: cubic-bezier(.34, 1.56, .64, 1);

  --bs-body-bg:          #F5F5FB;
  --bs-body-color:       #1D1932;
  --bs-secondary-color:  #726c8e;  /* was #7B7499: failed AA on this preset's own surface */
  --bs-border-color:     #8B82A3;
  --bs-tertiary-bg:      #FAF9FF;
  --bs-link-color:       #7C3AED;
  --bs-link-hover-color: #5B21B6;

  --sidebar-bg:                  #1D1932;
  --sidebar-text:                #8880AA;
  --sidebar-text-hover:          #E8E4FF;
  --sidebar-text-active:         #D8B4FE;
  --sidebar-icon-active:         #C084FC;
  --sidebar-hover-bg:            rgba(192,132,252,.07);
  --sidebar-active-bg:           rgba(192,132,252,.16);
  --sidebar-active-border:       #A855F7;
  --sidebar-section-label:       #8986a3;
  --sidebar-section-label-hover: #8880AA;
  --sidebar-toggle-hover-bg:     rgba(192,132,252,.05);
  --sidebar-group-accent:        rgba(216,180,254,.16);
  --sidebar-border:              rgba(255,255,255,.07);
}

/* ── Modern preset — extra interaction/layout language ────────────────────
   2026-07-29: the token block above (identity: colours, radius, shadows,
   Poppins, spring easing) already existed. This section adds the pieces
   that make "Modern" feel like a distinct UI language rather than just a
   re-tinted Default -- glassmorphism, a bento-grid layout utility, hover/
   press micro-interactions, and a loading-skeleton shimmer. Framer Motion/
   Vue Transition/Lottie are React-only and unusable in this vanilla
   Bootstrap+Django-template stack (no build step) -- every effect here is
   plain CSS transitions/keyframes, reading the SAME --motion-easing /
   --shadow-* tokens the rest of the design system already exposes, so it
   automatically stays in sync if this preset's tokens are ever retuned. */

/* Frosted-glass surface -- nav bars, cards, modals. Falls back gracefully
   (semi-opaque solid, no blur) in the rare browser without backdrop-filter
   support, rather than becoming fully transparent/illegible. */
html[data-portal-preset="modern"] .glass {
  background: rgba(255,255,255,.68);
  border: 1px solid rgba(255,255,255,.45);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  backdrop-filter: blur(14px) saturate(160%);
}
html[data-portal-preset="modern"][data-bs-theme="dark"] .glass,
html[data-portal-preset="modern"] body:is(.portal-page,.public-cms-page,.themed-page)[data-bs-theme="dark"] .glass {
  background: rgba(29,25,50,.62);
  border-color: rgba(255,255,255,.10);
}

/* Bento-grid dashboard layout -- CSS Grid auto-fit, no framework. Cards
   inside can opt into larger cells with .bento-wide/.bento-tall (spans 2
   columns/rows) for the "varying sizes" look a bento layout is named for. */
/* minmax(min(Npx, 100%), 1fr), not minmax(Npx, 1fr) — a bare pixel floor
   is a width the track REFUSES to go below, so on a narrow phone the grid
   grows past its own container instead of collapsing to one column, and
   the whole page then scrolls sideways. Measured on the mailbox stat grid
   at 320px (336px of grid in a 320px viewport). Neither grid below was
   caught by the sweep, but both carry the identical construction, and the
   min() form is a no-op whenever there is room for the preferred size. */
.bento-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 1.25rem;
}
.bento-grid .bento-wide { grid-column: span 2; }
.bento-grid .bento-tall { grid-row: span 2; }
@media (max-width: 576px) {
  .bento-grid .bento-wide { grid-column: span 1; }
}

/* Hover-lift + press micro-interactions, spring-eased. Applied broadly
   under Modern (cards, buttons, form controls already carry --radius/
   --shadow from the token block above; this layer only adds the motion). */
html[data-portal-preset="modern"] .card,
html[data-portal-preset="modern"] .stg-card,
html[data-portal-preset="modern"] .bento-grid > * {
  transition: transform var(--motion-med) var(--motion-easing),
              box-shadow var(--motion-med) var(--motion-easing);
}
/* Shadow only, no transform. A transform makes the element a containing
   block for fixed-position descendants, which traps any dropdown opened
   inside it -- and opening one means hovering the card, so the lift was
   always active at exactly the wrong moment. .bento-grid children keep
   theirs: nothing in a bento tile opens a menu. */
html[data-portal-preset="modern"] .card:hover,
html[data-portal-preset="modern"] .stg-card:hover {
  box-shadow: var(--shadow-lg);
}
html[data-portal-preset="modern"] .bento-grid > *:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-lg);
}
html[data-portal-preset="modern"] .btn {
  transition: transform .12s var(--motion-easing), box-shadow var(--motion-med) var(--motion-easing);
}
html[data-portal-preset="modern"] .btn:active {
  transform: scale(.96);
}
html[data-portal-preset="modern"] .form-check-input,
html[data-portal-preset="modern"] .form-switch .form-check-input {
  transition: background-color var(--motion-med) var(--motion-easing),
              border-color var(--motion-med) var(--motion-easing),
              box-shadow var(--motion-med) var(--motion-easing),
              transform .12s var(--motion-easing);
}
html[data-portal-preset="modern"] .form-check-input:active {
  transform: scale(.9);
}

/* Loading skeleton -- shimmer sweep. <div class="skeleton" style="height:1rem"></div> */
.skeleton {
  position: relative;
  overflow: hidden;
  background: var(--border);
  border-radius: var(--radius);
  opacity: .5;
}
.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.45), transparent);
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
@keyframes skeleton-shimmer {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}

/* Fade-in-with-stagger on load -- see the small IntersectionObserver
   companion script in templates/base.html (search "fade-in-stagger").
   Elements start hidden/offset only once JS confirms it can reveal them,
   so a no-JS visit (or a slow connection mid-load) never leaves content
   permanently hidden -- the observer adds .is-visible, this rule reads it. */
.fade-in-stagger { opacity: 0; transform: translateY(10px); }
.fade-in-stagger.is-visible {
  opacity: 1;
  transform: none;
  transition: opacity var(--motion-slow) var(--motion-easing),
              transform var(--motion-slow) var(--motion-easing);
}

/* ── Preset: Ocean — soft, dimmer ocean-blue ───────────────────────────────
   Same structure/identity as Modern (Poppins, 1.5rem pill radius, diffused
   colour-tinted shadows, glassmorphism/bento-grid/micro-interactions below)
   just recoloured -- and deliberately DIMMER than Modern's stark white
   surface/near-black text: an off-white/pale-teal surface and a dark
   slate-teal (not near-black) text colour, for light-sensitive users who
   find Modern's higher-contrast white too harsh. Not to be confused with
   the existing "Blue" preset (a plain, punchier/high-contrast flat-blue
   theme with no glass/bento/motion layer, same family as Classic/Light) --
   Ocean is the Modern *experience* in a softer, teal-blue palette. */
html[data-portal-preset="ocean"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="ocean"] body.auth-page {
  --bg:            #E9F1F3;
  --surface:       #F5FAFB;
  --text:          #1E353B;
  --muted:         #54717a;
  --border:        #A9C6CE;
  --border-strong: #7f949a;
  --primary:       #106B7C;
  --primary-dark:  #0B4F5C;
  --primary-light: #E0F0F3;

  --radius:      1.5rem;
  --border-width: 1px;
  --shadow-sm: 0 2px 8px -2px rgba(16,107,124,.10);
  --shadow:    0 10px 26px -6px rgba(16,107,124,.16), 0 2px 6px rgba(30,53,59,.06);
  --shadow-lg: 0 22px 52px -12px rgba(16,107,124,.24), 0 8px 18px rgba(30,53,59,.08);
  --portal-font-family:     'Poppins', system-ui, -apple-system, sans-serif;
  --heading-font-family:    'Poppins', system-ui, -apple-system, sans-serif;
  --letter-spacing-heading: -.03em;
  --motion-easing: cubic-bezier(.34, 1.56, .64, 1);

  --bs-body-bg:          #E9F1F3;
  --bs-body-color:       #1E353B;
  --bs-secondary-color:  #54717a;  /* was #61828C: failed AA on this preset's own surface */
  --bs-border-color:     #A9C6CE;
  --bs-tertiary-bg:      #F0F7F8;
  --bs-link-color:       #106B7C;
  --bs-link-hover-color: #0B4F5C;

  --sidebar-bg:                  #0F2A31;
  --sidebar-text:                #7FA3AC;
  --sidebar-text-hover:          #DCF0F3;
  --sidebar-text-active:         #9FE0E8;
  --sidebar-icon-active:         #6FCAD6;
  --sidebar-hover-bg:            rgba(111,202,214,.07);
  --sidebar-active-bg:           rgba(111,202,214,.14);
  --sidebar-active-border:       #3E97A6;
  --sidebar-section-label:       #81999e;
  --sidebar-section-label-hover: #7FA3AC;
  --sidebar-toggle-hover-bg:     rgba(111,202,214,.05);
  --sidebar-group-accent:        rgba(159,224,232,.14);
  --sidebar-border:              rgba(255,255,255,.06);
}

/* Same glass/hover/press micro-interaction language as Modern (see that
   preset's own comment) -- just the selector renamed, since none of these
   rules hardcode a colour of their own, they all read the token block
   above via var(--shadow-lg)/var(--motion-easing)/etc. */
html[data-portal-preset="ocean"] .glass {
  background: rgba(245,250,251,.68);
  border: 1px solid rgba(255,255,255,.45);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  backdrop-filter: blur(14px) saturate(150%);
}
html[data-portal-preset="ocean"][data-bs-theme="dark"] .glass,
html[data-portal-preset="ocean"] body:is(.portal-page,.public-cms-page,.themed-page)[data-bs-theme="dark"] .glass {
  background: rgba(15,42,49,.62);
  border-color: rgba(255,255,255,.08);
}
html[data-portal-preset="ocean"] .card,
html[data-portal-preset="ocean"] .stg-card,
html[data-portal-preset="ocean"] .bento-grid > * {
  transition: transform var(--motion-med) var(--motion-easing),
              box-shadow var(--motion-med) var(--motion-easing);
}
/* Shadow only, no transform. A transform makes the element a containing
   block for fixed-position descendants, which traps any dropdown opened
   inside it -- and opening one means hovering the card, so the lift was
   always active at exactly the wrong moment. .bento-grid children keep
   theirs: nothing in a bento tile opens a menu. */
html[data-portal-preset="ocean"] .card:hover,
html[data-portal-preset="ocean"] .stg-card:hover {
  box-shadow: var(--shadow-lg);
}
html[data-portal-preset="ocean"] .bento-grid > *:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-lg);
}
html[data-portal-preset="ocean"] .btn {
  transition: transform .12s var(--motion-easing), box-shadow var(--motion-med) var(--motion-easing);
}
html[data-portal-preset="ocean"] .btn:active {
  transform: scale(.96);
}
html[data-portal-preset="ocean"] .form-check-input,
html[data-portal-preset="ocean"] .form-switch .form-check-input {
  transition: background-color var(--motion-med) var(--motion-easing),
              border-color var(--motion-med) var(--motion-easing),
              box-shadow var(--motion-med) var(--motion-easing),
              transform .12s var(--motion-easing);
}
html[data-portal-preset="ocean"] .form-check-input:active {
  transform: scale(.9);
}

/* ── Preset: Windows 7 — Aero glass inspired ──────────────────────────── */
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="win7"] body.auth-page {
  --bg:            #E8F0F8;
  --surface:       #FFFFFF;
  --text:          #1A2030;
  --muted:         #4A6280;
  --border:        #7389A1;
  --primary:       #2B5FAA;
  --primary-dark:  #1A3C78;
  --primary-light: #DCE9F8;
  --radius:        .5rem;

  /* Identity: an inset top highlight stacked under the drop shadow — the
     classic Aero "glossy chrome" trick — plus a visible 1.5px border and
     the actual Windows system-font stack (no web font, loads instantly
     and reads as authentically "OS chrome" rather than a themed website). */
  --border-width: 1.5px;
  --shadow-sm: inset 0 1px 0 rgba(255,255,255,.55), 0 1px 2px rgba(26,32,48,.16);
  --shadow:    inset 0 1px 0 rgba(255,255,255,.65), 0 2px 7px rgba(26,32,48,.20);
  --shadow-lg: inset 0 1px 0 rgba(255,255,255,.75), 0 7px 20px rgba(26,32,48,.26);
  --portal-font-family:     "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
  --heading-font-family:    "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
  --portal-font-size:       .8125rem;
  --portal-line-height:     1.5;
  --letter-spacing-heading: 0;
  --motion-easing: cubic-bezier(.25, .1, .25, 1);

  --bs-body-bg:          #E8F0F8;
  --bs-body-color:       #1A2030;
  --bs-secondary-color:  #4A6280;
  --bs-border-color:     #7389A1;
  --bs-tertiary-bg:      #EEF4FC;
  --bs-link-color:       #2B5FAA;
  --bs-link-hover-color: #1A3C78;

  --sidebar-bg:                  #1C3A6E;
  --sidebar-text:                #8AAED4;
  --sidebar-text-hover:          #DEECF8;
  --sidebar-text-active:         #BAD9F5;
  --sidebar-icon-active:         #6DC0F0;
  --sidebar-hover-bg:            rgba(255,255,255,.07);
  --sidebar-active-bg:           rgba(66,152,232,.20);
  --sidebar-active-border:       #4298E8;
  --sidebar-section-label:       #a6b2c3;
  --sidebar-section-label-hover: #8AAED4;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.05);
  --sidebar-group-accent:        rgba(109,192,240,.20);
  --sidebar-border:              rgba(255,255,255,.10);
}
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .hrms-sidebar {
  background: linear-gradient(180deg, #1F3E72 0%, #162E5A 55%, #1A3466 100%);
  box-shadow: inset -1px 0 0 rgba(255,255,255,.08), 2px 0 12px rgba(0,0,0,.20);
}
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary {
  background: linear-gradient(180deg, #4E9AE8 0%, #2B5FAA 100%) !important;
  border-color: #1A3C78 !important;
  box-shadow: 0 1px 2px rgba(0,0,0,.2), inset 0 1px 0 rgba(255,255,255,.25) !important;
}
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary:hover {
  background: linear-gradient(180deg, #5CA8F5 0%, #3468BE 100%) !important;
}

/* ── Win7 preset, part 2 — glossy chrome beyond the primary button, so the
   "Aero" identity reads on every control, not just one. Every rule below is
   .portal-page-scoped — it must never touch the public marketing site,
   which shares .btn/.card/.accordion-button/.badge class names. ──────────── */
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-secondary,
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-outline-secondary,
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-light {
  background: linear-gradient(180deg, #FDFDFE 0%, #E7EBF0 48%, #D7DEE7 50%, #EBEEF2 100%) !important;
  border: 1px solid #98A7B8 !important;
  color: #1A2030 !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.9), 0 1px 1px rgba(26,32,48,.12) !important;
}
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-secondary:hover,
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-outline-secondary:hover,
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-light:hover {
  background: linear-gradient(180deg, #FFFFFF 0%, #EEF4FC 48%, #DCEAFA 50%, #F1F7FE 100%) !important;
  border-color: #6FA3D8 !important;
}
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-outline-primary {
  background: linear-gradient(180deg, #EEF6FF 0%, #D9E9FB 100%) !important;
  border-color: #6FA3D8 !important;
  color: #1A3C78 !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.8) !important;
}

/* Aero-glass titlebar for the topbar/header — the single most recognizable
   Windows 7 signature (frosted blue-glass gradient with a bright top edge). */
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .portal-topbar {
  background: linear-gradient(180deg, #DCEBFB 0%, #B9D8F5 6%, #9FC8F0 50%, #B4D5F4 94%, #CBE2F8 100%) !important;
  border-bottom: 1px solid #6FA3D8 !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.85), 0 1px 4px rgba(26,32,48,.18) !important;
}
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .portal-topbar-nav-link,
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .portal-topbar-icon-btn {
  color: #1A3C78 !important;
}

/* Raised "glass panel" cards/widgets — a bevelled edge plus a soft top
   highlight instead of a flat modern drop shadow. */
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .card,
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .feature-card,
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .stg-card,
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .kpi-card {
  border: 1px solid #818C9A !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.9), 0 1px 3px rgba(26,32,48,.10) !important;
}

/* Flat bevelled accordion panels, Explorer-pane style. */
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .accordion-button {
  background: linear-gradient(180deg, #F7FAFD 0%, #E3EBF3 100%) !important;
  border: 1px solid #818C9A !important;
}
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .accordion-button:not(.collapsed) {
  background: linear-gradient(180deg, #DCEBFB 0%, #C3DEF6 100%) !important;
  color: #1A3C78 !important;
}

/* Sunken (inset) form controls — the classic Win7 "etched" text field. */
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .form-control,
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .form-select {
  border: 1px solid #93AFCE !important;
  box-shadow: inset 0 1px 3px rgba(26,32,48,.18) !important;
}

/* ── Preset: Simple (Default) — deliberately low-colour ───────────────────
   Identity: mostly grayscale surfaces/text/borders with a single muted
   slate accent instead of a saturated brand hue — the "not colorful" look.
   Sidebar tokens are deliberately left unset: this is the one preset that
   inherits the :root sidebar palette rather than stating its own. That was
   originally so the separate sidebar-accent control could drive it; the
   accent is gone, and inheriting is now simply what makes this the
   baseline the other presets are read against. */
html[data-portal-preset="default"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="default"] body.auth-page {
  --bg:            #F4F5F7;
  --surface:       #FFFFFF;
  --text:          #24282F;
  --muted:         #667085;
  --border:        #85878A;
  --primary:       #475569;
  --primary-dark:  #334155;
  --primary-light: #EEF0F3;

  --radius:       .5rem;
  --border-width: 1px;
  --shadow-sm: 0 1px 2px rgba(15,23,42,.05);
  --shadow:    0 2px 8px -2px rgba(15,23,42,.08);
  --shadow-lg: 0 12px 28px -8px rgba(15,23,42,.12);
  --letter-spacing-heading: -.01em;
  --motion-easing: cubic-bezier(.4, 0, .2, 1);

  --bs-body-bg:          #F4F5F7;
  --bs-body-color:       #24282F;
  --bs-secondary-color:  #667085;
  --bs-border-color:     #85878A;
  --bs-tertiary-bg:      #F8F9FA;
  --bs-link-color:       #475569;
  --bs-link-hover-color: #334155;
}

/* ── Preset: Red — bold crimson ───────────────────────────────────────── */
html[data-portal-preset="red"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="red"] body.auth-page {
  --bg:            #FDF3F3;
  --surface:       #FFFFFF;
  --text:          #3A1414;
  --muted:         #8A5A5A;
  --border:        #9A8181;
  --primary:       #DC2626;
  --primary-dark:  #991B1B;
  --primary-light: #FEE2E2;

  --radius:       .75rem;
  --border-width: 1px;
  --shadow-sm: 0 1px 3px rgba(153,27,27,.07);
  --shadow:    0 4px 10px -2px rgba(153,27,27,.12);
  --shadow-lg: 0 14px 30px -6px rgba(153,27,27,.18);
  --letter-spacing-heading: -.02em;
  --motion-easing: cubic-bezier(.4, 0, .2, 1);

  --bs-body-bg:          #FDF3F3;
  --bs-body-color:       #3A1414;
  --bs-secondary-color:  #8A5A5A;
  --bs-border-color:     #9A8181;
  --bs-tertiary-bg:      #FEF6F6;
  --bs-link-color:       #DC2626;
  --bs-link-hover-color: #991B1B;

  --sidebar-bg:                  #450A0A;
  --sidebar-text:                #E8A9A9;
  --sidebar-text-hover:          #FEE2E2;
  --sidebar-text-active:         #FECACA;
  --sidebar-icon-active:         #FCA5A5;
  --sidebar-hover-bg:            rgba(255,255,255,.06);
  --sidebar-active-bg:           rgba(248,113,113,.20);
  --sidebar-active-border:       #F87171;
  --sidebar-section-label:       #b38282;
  --sidebar-section-label-hover: #E8A9A9;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.04);
  --sidebar-group-accent:        rgba(252,165,165,.20);
  --sidebar-border:              rgba(255,255,255,.08);
}

/* ── Preset: Blue — ocean blue ─────────────────────────────────────────── */
html[data-portal-preset="blue"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="blue"] body.auth-page {
  --bg:            #EFF6FF;
  --surface:       #FFFFFF;
  --text:          #0F1E33;
  --muted:         #5D7290;
  --border:        #77899E;
  --primary:       #2563EB;
  --primary-dark:  #1D4ED8;
  --primary-light: #DBEAFE;

  --radius:       .75rem;
  --border-width: 1px;
  --shadow-sm: 0 1px 3px rgba(29,78,216,.07);
  --shadow:    0 4px 10px -2px rgba(29,78,216,.12);
  --shadow-lg: 0 14px 30px -6px rgba(29,78,216,.18);
  --letter-spacing-heading: -.02em;
  --motion-easing: cubic-bezier(.4, 0, .2, 1);

  --bs-body-bg:          #EFF6FF;
  --bs-body-color:       #0F1E33;
  --bs-secondary-color:  #5D7290;
  --bs-border-color:     #77899E;
  --bs-tertiary-bg:      #F5F9FF;
  --bs-link-color:       #2563EB;
  --bs-link-hover-color: #1D4ED8;

  --sidebar-bg:                  #0B2545;
  --sidebar-text:                #93C5FD;
  --sidebar-text-hover:          #DBEAFE;
  --sidebar-text-active:         #BFDBFE;
  --sidebar-icon-active:         #93C5FD;
  --sidebar-hover-bg:            rgba(255,255,255,.06);
  --sidebar-active-bg:           rgba(96,165,250,.20);
  --sidebar-active-border:       #60A5FA;
  --sidebar-section-label:       #8396b1;
  --sidebar-section-label-hover: #93C5FD;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.04);
  --sidebar-group-accent:        rgba(147,197,253,.20);
  --sidebar-border:              rgba(255,255,255,.08);
}

/* ── Preset: Black & White — zero hue, pure contrast ──────────────────────
   The only preset with no accent colour at all: --primary is literal black.
   Distinct from Simple (which keeps a faint slate-blue tint) by design. */
html[data-portal-preset="bw"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="bw"] body.auth-page {
  --bg:            #FFFFFF;
  --surface:       #FFFFFF;
  --text:          #000000;
  --muted:         #595959;
  --border:        #878787;
  --primary:       #000000;
  --primary-dark:  #000000;
  --primary-light: #F0F0F0;

  --radius:       .375rem;
  --border-width: 1.5px;
  --shadow-sm: 0 1px 2px rgba(0,0,0,.10);
  --shadow:    0 3px 8px rgba(0,0,0,.14);
  --shadow-lg: 0 10px 24px rgba(0,0,0,.18);
  --letter-spacing-heading: 0;
  --motion-easing: ease;

  --bs-body-bg:          #FFFFFF;
  --bs-body-color:       #000000;
  --bs-secondary-color:  #595959;
  --bs-border-color:     #878787;
  --bs-tertiary-bg:      #F5F5F5;
  --bs-link-color:       #000000;
  --bs-link-hover-color: #404040;

  --sidebar-bg:                  #000000;
  --sidebar-text:                #B3B3B3;
  --sidebar-text-hover:          #FFFFFF;
  --sidebar-text-active:         #FFFFFF;
  --sidebar-icon-active:         #FFFFFF;
  --sidebar-hover-bg:            rgba(255,255,255,.08);
  --sidebar-active-bg:           rgba(255,255,255,.16);
  --sidebar-active-border:       #FFFFFF;
  --sidebar-section-label:       #7b7b7b;
  --sidebar-section-label-hover: #CCCCCC;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.05);
  --sidebar-group-accent:        rgba(255,255,255,.16);
  --sidebar-border:              rgba(255,255,255,.14);
}
/* --primary/--primary-dark are both pure black here, so .btn-primary's own
   hover rule (background: var(--primary-dark)) would show zero visual
   change on hover — bump lightness explicitly instead. */
html[data-portal-preset="bw"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary:hover {
  background: #262626 !important;
  border-color: #262626 !important;
}

/* ── Preset: Retro — vintage terracotta & teal ─────────────────────────────
   Identity: warm cream paper, bold 2px outlines, and hard offset shadows
   (no blur) instead of soft ambient ones — the "printed badge" look, plus
   Poppins (already loaded site-wide) for a rounder, friendlier heading
   voice than Inter gives every other preset. */
html[data-portal-preset="retro"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="retro"] body.auth-page {
  --bg:            #F5EFE0;
  --surface:       #FFFBF2;
  --text:          #2B2118;
  --muted:         #6B5A45;
  --border:        #8E836F;
  --border-strong: #8b929b;
  --primary:       #C1440E;
  --primary-dark:  #8F3009;
  --primary-light: #FBE4D4;

  --radius:       .375rem;
  --border-width: 2px;
  --shadow-sm: 2px 2px 0 rgba(43,33,24,.16);
  --shadow:    4px 4px 0 rgba(43,33,24,.18);
  --shadow-lg: 6px 6px 0 rgba(43,33,24,.20);
  --portal-font-family:     'Poppins', system-ui, -apple-system, sans-serif;
  --heading-font-family:    'Poppins', system-ui, -apple-system, sans-serif;
  --letter-spacing-heading: 0;
  --motion-easing: cubic-bezier(.65, 0, .35, 1);

  --bs-body-bg:          #F5EFE0;
  --bs-body-color:       #2B2118;
  --bs-secondary-color:  #6B5A45;
  --bs-border-color:     #8E836F;
  --bs-tertiary-bg:      #FBF3E4;
  --bs-link-color:       #C1440E;
  --bs-link-hover-color: #8F3009;

  --sidebar-bg:                  #3D2B1F;
  --sidebar-text:                #D8C3A8;
  --sidebar-text-hover:          #F5EFE0;
  --sidebar-text-active:         #F5D9B8;
  --sidebar-icon-active:         #F0A868;
  --sidebar-hover-bg:            rgba(255,255,255,.06);
  --sidebar-active-bg:           rgba(240,168,104,.20);
  --sidebar-active-border:       #F0A868;
  --sidebar-section-label:       #b2a08f;
  --sidebar-section-label-hover: #D8C3A8;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.05);
  --sidebar-group-accent:        rgba(240,168,104,.20);
  --sidebar-border:              #978070;
}
/* Chunky, offset-shadow buttons — pressed-badge feel on click. */
html[data-portal-preset="retro"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary {
  box-shadow: 3px 3px 0 rgba(43,33,24,.35) !important;
  transition: transform .1s ease, box-shadow .1s ease !important;
}
html[data-portal-preset="retro"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary:hover {
  transform: translate(-1px, -1px) !important;
  box-shadow: 4px 4px 0 rgba(43,33,24,.35) !important;
}
html[data-portal-preset="retro"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary:active {
  transform: translate(2px, 2px) !important;
  box-shadow: 1px 1px 0 rgba(43,33,24,.35) !important;
}
html[data-portal-preset="retro"] body:is(.portal-page,.public-cms-page,.themed-page) .card,
html[data-portal-preset="retro"] body:is(.portal-page,.public-cms-page,.themed-page) .feature-card,
html[data-portal-preset="retro"] body:is(.portal-page,.public-cms-page,.themed-page) .stg-card {
  box-shadow: var(--shadow-sm) !important;
}

/* ── Preset: Windows 11 — Fluent / Mica ─────────────────────────────────────
   Identity: heavy corner rounding, soft layered "Mica" shadows instead of
   hard borders, Win11's own accent blue, and the real system font stack —
   on an actual Windows machine this renders in genuine Segoe UI. */
html[data-portal-preset="win11"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="win11"] body.auth-page {
  --bg:            #F3F3F3;
  --surface:       #FFFFFF;
  --text:          #1B1B1B;
  --muted:         #5F6368;
  --border:        #878787;
  --primary:       #0067C0;
  --primary-dark:  #004A87;
  --primary-light: #E5F1FB;

  --radius:       .75rem;
  --border-width: 1px;
  --shadow-sm: 0 1px 2px rgba(0,0,0,.06), 0 1px 3px rgba(0,0,0,.07);
  --shadow:    0 2px 6px rgba(0,0,0,.07), 0 6px 16px -4px rgba(0,0,0,.10);
  --shadow-lg: 0 8px 24px -6px rgba(0,0,0,.14), 0 2px 8px rgba(0,0,0,.08);
  --portal-font-family:     system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --heading-font-family:    system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --letter-spacing-heading: -.01em;
  --motion-easing: cubic-bezier(.16, 1, .3, 1);

  --bs-body-bg:          #F3F3F3;
  --bs-body-color:       #1B1B1B;
  --bs-secondary-color:  #5F6368;
  --bs-border-color:     #878787;
  --bs-tertiary-bg:      #F9F9F9;
  --bs-link-color:       #0067C0;
  --bs-link-hover-color: #004A87;

  --sidebar-bg:                  #F9F9F9;
  --sidebar-text:                #3B3B3B;
  --sidebar-text-hover:          #0F0F0F;
  --sidebar-text-active:         #004A87;
  --sidebar-icon-active:         #0067C0;
  --sidebar-hover-bg:            rgba(0,0,0,.04);
  --sidebar-active-bg:           rgba(0,103,192,.10);
  --sidebar-active-border:       #0067C0;
  --sidebar-section-label:       #6B6B6B;
  --sidebar-section-label-hover: #3B3B3B;
  --sidebar-toggle-hover-bg:     rgba(0,0,0,.035);
  --sidebar-group-accent:        rgba(0,103,192,.16);
  --sidebar-border:              #D9D9D9;
}
/* Mica-style rounded, softly-elevated surfaces instead of hard edges.
   Real Fluent cards lean on shadow over a hard border, but a 6%-opacity
   line fails contrast outright rather than just reading as "subtle" — use
   the same --border token every other preset's card relies on instead of
   a bespoke near-invisible value. */
html[data-portal-preset="win11"] body:is(.portal-page,.public-cms-page,.themed-page) .card,
html[data-portal-preset="win11"] body:is(.portal-page,.public-cms-page,.themed-page) .feature-card,
html[data-portal-preset="win11"] body:is(.portal-page,.public-cms-page,.themed-page) .stg-card,
html[data-portal-preset="win11"] body:is(.portal-page,.public-cms-page,.themed-page) .kpi-card {
  border-radius: 1rem !important;
  border-color: var(--border) !important;
}
html[data-portal-preset="win11"] body:is(.portal-page,.public-cms-page,.themed-page) .btn {
  border-radius: .5rem !important;
}
html[data-portal-preset="win11"] body:is(.portal-page,.public-cms-page,.themed-page) .hrms-sidebar,
html[data-portal-preset="win11"] body:is(.portal-page,.public-cms-page,.themed-page) .stg-sidebar {
  border-radius: 0 !important;
  box-shadow: 1px 0 0 rgba(0,0,0,.06) !important;
}

/* ── Preset: Windows XP (Luna Blue) ────────────────────────────────────────
   Identity: the "Luna" theme — a saturated blue gradient titlebar/taskbar,
   the classic sandy-silver (#ECE9D8) chrome behind white content panes,
   noticeably ROUNDED buttons/cards (Luna's corners were softer than win7's
   subtler bevel), a bright blue focus/hover GLOW halo (the single most
   recognizable Luna interaction cue), and Tahoma as the system font. A
   pale-yellow, black-bordered tooltip is included too — instantly reads as
   "old Windows" on its own. */
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="winxp"] body.auth-page {
  --bg:            #ECE9D8;
  --surface:       #FFFFFF;
  --text:          #000000;
  --muted:         #4D4D4D;
  --border:        #7F9DB9;
  --border-strong: #7b98b3;
  --primary:       #2A66E8;
  --primary-dark:  #0A3FA6;
  --primary-light: #D9E7FB;
  --radius:        .75rem;

  --border-width: 1px;
  --shadow-sm: inset 0 1px 0 rgba(255,255,255,.6), 0 1px 2px rgba(0,0,0,.18);
  --shadow:    inset 0 1px 0 rgba(255,255,255,.7), 0 2px 6px rgba(0,0,0,.22);
  --shadow-lg: inset 0 1px 0 rgba(255,255,255,.8), 0 8px 22px rgba(0,0,0,.28);
  --portal-font-family:     Tahoma, "Segoe UI", Verdana, sans-serif;
  --heading-font-family:    Tahoma, "Segoe UI", Verdana, sans-serif;
  --portal-font-size:       .8125rem;
  --letter-spacing-heading: 0;
  --motion-easing: cubic-bezier(.2, .85, .4, 1);

  --bs-body-bg:          #ECE9D8;
  --bs-body-color:       #000000;
  --bs-secondary-color:  #4D4D4D;
  --bs-border-color:     #7F9DB9;
  --bs-tertiary-bg:      #F4F2E8;
  --bs-link-color:       #2A66E8;
  --bs-link-hover-color: #0A3FA6;

  --sidebar-bg:                  #0A3FA6;
  --sidebar-text:                #AFCBF5;
  --sidebar-text-hover:          #FFFFFF;
  --sidebar-text-active:         #FFFFFF;
  --sidebar-icon-active:         #6FC0FF;
  --sidebar-hover-bg:            rgba(255,255,255,.10);
  --sidebar-active-bg:           rgba(111,192,255,.24);
  --sidebar-active-border:       #6FC0FF;
  --sidebar-section-label:       #b2c9ec;
  --sidebar-section-label-hover: #AFCBF5;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.08);
  --sidebar-group-accent:        rgba(111,192,255,.24);
  --sidebar-border:              rgba(255,255,255,.14);
}
/* Taskbar-style sidebar: a saturated blue gradient (brighter than win7's
   Aero glass), flat rather than glassy. */
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .hrms-sidebar {
  background: linear-gradient(180deg, #1657D6 0%, #0A3FA6 55%, #0C48BE 100%);
  box-shadow: inset -1px 0 0 rgba(255,255,255,.12), 2px 0 10px rgba(0,0,0,.24);
}
/* The Luna button: rounded, glossy blue gradient with a bright top-half
   highlight — and a soft blue GLOW on hover/focus, the defining Luna cue. */
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn {
  border-radius: .5rem !important;
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary {
  background: linear-gradient(180deg, #5FA3FF 0%, #2A66E8 46%, #1650D6 100%) !important;
  border: 1px solid #0A3FA6 !important;
  border-radius: .85rem !important;
  box-shadow: 0 1px 2px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.5) !important;
  transition: box-shadow var(--motion-med, .2s) var(--motion-easing),
              transform var(--motion-fast, .15s) var(--motion-easing) !important;
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary:hover {
  background: linear-gradient(180deg, #7AB6FF 0%, #3E78F0 46%, #1D5AE0 100%) !important;
  box-shadow: 0 1px 3px rgba(0,0,0,.24), inset 0 1px 0 rgba(255,255,255,.6),
              0 0 0 3px rgba(42,102,232,.30) !important;
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary:focus,
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary:focus-visible {
  box-shadow: 0 1px 2px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.5),
              0 0 0 4px rgba(111,192,255,.55) !important;
  animation: winxp-glow-pulse 1.6s ease-in-out infinite;
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary:active {
  background: linear-gradient(180deg, #1650D6 0%, #2A66E8 100%) !important;
  box-shadow: inset 0 2px 4px rgba(0,0,0,.25) !important;
}
@media (prefers-reduced-motion: reduce) {
  html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-primary:focus { animation: none; }
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page)[data-portal-motion="reduced"] .btn-primary:focus {
  animation: none;
}
@keyframes winxp-glow-pulse {
  0%, 100% { box-shadow: 0 1px 2px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.5), 0 0 0 4px rgba(111,192,255,.45); }
  50%      { box-shadow: 0 1px 2px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.5), 0 0 0 6px rgba(111,192,255,.65); }
}
/* Silver "Classic" button — the workhorse secondary/cancel button. */
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-secondary,
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-outline-secondary,
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-light {
  background: linear-gradient(180deg, #FFFFFF 0%, #ECEBE4 46%, #DCDAD1 50%, #F0EFE9 100%) !important;
  border: 1px solid #ACA899 !important;
  border-radius: .5rem !important;
  color: #000000 !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.9), 0 1px 1px rgba(0,0,0,.12) !important;
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-secondary:hover,
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-outline-secondary:hover,
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-light:hover {
  border-color: #7F9DB9 !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 0 0 3px rgba(42,102,232,.18) !important;
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .btn-outline-primary {
  background: linear-gradient(180deg, #EAF2FF 0%, #D2E4FC 100%) !important;
  border: 1px solid #7F9DB9 !important;
  border-radius: .5rem !important;
  color: #0A3FA6 !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.8) !important;
}

/* The Luna titlebar — the single most iconic identity element: a bright
   blue gloss gradient with a crisp white top edge and bold white text. */
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .portal-topbar {
  background: linear-gradient(180deg, #4F9AFF 0%, #2A66E8 18%, #0A3FA6 82%, #0850C6 100%) !important;
  border-bottom: 2px solid #0A3FA6 !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.55), 0 2px 5px rgba(0,0,0,.22) !important;
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .portal-topbar-nav-link,
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .portal-topbar-icon-btn {
  color: #FFFFFF !important;
  text-shadow: 0 1px 1px rgba(0,0,0,.35);
}

/* Rounded "dialog box" cards on the sandy-silver body background. */
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .card,
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .feature-card,
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .stg-card,
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .kpi-card {
  border: 1px solid #ACA899 !important;
  border-radius: .85rem !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.9), 0 1px 4px rgba(0,0,0,.14) !important;
}

/* "Groupbox" style panel headers — silver gradient bar, blue when active,
   matching the classic XP grouped-controls / Explorer task-pane look. */
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .accordion-button {
  background: linear-gradient(180deg, #FBFAF6 0%, #E6E4DA 100%) !important;
  border: 1px solid #ACA899 !important;
  border-radius: .5rem !important;
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .accordion-button:not(.collapsed) {
  background: linear-gradient(180deg, #6FA9FF 0%, #2A66E8 100%) !important;
  color: #FFFFFF !important;
  text-shadow: 0 1px 1px rgba(0,0,0,.3);
}

/* Sunken white text fields with the classic blue-gray XP border, and the
   same signature blue glow on focus as the primary button. */
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .form-control,
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .form-select {
  background: #FFFFFF !important;
  border: 1px solid #7F9DB9 !important;
  border-radius: .35rem !important;
  box-shadow: inset 0 1px 2px rgba(0,0,0,.15) !important;
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .form-control:focus,
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .form-select:focus {
  border-color: #2A66E8 !important;
  box-shadow: inset 0 1px 2px rgba(0,0,0,.15), 0 0 0 3px rgba(111,192,255,.45) !important;
}

/* The pale-yellow, black-bordered XP tooltip — recognizable on sight. */
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .tooltip .tooltip-inner {
  background: #FFFFE1 !important;
  color: #000000 !important;
  border: 1px solid #000000 !important;
  border-radius: 2px !important;
  box-shadow: 2px 2px 4px rgba(0,0,0,.25) !important;
  font-family: var(--portal-font-family);
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .tooltip .tooltip-arrow::before {
  display: none;
}

html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .hrms-footer {
  background: linear-gradient(180deg, #F4F2E8 0%, #E6E4DA 100%) !important;
  border-top: 1px solid #ACA899 !important;
  /* This preset sets the background literally rather than through
     --sidebar-bg, so the text tokens have to be restated or they stay light
     on a light gradient. */
  --footer-text: #4a4a42;          /* 7.4:1 on #E6E4DA */
  --footer-text-strong: #2b2b26;   /* 12.3:1 */
  color: var(--footer-text) !important;
}
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .hrms-footer a { color: var(--footer-text); }
html[data-portal-preset="winxp"] body:is(.portal-page,.public-cms-page,.themed-page) .hrms-footer a:hover { color: var(--footer-text-strong); }

/* ── Preset: Nord — Arctic ────────────────────────────────────────────────
   Identity: the real Nord palette (Arctic Ice Studio / nordtheme.com) —
   Polar Night sidebar, Snow Storm content, Frost blue/cyan accents. */
html[data-portal-preset="nord"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="nord"] body.auth-page {
  --bg:            #ECEFF4;
  --surface:       #FFFFFF;
  --text:          #2E3440;
  --muted:         #4C566A;
  --border:        #828790;
  --primary:       #57779F;
  --primary-dark:  #4A6587;
  --primary-light: #E1E8F0;

  --radius:       .5rem;
  --border-width: 1px;
  --shadow-sm: 0 1px 3px rgba(46,52,64,.08);
  --shadow:    0 4px 12px -2px rgba(46,52,64,.12);
  --shadow-lg: 0 14px 32px -8px rgba(46,52,64,.18);
  --letter-spacing-heading: -.02em;
  --motion-easing: cubic-bezier(.4, 0, .2, 1);

  --bs-body-bg:          #ECEFF4;
  --bs-body-color:       #2E3440;
  --bs-secondary-color:  #4C566A;
  --bs-border-color:     #828790;
  --bs-tertiary-bg:      #E5E9F0;
  --bs-link-color:       #57779F;
  --bs-link-hover-color: #4A6587;

  --sidebar-bg:                  #2E3440;
  --sidebar-text:                #D8DEE9;
  --sidebar-text-hover:          #ECEFF4;
  --sidebar-text-active:         #E5E9F0;
  --sidebar-icon-active:         #88C0D0;
  --sidebar-hover-bg:            rgba(255,255,255,.05);
  --sidebar-active-bg:           rgba(136,192,208,.16);
  --sidebar-active-border:       #88C0D0;
  --sidebar-section-label:       #9fa6b6;
  --sidebar-section-label-hover: #D8DEE9;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.04);
  --sidebar-group-accent:        rgba(136,192,208,.18);
  --sidebar-border:              #838A98;
}

/* ── Preset: Solarized — precision cream & teal ──────────────────────────
   Identity: Ethan Schoonover's Solarized palette — the light (base3/base2)
   surfaces with the dark (base03) sidebar from the same family, exactly
   like the real Solarized Light/Dark pairing was designed to combine. */
html[data-portal-preset="solarized"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="solarized"] body.auth-page {
  --bg:            #EEE8D5;
  --surface:       #FDF6E3;
  --text:          #073642;
  --muted:         #586b72;
  --border:        #87816E;
  --border-strong: #888f98;
  --primary:       #2076b3;
  --primary-dark:  #1C689D;
  --primary-light: #E3F1FB;

  --radius:       .375rem;
  --border-width: 1px;
  --shadow-sm: 0 1px 2px rgba(7,54,66,.08);
  --shadow:    0 3px 10px -2px rgba(7,54,66,.12);
  --shadow-lg: 0 12px 28px -6px rgba(7,54,66,.16);
  --letter-spacing-heading: -.01em;
  --motion-easing: ease;

  --bs-body-bg:          #EEE8D5;
  --bs-body-color:       #073642;
  --bs-secondary-color:  #586b72;  /* was #5F737B: 4.06:1 on this preset bg */
  --bs-border-color:     #87816E;
  --bs-tertiary-bg:      #FDF6E3;
  --bs-link-color:       #217AB9;
  --bs-link-hover-color: #1C689D;

  --sidebar-bg:                  #002B36;
  --sidebar-text:                #93A1A1;
  --sidebar-text-hover:          #EEE8D5;
  --sidebar-text-active:         #E3F1FB;
  --sidebar-icon-active:         #2AA198;
  --sidebar-hover-bg:            rgba(255,255,255,.05);
  --sidebar-active-bg:           rgba(42,161,152,.18);
  --sidebar-active-border:       #2AA198;
  --sidebar-section-label:       #84999e;
  --sidebar-section-label-hover: #93A1A1;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.04);
  --sidebar-group-accent:        rgba(42,161,152,.18);
  --sidebar-border:              #57838E;
}

/* ── Preset: Emerald — premium forest & gold ─────────────────────────────
   Identity: deep-green "premium fintech" surfaces with a gold accent on
   active sidebar items — a classic emerald + gold luxury pairing. */
html[data-portal-preset="emerald"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="emerald"] body.auth-page {
  --bg:            #F2F7F5;
  --surface:       #FFFFFF;
  --text:          #0F2A22;
  --muted:         #4F6B60;
  --border:        #778C83;
  --primary:       #047857;
  --primary-dark:  #035C43;
  --primary-light: #D1FAE5;

  --radius:       .625rem;
  --border-width: 1px;
  --shadow-sm: 0 1px 3px rgba(4,55,39,.08);
  --shadow:    0 4px 14px -2px rgba(4,55,39,.12);
  --shadow-lg: 0 16px 36px -8px rgba(4,55,39,.18);
  --letter-spacing-heading: -.02em;
  --motion-easing: cubic-bezier(.4, 0, .2, 1);

  --bs-body-bg:          #F2F7F5;
  --bs-body-color:       #0F2A22;
  --bs-secondary-color:  #4F6B60;
  --bs-border-color:     #778C83;
  --bs-tertiary-bg:      #EAF4F0;
  --bs-link-color:       #047857;
  --bs-link-hover-color: #035C43;

  --sidebar-bg:                  #0B2A21;
  --sidebar-text:                #A9CFC0;
  --sidebar-text-hover:          #F2F7F5;
  --sidebar-text-active:         #F3E2AE;
  --sidebar-icon-active:         #D4AF37;
  --sidebar-hover-bg:            rgba(255,255,255,.05);
  --sidebar-active-bg:           rgba(212,175,55,.16);
  --sidebar-active-border:       #D4AF37;
  --sidebar-section-label:       #7b9c90;
  --sidebar-section-label-hover: #A9CFC0;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.04);
  --sidebar-group-accent:        rgba(212,175,55,.18);
  --sidebar-border:              #5D8275;
}

/* ── Font family options — Settings → Appearance ───────────────────────── */
html[data-portal-font="system"] { --portal-font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
html[data-portal-font="poppins"] { --portal-font-family: 'Poppins', system-ui, sans-serif; }

/* ── Density (layout) presets — Settings → Appearance ──────────────────── */

/* Spacing-scale override. Added 2026-08-04, and this is what turns "compact"
   from a handful of hand-tightened selectors into a global setting: any
   component built on --space-* tightens automatically, with no per-page work
   and no new rule here when a component is added.

   Only the mid-range steps shrink. --space-1/2 are already at the floor of
   useful spacing (4/8px) and shrinking them produces cramped, touch-hostile
   controls; --space-7/8 are page-section rhythm, where compact users still
   want clear separation between major blocks. Squeezing everything uniformly
   is what makes "compact" modes feel broken rather than dense.

   Interactive HIT AREAS deliberately do not shrink with this — WCAG 2.2
   target-size minimums apply in compact mode too (see
   docs/frontend-design-system.md §4). Compact reduces the space BETWEEN
   things, not the things you have to hit. */
html[data-portal-density="compact"] {
  --space-3: .5rem;    /* 12 -> 8  */
  --space-4: .75rem;   /* 16 -> 12 */
  --space-5: 1rem;     /* 24 -> 16 */
  --space-6: 1.5rem;   /* 32 -> 24 */
}

/* The pre-existing hand-tightened rules below predate the spacing scale and
   are kept as-is: they target components that don't consume --space-* yet.
   As those components migrate (implementation order step 3) these should be
   deleted, not maintained in parallel. */
html[data-portal-density="compact"] .sidebar-link { padding: .42rem .65rem; font-size: .81rem; margin-bottom: 0; }
html[data-portal-density="compact"] .hrms-sidebar { padding: 1rem .75rem; }
html[data-portal-density="compact"] .sidebar-section-toggle { margin-top: .85rem; padding: .25rem .65rem; }
html[data-portal-density="compact"] .hrms-table thead th { padding: .55rem .75rem; }
html[data-portal-density="compact"] .hrms-table tbody td { padding: .6rem .75rem; }

/* ── Table components (Phase 4 of the UI audit) ───────────────────────────
   Furniture shared by every table: sortable header, pager, empty state.
   Everything here resolves through existing tokens, so these inherit all 16
   presets with no preset-specific rules -- the same property that lets
   messages-center.css stay preset-agnostic. */
.hrms-sort-th { white-space: nowrap; }
.hrms-sort-link {
  display: inline-flex; align-items: center; gap: .25rem;
  color: var(--muted);
  font-weight: 600;
  text-decoration: none;
  border-radius: .25rem;
}
.hrms-sort-link:hover { color: var(--text); }
.hrms-sort-link.is-active { color: var(--text); }
/* A visible focus ring is required here: these are the primary keyboard
   affordance for reordering a table and were previously plain links with
   the browser default suppressed by a text-reset utility. */
.hrms-sort-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.hrms-pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  margin-top: .9rem;
}
.hrms-pager-count { font-size: var(--fs-2xs); color: var(--muted); font-variant-numeric: tabular-nums; }

.hrms-empty-row td { border-bottom: none; }
.hrms-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .3rem;
  padding: 2.5rem 1.25rem;
  color: var(--muted);
  text-align: center;
}
.hrms-empty svg { opacity: .3; margin-bottom: .35rem; }
.hrms-empty-msg  { margin: 0; font-size: var(--fs-md); font-weight: 600; color: var(--text); }
.hrms-empty-hint { margin: 0; font-size: var(--fs-xs); }

/* ── Control boundaries ───────────────────────────────────────────────────
   The one place --border-strong is required rather than optional: a user has
   to be able to SEE where an input begins. Bootstrap's .form-control/.form-select
   default to --bs-border-color, which tracks the subtle --border. */
.form-control,
.form-select,
.form-check-input,
.hrms-input,
textarea.form-control {
  border-color: var(--border-strong, var(--border));
}

/* ── Toggle switch alignment ──────────────────────────────────────────────
   A switch and its label are one row and have to read as one row.

   Bootstrap positions the control with `float: left` plus a `margin-top`
   tuned for a 1em-square checkbox. Neither survives contact with this
   product: the accessibility pass grows switches to 24px on touch pointers
   (WCAG 2.2 target size), and a 24px control cannot be centred against a 17px
   line by a margin that assumes it is 16px. Measured before this rule: 8
   switches off by +3.5px on a coarse pointer, 1 off by +2.6px on a fine one.

   Flexbox instead of arithmetic. `align-items: center` centres whatever the
   control's height turns out to be against whatever the label's turns out to
   be, so the same rule holds for both pointer sizes, every preset's font, and
   any future change to either -- there is no magic number to go stale.

   `padding-left: 0` and `float: none` undo Bootstrap's own layout, which
   would otherwise fight the flex container and indent the label twice. */
.form-check.form-switch {
  display: flex;
  align-items: center;
  gap: .55em;
  padding-left: 0;
  min-height: 0;
}
.form-check.form-switch > .form-check-input {
  float: none;
  margin-left: 0;
  margin-top: 0;
  flex-shrink: 0;
}
.form-check.form-switch > .form-check-label { margin-bottom: 0; }

/* Switches built without Bootstrap's wrapper -- /kiosk/manage/new/ uses a
   bare `<input class="form-check-input" role="switch">` in its own container.
   No flex parent to lean on, so these centre on the text baseline instead. */
.form-check-input[role="switch"]:not(.form-check > *) {
  vertical-align: middle;
  margin-top: 0;
}

/* ── Motion presets — Settings → Appearance ───────────────────────────────
   Three levels, applied app-wide (Phase 4 of the UI audit). The previous
   version of this block named six sidebar selectors explicitly, so every
   other transition in the app — page reveals, card hovers, the mailbox
   fade, dashboard entrance staggers, loading shimmers — ignored the
   setting entirely. Two problems with enumerating selectors: it silently
   misses whatever gets added next, and ~137 of the ~200 transition
   declarations in this codebase are inline in templates, where no
   selector list here can reach them. A universal selector CAN reach them,
   which is why these use `*`.

   `!important` and `*` are deliberate here and not a shortcut. This is a
   user accessibility preference overriding presentational intent, which is
   exactly the case the cascade's importance layer exists for — the same
   reasoning behind the near-universal `prefers-reduced-motion` reset
   pattern.

   REDUCED keeps a perceptible-but-brief transition (~1 frame) rather than
   removing it: instant state flips read as jarring, and motion sensitivity
   is generally triggered by large or sustained movement, not a short fade.
   DISABLED removes motion outright, including scroll-reveal and looping
   shimmers, for users who want none at all. */
html[data-portal-motion="reduced"] *,
html[data-portal-motion="reduced"] *::before,
html[data-portal-motion="reduced"] *::after {
  transition-duration: 60ms !important;
  animation-duration: 60ms !important;
  scroll-behavior: auto !important;
}

html[data-portal-motion="disabled"] *,
html[data-portal-motion="disabled"] *::before,
html[data-portal-motion="disabled"] *::after {
  transition: none !important;
  animation: none !important;
  scroll-behavior: auto !important;
}

/* A looping animation is the one case where "duration: 60ms" is worse than
   no animation at all -- it spins 16x faster instead of calming down. Kill
   indefinitely-repeating motion outright at the Reduced level too. */
html[data-portal-motion="reduced"] [class*="shimmer"],
html[data-portal-motion="reduced"] [class*="skeleton"],
html[data-portal-motion="reduced"] [class*="pulse"],
html[data-portal-motion="reduced"] [class*="spin"] {
  animation: none !important;
}

/* The OS-level preference is honoured even when the tenant/user setting is
   left on Smooth -- an explicit "Off" choice in Settings is an escalation
   of this, never a way to opt back INTO motion someone asked their system
   to suppress. */
@media (prefers-reduced-motion: reduce) {
  html:not([data-portal-motion="disabled"]) *,
  html:not([data-portal-motion="disabled"]) *::before,
  html:not([data-portal-motion="disabled"]) *::after {
    transition-duration: 60ms !important;
    animation-duration: 60ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── Public CMS pages — follow the selected UI theme ────────────────────
   This block used to pin the whole public site to one hardcoded dark navy
   palette (--bg #0f172a / --surface #1e293b + a matching set of Bootstrap
   token overrides), deliberately, so that the light page body would stop
   looking unfinished between the dark hero bands the seeded content ships
   with. The cost was that Settings → Appearance → UI theme preset governed
   the portal only: picking Ocean, Classic or Light changed every /portal/
   screen and left the public site identical, which is not what a
   site-wide theme setting is understood to mean.

   The public site now inherits the SAME token set as every other surface.
   Two things make that work:

     * every html[data-portal-preset="X"] block declares its tokens on
       `body:is(.portal-page,.public-cms-page,.themed-page)`, so a public page picks up
       the active preset's --bg/--surface/--text/--border/--bs-* exactly as
       a portal page does — including the Bootstrap mirrors, which is what
       the deleted overrides here were compensating for;

     * the hero/CTA bands that motivated the forced-dark palette are no
       longer fixed navy either — they derive from --primary via
       --public-hero-bg / --public-hero-vivid below, so they re-tint with
       the preset instead of fighting it.

   Nothing is restated here: a token declared in this block would win over
   the preset (custom properties resolve to the nearest declaring ancestor,
   and this selector sits on the same <body> element), which is precisely
   the trap that made the preset invisible on the public site. Public-site
   colour belongs in a preset block or in :root, never here. */

/* ── Public hero / CTA bands ──────────────────────────────────────────────
   The full-bleed colour bands the public templates draw behind a page title
   (public/page.html, public/content_group_gate.html) and the marketing hero
   (.hero-section). All three were fixed indigo/navy literals, which is what
   forced the rest of the public site to be dark to match; derived from
   --primary/--accent they re-tint with whichever preset is selected.

   Every stop is mixed toward --public-hero-base, so the band stays dark
   enough for --public-hero-text (white) NO MATTER how light the preset's
   --primary is. That is the whole reason these are mixes rather than a bare
   var(--primary): several presets (Dark #818CF8, Glassmorphism) use a
   deliberately light accent that white text cannot sit on -- Dark's own
   --on-primary is near-black for exactly that reason.

   Measured against white at every stop, across all 22 presets. Worst case:

     --public-hero-bg      5.14:1  (glassmorphism; median 10.0:1)
     --public-hero-vivid   4.06:1  (glassmorphism; every other preset 5.98:1)

   --public-hero-bg carries the .lead subtitle at 20px regular, which is NOT
   large text, so it has to clear 4.5:1 outright -- it does, everywhere. Only
   --public-hero-vivid sits below that, and its copy is display-sized (the
   .hero-section h1 + lead), where AA-large's 3:1 applies. For reference the
   fixed gradient this replaces ended on a bare #0ea5e9, which measured
   2.77:1 under white on every preset including the default.

   On body rather than :root: --primary is declared on the body element by
   every preset block, and a custom property is substituted against the
   element that declares it -- from :root these would resolve against the
   :root fallback indigo on every preset. Same trap as --text-soft below. */
body {
  --public-hero-base: #0b1026;
  --public-hero-text: #fff;
  --public-hero-bg: linear-gradient(135deg,
      color-mix(in srgb, var(--primary) 30%, var(--public-hero-base))   0%,
      color-mix(in srgb, var(--primary) 58%, var(--public-hero-base))  50%,
      color-mix(in srgb, var(--accent)  32%, var(--public-hero-base)) 100%);
  --public-hero-vivid: linear-gradient(135deg,
      color-mix(in srgb, var(--primary) 55%, var(--public-hero-base))   0%,
      color-mix(in srgb, var(--primary) 68%, var(--public-hero-base))  50%,
      color-mix(in srgb, var(--accent)  60%, var(--public-hero-base)) 100%);
}

/* The title band public/page.html and public/content_group_gate.html draw
   above CMS content. A class, not the inline `style="background:linear-
   gradient(135deg,#1e1b4b...)"` + `.text-white` it replaces: three copies of
   one literal across two templates could not follow a theme, and there is no
   way to write a CSS variable into a Bootstrap utility class. */
.public-page-hero {
  background: var(--public-hero-bg);
  color: var(--public-hero-text);
}
.public-page-hero h1,
.public-page-hero .h1 { color: inherit; }

/* Readonly identity fields (contact form's name/email) stay visibly
   distinct from the ones you can actually type in -- otherwise "this is
   from your account" reads as "this field is broken". Themed rather than
   hardcoded (#172033/#94a3b8 read as a broken dark box on a light preset);
   the mix darkens on dark surfaces and lightens on light ones because
   --text is the theme's own foreground. */
body.public-cms-page .form-control[readonly],
body.public-cms-page .form-select[readonly] {
  background-color: color-mix(in srgb, var(--surface) 92%, var(--text));
  color: var(--muted);
}

/* ── Base ── */
body {
  font-family: var(--portal-font-family);
  background: var(--bg);
  color: var(--text);
  font-size: var(--portal-font-size);
  line-height: var(--portal-line-height, 1.6);
}
/* --primary-text, not --primary. --primary is the button FILL; using it for
   link TEXT put body-copy links at 3.97:1 on --bg (the page canvas is a step
   darker than --surface, which is the only ground the token audit checked) and
   it failed on 7 of the 22 presets. --primary-text is the same hue solved for
   legibility -- it clears AA on every preset's canvas. Guarded by the
   "link text on page bg" pair in preset_audit.py. */
a { color: var(--primary-text); text-decoration: none; }
a:hover { color: var(--primary-dark); }

/* ── Outline buttons: the border is the fill, the LABEL is ink ─────────────
   Bootstrap gives `.btn-outline-danger` a transparent background and paints
   both the border AND the text with the vivid `--bs-danger`. That is right for
   the border (SC 1.4.11 asks 3:1 of a boundary, and it clears that) and wrong
   for the text, which needs 4.5:1: #EF4444 on white measures 3.76:1, found on
   /me/privacy/'s "Request account deletion".

   The `--*-text` tokens exist for precisely this and are already audited
   against both grounds. The border keeps the vivid value, so nothing about the
   button's appearance changes except that its label becomes readable.

   Setting `--bs-btn-color` here does NOT work and was tried first: this file
   already gives each outline button a direct `color:` (search
   `.btn-outline-danger {`), and a direct declaration beats
   `.btn { color: var(--bs-btn-color) }` no matter what the variable holds.
   The fix is on those four rules; this note stays as the signpost. */

/* A hero paints its own dark ground and declares its own foreground, but had
   no rule for links -- so a bare <a> in hero copy inherited the light-ground
   link colour and sat dark-on-dark. Buttons are excluded: they carry a fill
   and foreground of their own. */
.hero-section a:not(.btn) { color: var(--public-hero-text); text-decoration: underline; }
.hero-section a:not(.btn):hover { color: #fff; }

/* WCAG 2.2 target size. A flex link is a standalone row -- a card action, a
   list item -- never a word inside a sentence, so 2.5.8's inline exception
   does not cover it. These measured 22px. */
body.public-cms-page a:is(.d-flex, .d-inline-flex) { min-height: 24px; }

/* .btn-link paints itself from --bs-btn-color, so the `a {}` rule above never
   reaches it -- it was the single contrast node left on the public site after
   the link role was fixed. Same substitution: the fill token for the text one. */
.btn-link {
  --bs-btn-color:       var(--primary-text);
  --bs-btn-hover-color: var(--primary-dark);
}

/* Heading typeface/tracking is a per-preset token so Classic (serif),
   Modern (Poppins) and Win7 (Segoe UI) read as genuinely different
   typography, not just a re-tinted Default — see the --heading-font-family
   / --letter-spacing-heading overrides on each html[data-portal-preset]
   block above. */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--heading-font-family, var(--portal-font-family));
  letter-spacing: var(--letter-spacing-heading, -.02em);
}

/* ── Navbar (public site) ── */
.hrms-nav {
  /* Fully opaque — this header is sticky over hero banners with colorful
     gradients (Home, News) that scroll underneath it. A semi-transparent
     backdrop-blur background let that color bleed through and washed out
     the muted-gray nav-link text, making the links hard to read/easy to
     miss against busy content rather than the plain page background it
     looked fine against in isolation. */
  background: var(--surface, #fff);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 1px 2px rgb(0 0 0 / .03);
  position: sticky;
  top: 0;
  z-index: 1030;
  padding: .25rem 0;
}
/* ── Header heights ───────────────────────────────────────────────────────
   One definition each, because these three are arithmetically coupled: the
   sidebar's sticky offset and its calc(100vh - N) must track whatever the
   header above it actually measures, and the portal topbar is the public
   nav's height plus its own padding and border. They were previously ten
   separate hardcoded values, so changing the header meant finding all of
   them and getting the arithmetic right by hand.
   ------------------------------------------------------------------------ */
:root {
  --nav-container-h:  60px;   /* public .hrms-nav container (was 76px) */
  --portal-topbar-h:  58px;   /* portal/mgmt topbar (was 72px, hardcoded in
                                 portal-compact.css and 13px out of step with
                                 the 85px this file assumed) */
  --nav-offset:       var(--portal-topbar-h);  /* the mgmt sidebar hangs from
                                 the topbar, so it is the same number by
                                 definition -- not an independent value */
}

.hrms-nav .container { min-height: var(--nav-container-h); display: flex; align-items: center; }
.hrms-nav .navbar-brand {
  display: flex; align-items: center;
  font-weight: 800;
  font-size: 1.05rem;
  letter-spacing: -.02em;
  max-width: 70vw;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.hrms-nav .navbar-brand svg { color: var(--primary); }
/* The hamburger inherits currentColor from the button. Stated on every theme
   rather than only under [data-bs-theme="dark"], so it tracks the preset the
   same way .nav-link beside it does. */
.hrms-nav .navbar-toggler svg { color: var(--muted); }
.hrms-nav .nav-link {
  position: relative;
  font-weight: 500;
  font-size: .9375rem;
  color: var(--muted);
  padding: .65rem .95rem;
  border-radius: var(--radius-sm);
  transition: background .15s ease, color .15s ease;
}
.hrms-nav .nav-link::after {
  content: ""; position: absolute; left: .85rem; right: .85rem; bottom: .3rem; height: 2px;
  background: var(--primary); border-radius: 2px; transform: scaleX(0); transform-origin: center;
  transition: transform .18s ease;
}
.hrms-nav .nav-link:hover { color: var(--text); }
.hrms-nav .nav-link:hover::after { transform: scaleX(.45); }
.hrms-nav .nav-link.active { color: var(--primary); font-weight: 600; }
.hrms-nav .nav-link.active::after { transform: scaleX(1); }
.hrms-nav .btn-nav-cta {
  display: inline-flex; align-items: center;
  background: var(--primary);
  color: #fff;
  font-weight: 600;
  font-size: .875rem;
  padding: .5rem 1.15rem;
  border-radius: .55rem;
  border: none;
  box-shadow: 0 1px 2px rgb(0 0 0 / .05), 0 0 0 0 rgb(79 70 229 / 0);
  transition: background .15s ease, transform .12s ease, box-shadow .15s ease;
}
.hrms-nav .btn-nav-cta:hover {
  background: var(--primary-dark); color: #fff; transform: translateY(-1px);
  box-shadow: 0 4px 12px -2px rgb(79 70 229 / .35);
}

/* ── Shared "signed in as" account button — used in both the public nav
   (.hrms-nav) and the portal topbar (.portal-topbar), both of which have
   a light/white background. This used to be a near-invisible white-on-
   white pill (rgba(255,255,255,.12) border + .06 background) inherited
   from an earlier dark-navbar design that never got updated when the
   navbar itself became light — fixed with a real neutral border and
   text color that's actually visible against a light surface, plus a
   matching dark-mode variant. ── */
.navbar-user-btn {
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: .875rem; line-height: 1;
  background: var(--bg);
  color: var(--text);
  padding: .35rem .6rem;
  transition: background-color .15s ease, border-color .15s ease;
}
.navbar-user-btn:hover { background: var(--primary-light); border-color: var(--primary); }
[data-bs-theme="dark"] .navbar-user-btn { border-color: #334155; background: rgba(255,255,255,.05); color: #e2e8f0; }
[data-bs-theme="dark"] .navbar-user-btn:hover { background: rgba(99,102,241,.15); border-color: #6366f1; }

/* ── Cards ── */
/* A CARD IS A SURFACE, not the page.

   `--bs-card-bg` has to be set HERE, on `.card` itself. Bootstrap declares
   `--bs-card-bg: var(--bs-body-bg)` inside its own `.card` rule, and a
   declaration on the element beats one inherited from an ancestor — so setting
   it in the token block above changed nothing at all. (Verified on production:
   the tokens shipped and cards still measured the page colour.)

   `--bs-body-bg` is the PAGE fill (#ECF0F1 on Flat), so every card rendered the
   same colour as the canvas behind it. Measured across ten public pages: 48 of
   48 visible cards matched the page exactly. Under Flat that is fatal — the
   preset has no shadow, and most of these cards carry `.border-0`, so fill was
   the only thing left that could separate them and it was the wrong one.
   See [[invisible-card-border0-on-surface]] for the same defect from the other
   direction. */
.card {
  --bs-card-bg: var(--surface);
  background-color: var(--surface);
  border: var(--border-width, 1px) solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  transition: box-shadow .2s var(--motion-easing), transform .2s var(--motion-easing);
}
.card:hover { box-shadow: var(--shadow); }
.card-header {
  background: transparent;
  border-bottom: 1px solid var(--border);
  font-weight: 600;
  font-size: .875rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .05em;
  padding: .875rem 1.25rem;
}

/* ── KPI stat cards ── */
.kpi-card { border-radius: var(--radius); padding: 1.25rem; position: relative; overflow: hidden; }
.kpi-card .kpi-icon {
  width: 2.75rem; height: 2.75rem;
  border-radius: .625rem;
  display: flex; align-items: center; justify-content: center;
  opacity: .9;
}
.kpi-card .kpi-value { font-size: 2rem; font-weight: 800; line-height: 1; margin-top: .5rem; }
.kpi-card .kpi-label { font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; opacity: .7; margin-top: .25rem; }
.kpi-card::after {
  content: '';
  position: absolute; right: -1rem; top: -1rem;
  width: 5rem; height: 5rem;
  border-radius: 50%;
  background: currentColor;
  opacity: .06;
}

/* ── Hero ── */
.hero-section {
  background: var(--public-hero-vivid);
  color: var(--public-hero-text);
  padding: 5rem 0 4rem;
  position: relative;
  overflow: hidden;
}
.hero-section::before {
  content: '';
  position: absolute; inset: 0;
  background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
/* Two soft radial glows add depth behind the hero copy/mockup without a
   licensed photo — z-index:1 on the container in home.html keeps content
   above both this and ::before. */
.hero-section::after {
  content: '';
  position: absolute; inset: 0;
  background:
    radial-gradient(38rem 26rem at 88% -10%, rgba(34,211,238,.20) 0%, transparent 60%),
    radial-gradient(30rem 24rem at -5% 110%, rgba(129,140,248,.22) 0%, transparent 65%);
  pointer-events: none;
}
.hero-section h1 { font-size: clamp(2rem, 5vw, 3.25rem); font-weight: 800; letter-spacing: -.03em; }
.hero-section p.lead { font-size: 1.125rem; opacity: .88; max-width: 600px; }

/* ── Interior pages: the same band, about half as tall ────────────────────
   Home's hero is a landing hero and earns 80px/64px of air. Every other
   public page shares the band so the site opens the same way everywhere
   (see templates/public/_page_intro.html), but an interior page's job is to
   name itself and get out of the way — a full-height hero above /faq/ pushes
   the first question below the fold. The h1 steps down with it; the colour,
   texture and glows are untouched, which is the part that has to match. */
.hero-section.is-compact { padding: 2.5rem 0 2rem; }
.hero-section.is-compact h1 { font-size: clamp(1.55rem, 3vw, 2.1rem); }
.hero-section.is-compact p { font-size: 1rem; max-width: 68ch; }
.hero-section.is-compact p.lead { font-size: 1.05rem; }
/* Authored `mb-5` under the heading is spacing for a heading that sat on the
   page; inside a band the band's own padding provides it, and leaving both
   makes the "compact" hero taller than the full one it replaces. */
.hero-section.is-compact > .container > :last-child { margin-bottom: 0 !important; }

/* `.text-muted` is the single most common class in these authored intros, and
   inside the band it resolves to --muted — a mid grey chosen for a light page,
   which on the band measures under 3:1. Muted here means "a step back from
   white", not "the page's muted grey". Same for the small/​opacity variants
   Bootstrap ships, so a subtitle stays a subtitle. */
/* Scoped to `.hero-section` ONLY, deliberately.
   `.text-muted` resolves to --muted, a grey picked for a light page, and inside
   the intro band it lands under 3:1 — so the band's own component fixes it.

   Extending the same idea to CMS bands (`[style*="--public-hero"]`) was tried
   three ways and each traded one failure for another, because a themed band is
   NOT uniformly dark: several wrap cards that paint their own light surface.
   Measured on 12 public pages — a plain descendant selector cost 38 failures on
   /packages-and-subscriptions/ and 40 on /solutions/; adding
   `:not(.card *):not([style*="background"] *)` excluded the band itself and did
   nothing; redefining --muted inside the band leaked past the card exceptions
   and cost 21. The remaining CMS-band body copy measures 4.04:1 — under AA for
   normal text, above the 3:1 large-text bar, and not worth a fourth attempt
   that makes something else invisible. Left as a known gap on purpose. */
.hero-section :is(.text-muted, .text-body-secondary, small) {
  color: rgba(255, 255, 255, .82) !important;
}
.hero-section :is(.text-dark, .text-body, .text-black) { color: #fff !important; }

@media (max-width: 575px) {
  .hero-section.is-compact { padding: 1.75rem 0 1.5rem; }
}
.hero-badge {
  display: inline-flex; align-items: center; gap: .375rem;
  background: rgba(255,255,255,.15);
  border: 1px solid rgba(255,255,255,.2);
  padding: .3rem .85rem;
  border-radius: 999px;
  font-size: .8rem; font-weight: 600;
  backdrop-filter: blur(4px);
  margin-bottom: 1.25rem;
}

/* ── Feature cards ── */
.feature-card {
  background: var(--surface);
  border: var(--border-width, 1px) solid var(--border);
  border-radius: var(--radius);
  padding: 1.5rem;
  height: 100%;
  transition: transform .2s var(--motion-easing), box-shadow .2s var(--motion-easing);
}
.feature-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
.feature-icon {
  width: 3rem; height: 3rem;
  border-radius: var(--radius-sm);
  display: flex; align-items: center; justify-content: center;
  background: var(--primary-light);
  margin-bottom: 1rem;
}
.feature-icon svg { color: var(--primary); }

/* ── A bare `.badge` must not assume white text ───────────────────────────
   Bootstrap's `.badge` hardcodes `color: #fff` and expects a `text-bg-*` /
   `bg-*` class to supply the dark ground that makes it readable. A badge
   written with NEITHER is white on whatever is behind it — which was fine
   while the public canvas was always dark, and is invisible now that it
   follows the theme.

   Eight of them were live on the home page: the `FLAGSHIP` / `PRODUCT`
   eyebrow labels on the product cards, measuring 1.15:1 on Flat's #ECF0F1.

   `color: inherit` rather than a token: the badge should read as part of the
   card it sits in, whatever that card's text colour is — which also keeps it
   correct inside a dark band, where inherit is already light. Scoped to the
   public site and to badges that declare no ground of their own, so every
   correctly-authored `text-bg-primary` badge is untouched. Verified across ten
   live pages: unreadable badges 18 -> 10, and not one page got worse. */
body.public-cms-page .badge:not([class*="text-bg-"]):not([class*="bg-"]):not([style*="background"]) {
  color: inherit;
}

/* ── Stroke icons that were authored without a stroke ─────────────────────
   A Lucide/Feather icon is `fill="none"` plus `stroke="currentColor"`. Drop
   the stroke half and the <svg> still lays out, still has its box, still
   passes every contrast, size and alt-text check -- and paints ZERO pixels.
   17 such icons were live on the public site (10 on /faq/, 4 on /about/,
   3 on /contact/), all in CMS-stored HTML, showing as empty tinted squares.
   They have been invisible under every preset since they were authored; Flat
   is simply where an empty square stops looking deliberate.

   The selector is deliberately narrow, and each half of it is load-bearing:
   the SVG must declare fill:none and carry no stroke of its own, AND the
   shape must declare neither. That combination cannot paint by construction,
   so nothing this matches was ever visible. Verified against production
   rather than argued: it repaired all 17 and added an outline to 0 of the
   99 correctly-authored fill:none icons on / and /solutions/, whose shapes
   carry their own stroke attribute and are therefore excluded. */
svg[fill="none"]:not([stroke]) >
  :where(path,rect,circle,line,polyline,polygon,ellipse):not([fill]):not([stroke]) {
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Product/cross-sell cards on the public marketing pages get the same
   lift-on-hover as .feature-card. Scoped to body.public-cms-page (not a
   bare .card rule) so portal/admin cards — dense dashboard tiles that
   shouldn't jump around on hover — are unaffected. */
body.public-cms-page .card {
  transition: transform .2s var(--motion-easing), box-shadow .2s var(--motion-easing);
}
body.public-cms-page .card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }

/* ── "How it works" numbered steps ──────────────────────────────────────
   The connecting dashed line that used to run behind the circles has been
   removed: the numbered circles already convey sequence, and the dashes read
   as a stray rule rather than a connector -- particularly on the dark theme,
   where they sit at low contrast between widely-spaced columns.

   .how-steps is kept as the row hook (the columns still rely on its
   stacking context) so the markup does not have to change.
   ── */
.how-steps { position: relative; }
.how-steps > [class*="col-"] { position: relative; z-index: 1; }

/* ── Home "who we serve" carousel — real photography, high-res sources,
   slow Ken-Burns zoom on the active slide only (kept subtle; the zoom
   lives on the inner .who-slide-bg layer, never on .carousel-item itself,
   so it can't fight Bootstrap's own translateX transform during slide
   transitions). ── */
.who-slide {
  position: relative;
  height: 440px;
  display: flex; align-items: flex-end;
  overflow: hidden;
}
.who-slide-bg {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
  transform: scale(1.04);
  transition: transform 7s cubic-bezier(.25,.46,.45,.94);
}
.carousel-item.active .who-slide-bg { transform: scale(1.15); }
.who-slide-scrim {
  position: absolute; inset: 0;
  background: linear-gradient(0deg, rgba(5,10,20,.92) 0%, rgba(5,10,20,.55) 55%, rgba(5,10,20,.15) 100%);
}
.who-slide-credit {
  position: absolute; right: .75rem; bottom: .5rem; z-index: 1;
  font-size: .65rem; color: rgba(255,255,255,.55);
  letter-spacing: .01em;
}
@media (prefers-reduced-motion: reduce) {
  .who-slide-bg { transition: none; transform: none; }
}
@media (max-width: 767.98px) { .who-slide { height: 340px; } }

#whoWeServeCarousel .carousel-control-prev,
#whoWeServeCarousel .carousel-control-next { width: 4rem; opacity: 1; }
#whoWeServeCarousel .carousel-control-prev-icon,
#whoWeServeCarousel .carousel-control-next-icon {
  width: 2.5rem; height: 2.5rem;
  background-color: rgba(15,23,42,.55);
  border: 1px solid rgba(255,255,255,.25);
  border-radius: 50%;
  background-size: 1.1rem;
  backdrop-filter: blur(3px);
  transition: background-color .15s ease, transform .15s ease;
}
#whoWeServeCarousel .carousel-control-prev:hover .carousel-control-prev-icon,
#whoWeServeCarousel .carousel-control-next:hover .carousel-control-next-icon {
  background-color: rgba(79,70,229,.85);
  transform: scale(1.08);
}
#whoWeServeCarousel .carousel-indicators { margin-bottom: .25rem; }
#whoWeServeCarousel .carousel-indicators [data-bs-target] {
  width: 2.25rem; height: 4px;
  border-radius: 999px;
  background-color: rgba(255,255,255,.35);
  opacity: 1;
  /* Transparent border, NOT `border: 0`.
     The generic rule further down already gives every carousel dot an 11px
     transparent band top and bottom, so a 4px bar is a 26px target. This
     block's `border: 0` was more specific (#id .class [attr]) and silently
     switched that off, which is why the home page's slide buttons measured
     36x4 -- a fifth of the WCAG 2.2 minimum, and the hardest kind of target
     to hit on a phone because it is thin in the direction of the thumb.
     background-clip keeps the visible bar 4px while the hit area is not. */
  border: 11px solid transparent;
  border-left-width: 0; border-right-width: 0;
  background-clip: padding-box;
  transition: background-color .2s ease, width .2s ease;
}
#whoWeServeCarousel .carousel-indicators .active { background-color: #818cf8; width: 3rem; }

/* ── Demo page (templates/public/demo.html) ──────────────────────────────
   Carousel framed like a small browser window (chrome dots bar + capped
   height) so the reused placeholder screenshot reads as an app preview
   rather than a full-bleed hero image, and a native <details> disclosure
   per accordion row for inline "how to use" instructions (no JS, no risk
   of ID collisions across three dozen module rows). ── */
body.public-cms-page .demo-carousel-frame {
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  /* The "browser window" is the app's own chrome, so it takes the theme's
     surface rather than a fixed navy. It was #0f172a with a white-alpha
     border, which on a light preset is a black rectangle with an invisible
     edge around a screenshot. */
  background: var(--surface);
  border: 1px solid var(--border);
}
body.public-cms-page .demo-window-chrome {
  display: flex;
  gap: .4rem;
  padding: .55rem .8rem;
  /* A tint OF the surface, in whichever direction the theme runs: lighter
     than a dark surface, darker than a light one. rgba(255,255,255,.04) only
     did the former and vanished entirely on the light presets. */
  background: color-mix(in srgb, var(--surface) 94%, var(--text));
}
body.public-cms-page .demo-window-chrome span {
  width: .6rem; height: .6rem;
  border-radius: 50%;
  display: inline-block;
}
body.public-cms-page .demo-window-chrome span:nth-child(1) { background: #ef4444; }
body.public-cms-page .demo-window-chrome span:nth-child(2) { background: #f59e0b; }
body.public-cms-page .demo-window-chrome span:nth-child(3) { background: #10b981; }
body.public-cms-page .demo-carousel-frame img {
  height: 420px;
  object-fit: cover;
  object-position: top center;
}
@media (max-width: 767.98px) { body.public-cms-page .demo-carousel-frame img { height: 280px; } }

body.public-cms-page details.demo-howto summary {
  cursor: pointer;
  font-size: .8rem;
  font-weight: 600;
  /* --primary is tuned as a fill on light surfaces; as text on this dark page
     it measured 2.32:1. --primary-text is the text-role sibling. */
  color: var(--primary-text);
  list-style: none;
}
body.public-cms-page details.demo-howto summary::-webkit-details-marker { display: none; }
body.public-cms-page details.demo-howto summary::before { content: "▸ "; }
body.public-cms-page details.demo-howto[open] summary::before { content: "▾ "; }
body.public-cms-page details.demo-howto ol { padding-left: 1.1rem; }

/* ── Demo page: module accordion ──────────────────────────────────────────
   Bootstrap's accordion is built for light surfaces: its default item
   background, border and chevron are all near-white, so on this page the
   items read as one undifferentiated slab and the chevron is invisible.
   Restated here against the page's own tokens.

   Readability: the module summaries used `.text-muted small`, which resolves
   to --muted (#94a3b8) at ~0.875rem. That measures 5.6:1 on --surface --
   technically AA, but it is the densest text on the page and sits under a
   bolder title, so it read as switched-off. --text-soft sits between --text
   and --muted: enough lift off --muted to stop the small print reading as
   switched-off, enough drop from --text to keep the hierarchy.

   Derived rather than fixed. It was #cbd5e1 -- a light-on-dark value, correct
   only while the public site was pinned dark, and near-white-on-white the
   moment it followed a light preset. Mixing the theme's OWN --text with its
   own --muted moves in whichever direction that preset runs, and can never
   land outside the range those two already passed the contrast audit at.

   Declared on `body`, NOT on :root. A custom property is substituted against
   the computed values of the element that DECLARES it, and the presets put
   --text/--muted on the body element -- so a :root declaration here would
   silently resolve against :root's light-theme --text on every preset. */
body {
  --text-soft: color-mix(in srgb, var(--text) 78%, var(--muted));
}

body.public-cms-page #demoModuleTree .accordion-item {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: .75rem;
  overflow: hidden;                 /* keeps the header's corners rounded */
  transition: border-color .15s ease, box-shadow .15s ease;
}
/* The accent-tinted edges were rgba(165,180,252,…) -- a fixed pale indigo,
   picked when this page was always dark. On a light preset that is a paler
   colour than the border it is meant to be highlighting over, so hovering an
   item made its outline WEAKER. Mixed from --primary instead: same intent,
   in whichever hue and direction the preset runs. */
body.public-cms-page #demoModuleTree .accordion-item:hover {
  border-color: color-mix(in srgb, var(--primary) 45%, transparent);
}
body.public-cms-page #demoModuleTree .accordion-item:has(.accordion-button:not(.collapsed)) {
  border-color: color-mix(in srgb, var(--primary) 60%, transparent);
  box-shadow: var(--shadow-lg);
}

body.public-cms-page #demoModuleTree .accordion-button {
  background: transparent;
  color: var(--text);
  font-weight: 700;
  font-size: 1.02rem;
  letter-spacing: -.01em;
  padding: 1rem 1.15rem;
  box-shadow: none;
}
body.public-cms-page #demoModuleTree .accordion-button:not(.collapsed) {
  background: linear-gradient(180deg,
      color-mix(in srgb, var(--primary) 18%, transparent),
      color-mix(in srgb, var(--primary) 6%, transparent));
  /* Was `color: #fff` over that same faint tint. White is only readable if
     the surface underneath is dark, which it was when the public site was
     pinned dark and is not on the fourteen light presets -- the open row's
     own title would have gone white-on-white. --primary-text is the
     text-role token, solved per preset against exactly this kind of
     primary tint. */
  color: var(--primary-text);
}
/* Bootstrap's chevron is a dark-ink SVG data URI. It disappears on a dark
   surface, so it is inverted -- but ONLY there: inverting it on a light
   preset produces a white chevron on a white row, which is the same bug in
   the other direction. Recoloured by filter rather than by shipping a second
   asset. */
body.public-cms-page #demoModuleTree .accordion-button::after {
  transition: transform .2s ease;
}
[data-bs-theme="dark"] body.public-cms-page #demoModuleTree .accordion-button::after {
  filter: invert(1) brightness(1.7);
}
body.public-cms-page #demoModuleTree .accordion-button:focus-visible {
  outline: 2px solid var(--primary-text);
  outline-offset: -2px;
}
body.public-cms-page #demoModuleTree .accordion-body { padding: .35rem 1.15rem 1rem; }

body.public-cms-page .demo-module-row { border-color: var(--border) !important; }
body.public-cms-page .demo-module-row:last-child { border-bottom: 0 !important; }
body.public-cms-page .demo-module-row .fw-semibold { color: var(--text); }
/* The readability fix: applies to the module summary and the how-to steps. */
body.public-cms-page .demo-module-row .text-muted,
body.public-cms-page details.demo-howto ol.text-muted {
  color: var(--text-soft) !important;
}

/* ── Demo page: clickable screenshots ─────────────────────────────────────
   A button wrapping the image, so it is keyboard-reachable and announced as
   a control. The zoom badge only appears on hover/focus -- present at rest it
   would sit on top of the very screenshot it is advertising. ── */
body.public-cms-page .demo-shot {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: none;
  cursor: zoom-in;
  transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease;
}
body.public-cms-page .demo-shot:hover,
body.public-cms-page .demo-shot:focus-visible {
  transform: translateY(-2px);
  /* Mixed from --primary rather than the fixed pale indigo this used to be,
     for the same reason as the accordion edges above. */
  border-color: color-mix(in srgb, var(--primary) 70%, transparent);
  box-shadow: var(--shadow-lg);
}
body.public-cms-page .demo-shot:focus-visible { outline: 2px solid var(--primary-text); outline-offset: 2px; }
body.public-cms-page .demo-shot img { display: block; width: 100%; }
body.public-cms-page .demo-shot-zoom {
  position: absolute;
  right: .6rem;
  bottom: .6rem;
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .35rem .6rem;
  border-radius: 999px;
  font-size: .75rem;
  font-weight: 600;
  color: #fff;
  background: rgba(15, 23, 42, .82);
  border: 1px solid rgba(255, 255, 255, .18);
  backdrop-filter: blur(4px);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .18s ease, transform .18s ease;
}
body.public-cms-page .demo-shot:hover .demo-shot-zoom,
body.public-cms-page .demo-shot:focus-visible .demo-shot-zoom {
  opacity: 1;
  transform: translateY(0);
}
/* Touch devices have no hover, so the affordance would never appear. */
@media (hover: none) {
  body.public-cms-page .demo-shot-zoom { opacity: 1; transform: none; }
}

.demo-shot-modal .modal-content {
  background: var(--surface, #1e293b);
  border: 1px solid rgba(255, 255, 255, .12);
  border-radius: var(--radius, 14px);
}
.demo-shot-modal .modal-title { color: #e2e8f0; font-size: .95rem; font-weight: 600; }
.demo-shot-modal .btn-close { filter: invert(1) grayscale(1) brightness(1.8); }

/* ── Sidebar (mgmt layout) ── */
/* ── App-shell scrolling ──────────────────────────────────────────────────
   The sidebar is exactly as tall as the viewport below the top bar, and
   scrolls ITSELF when its nav is taller than that.

   It used to be `min-height` + `overflow: hidden`, which is the combination
   that loses content: the column grew past the viewport, `position: sticky`
   pinned it, and everything below the fold was clipped with no way to reach
   it. On a 768px-tall laptop the lower nav sections (Benefits, Assets,
   Settings) were simply unreachable -- not scrolled off, GONE.

   `height` (not min-height) + `overflow-y: auto` is the standard app-shell
   pattern: independent scroll regions, so the sidebar can be long without
   pushing the page, and the page can be long without moving the nav.

   overflow-x stays hidden -- the collapse animation slides content out
   sideways, and `overflow-y: auto` alone would compute `overflow-x: auto`
   and flash a horizontal bar through every collapse. */
.hrms-sidebar {
  background-color: var(--sidebar-bg);
  background-image: linear-gradient(180deg, rgb(255 255 255 / .025) 0%, transparent 30%), linear-gradient(0deg, rgb(0 0 0 / .08) 0%, transparent 12%);
  height: calc(100vh - var(--nav-offset));
  /* dvh tracks the real viewport as mobile browser chrome shows/hides; the
     vh line above stays as the fallback for anything that lacks it. */
  height: calc(100dvh - var(--nav-offset));
  padding: 1.5rem 1rem;
  position: sticky;
  top: var(--nav-offset);
  width: 240px;
  min-width: 240px;
  transition: width var(--motion-med) ease, min-width var(--motion-med) ease, padding var(--motion-med) ease, background var(--motion-med) ease;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;   /* don't chain the page scroll at either end */
  flex-shrink: 0;
  box-shadow: 1px 0 0 var(--sidebar-border), 4px 0 24px -8px rgb(0 0 0 / .25);
  scrollbar-width: thin;
  scrollbar-color: var(--sidebar-border) transparent;
}
/* Thin, dim, and only visible over the sidebar -- a full-weight scrollbar on
   a dark nav column reads as a second border. */
.hrms-sidebar::-webkit-scrollbar { width: 8px; }
.hrms-sidebar::-webkit-scrollbar-track { background: transparent; }
.hrms-sidebar::-webkit-scrollbar-thumb {
  background: var(--sidebar-border);
  border-radius: 4px;
  border: 2px solid transparent;
  background-clip: content-box;
}
.hrms-sidebar::-webkit-scrollbar-thumb:hover { background: var(--sidebar-text-hover, #94a3b8); background-clip: content-box; }
/* The content column, as the other half of the same app shell: its own
   scroll region, exactly the viewport tall, so the top bar and the nav stay
   put while a long table scrolls under them.

   Desktop only. On a phone the sidebar is an overlay rather than a column,
   and a nested scroll container there fights the browser's own address-bar
   hide/show and its pull-to-refresh -- so below the breakpoint the page
   scrolls normally, which is what a phone user expects. */
@media (min-width: 768px) {
  .portal-main-scroll {
    height: calc(100vh - var(--nav-offset));
    height: calc(100dvh - var(--nav-offset));
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong, var(--border)) transparent;
  }
  .portal-main-scroll::-webkit-scrollbar { width: 10px; }
  .portal-main-scroll::-webkit-scrollbar-track { background: transparent; }
  .portal-main-scroll::-webkit-scrollbar-thumb {
    background: var(--border-strong, var(--border));
    border-radius: 5px;
    border: 2px solid transparent;
    background-clip: content-box;
  }
  .portal-main-scroll::-webkit-scrollbar-thumb:hover {
    background: var(--muted);
    background-clip: content-box;
  }
}

/* ── Collapsed state ── */
.hrms-sidebar.sidebar-collapsed {
  /* One rhythm for the whole rail. Every row in the icon-only state is the
     same box built from these two, so the column reads as a column instead
     of a ragged stack, and there is exactly one number to change to make the
     rail denser or roomier. --rail-item-h is above the 24px target-size
     floor (WCAG 2.2 SC 2.5.8) that portal-compact.css enforces elsewhere. */
  --rail-item-h: 2.25rem;
  --rail-gap: .125rem;
  width: 64px;
  min-width: 64px;
}
/* Horizontal padding only, so the vertical stays whatever the active density
   preset chose and collapsing doesn't also shift the rail's top edge.
   The leading `html` is load-bearing: the density presets above set `padding`
   through `html[data-portal-density="…"] .hrms-sidebar`, which out-specifies a
   bare `.hrms-sidebar.sidebar-collapsed` (same two class-level parts, and the
   element breaks the tie) — so under compact density the rail silently kept
   12px of side padding and this rule never applied at all. Matching that
   specificity, later in the file, is what makes it win. */
html .hrms-sidebar.sidebar-collapsed { padding-inline: .5rem; }

/* ── Sidebar position — Settings → Appearance → "Sidebar position" ──────
   Flipping the flex order moves the sidebar to the right edge without
   touching any of its own width/spacing/background rules; only the
   directional shadow (cast toward the content on the left by default)
   needs its own mirrored rule to still point at the content. ── */
html[data-sidebar-position="right"] .portal-shell { flex-direction: row-reverse; }
html[data-sidebar-position="right"] .hrms-sidebar {
  box-shadow: -1px 0 0 var(--sidebar-border), -4px 0 24px -8px rgb(0 0 0 / .25);
}
/* Collapsing/expanding fades + shrinks the text labels in sync with the
   sidebar's own width transition (both use --motion-med) instead of an
   instant display:none swap — toggling display can't be animated at all,
   so it used to make text vanish in a single frame while the sidebar box
   was still mid-way through its 200ms width animation, a jarring
   snap/flash right in the middle of an otherwise smooth motion. Overflow
   stays hidden on the link itself so the shrinking text never wraps or
   spills outside the icon-only rail while it's mid-transition. */
.sidebar-link-text {
  display: inline-block; overflow: hidden;
  max-width: 12rem; opacity: 1;
  transition: max-width var(--motion-med) ease, opacity var(--motion-fast) ease, margin var(--motion-med) ease;
}
/* The label-hiding + rail-geometry rules live in the ≥768px block further
   down ("Collapsed = icon rail"), NOT here. Below 768px the sidebar is an
   off-canvas drawer that shows itself in full regardless of the saved
   collapse preference, and these rules used to leak into it: the mobile
   block can override `display`, but `max-width: 0; opacity: 0` survived it,
   so a user whose stored preference was "collapsed" opened the drawer to
   group headers with blank labels. */
.hrms-sidebar.sidebar-collapsed .sidebar-user-info { display: none; }
.sidebar-user-avatar-only { display: none; }
.hrms-sidebar.sidebar-collapsed .sidebar-user-avatar-only {
  display: flex !important; justify-content: center; align-items: center;
  /* Hairline above it so the badge reads as the rail's footer rather than as
     one more icon that happens to sit further down than the rest. */
  margin-top: auto; padding-top: .6rem; position: relative;
  border-top: 1px solid var(--sidebar-border);
}
/* Profile hover popup in collapsed mode — positioned to the RIGHT of the
   badge. `left`/`bottom` here are just a fallback for the instant before
   JS runs (mgmt.html's positionAvatarPopup(), wired to mouseenter/focusin
   on .sidebar-user-avatar-only): those two numbers used to be hardcoded
   as "sidebar collapsed width (64px) + 4px gap" / "anchor near the
   viewport bottom", which drifted out of sync with the avatar's *actual*
   rendered position (density/preset changes, different viewport heights)
   and made the popup appear detached from the badge it belongs to. The
   JS now reads the real getBoundingClientRect() of the avatar every time
   the popup opens and sets `left`/`bottom` as inline styles, so this
   stays correct regardless of how the sidebar is sized. */
.sidebar-avatar-popup {
  display: none;
  position: fixed;          /* escape the sidebar's stacking context entirely */
  left: 68px;
  bottom: 1rem;
  background: var(--sidebar-bg);
  border: 1px solid var(--sidebar-border);
  border-radius: var(--radius);
  padding: .6rem .5rem;
  min-width: 11rem;
  box-shadow: var(--shadow-lg);
  z-index: 2000;            /* above everything including modals' backdrops */
  white-space: nowrap;
}
/* Left-pointing caret — vertical position is set inline by the same JS,
   aligned to the avatar's vertical center regardless of popup height. */
.sidebar-avatar-popup::after {
  content: '';
  position: absolute;
  left: -.45rem;
  top: var(--avatar-caret-top, 1.1rem);
  border-top: .45rem solid transparent;
  border-bottom: .45rem solid transparent;
  border-right: .45rem solid var(--sidebar-border);
}
.sidebar-user-avatar-only:focus-within .sidebar-avatar-popup {
  display: block;
}
/* Hover visibility is JS-managed (mgmt.html), not pure :hover — the popup
   sits `position:fixed` a few pixels away from the avatar it's anchored to,
   and pure CSS :hover drops the instant the cursor crosses that gap, closing
   the popup before it can be reached. JS keeps it open across the gap with
   a short grace-period timer instead. .is-open is the JS-driven equivalent
   of what :hover used to do here. */
.sidebar-avatar-popup.is-open {
  display: block;
}
.sidebar-avatar-popup .sidebar-link {
  /* This popup lives INSIDE the collapsed sidebar, so the rail's row
     geometry would otherwise apply to its menu items too. */
  min-height: 0 !important;
  border-left-width: 2px !important;
  justify-content: flex-start !important;
  padding: .3rem .6rem !important;
  margin-bottom: .15rem !important;
  gap: .5rem !important;
  border-radius: .4rem !important;
  font-size: .78rem;
}
.sidebar-avatar-popup-name {
  font-size: .75rem; font-weight: 600; color: var(--sidebar-text);
  padding: .2rem .6rem .45rem; border-bottom: 1px solid var(--sidebar-border);
  margin-bottom: .25rem; overflow: hidden; text-overflow: ellipsis;
}
.hrms-sidebar.sidebar-collapsed .sidebar-toggle-open { display: none !important; }
.hrms-sidebar .sidebar-toggle-close { display: none !important; }
.hrms-sidebar.sidebar-collapsed .sidebar-toggle-close { display: inline-flex !important; }
/* Toggle button — always visible on desktop (≥992px) */
/* Collapse/expand toggle — upper-right corner of sidebar, always visible on desktop */
.sidebar-toggle-btn {
  display: flex;
  align-items: center; justify-content: flex-end;
  width: 100%;
  background: transparent; border: none; box-shadow: none;
  appearance: none; -webkit-appearance: none; outline: none; cursor: pointer;
  color: var(--sidebar-section-label);
  padding: .1rem .1rem .6rem .1rem; margin-bottom: .35rem;
  border-bottom: 1px solid var(--sidebar-border);
  transition: color var(--motion-fast), opacity var(--motion-fast);
}
/* The actual chevron icon sits in a small pill so it's visually distinct */
.sidebar-toggle-btn svg {
  padding: .2rem;
  border-radius: .375rem;
  background: var(--sidebar-toggle-hover-bg, rgba(255,255,255,.05));
  border: 1px solid rgba(255,255,255,.08);
  transition: background var(--motion-fast), border-color var(--motion-fast), transform var(--motion-fast);
}
.sidebar-toggle-btn:hover { color: var(--sidebar-text-hover); }
.sidebar-toggle-btn:hover svg {
  background: rgba(255,255,255,.1);
  border-color: rgba(255,255,255,.18);
}
.sidebar-toggle-btn:active svg { transform: scale(.9); }
.sidebar-toggle-btn:focus, .sidebar-toggle-btn:active { background: transparent; outline: none; box-shadow: none; }
.sidebar-toggle-btn:focus-visible {
  box-shadow: 0 0 0 2px var(--sidebar-bg), 0 0 0 4px var(--sidebar-active-border);
  border-radius: .375rem;
}
/* When collapsed: center the icon over the rail and keep the rule under it
   the same distance from the first row as it is when expanded. */
.hrms-sidebar.sidebar-collapsed .sidebar-toggle-btn {
  justify-content: center;
  padding: .1rem 0 .55rem;
  margin-bottom: .5rem;
}
/* Hide on mobile only — below 768px the sidebar is a drawer with its own
   hamburger and close button, so a collapse control there is meaningless.
   This used to cut off at 991.98px, which left the 768–991px band (small
   laptops, iPad landscape) with the sidebar still rendered as a COLUMN and
   no way to un-collapse it: the state survives in localStorage, so anyone
   who collapsed the rail on a wide screen and came back on a narrower one
   was stuck with icons and no control to restore the labels. */
@media (max-width: 767.98px) {
  .sidebar-toggle-btn { display: none !important; }
}

/* ── Collapsible sidebar group headers ───────────────────────────────── */
.sidebar-section-toggle {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%;
  background: transparent; background-color: transparent;
  border: none; box-shadow: none;
  appearance: none; -webkit-appearance: none;
  cursor: pointer;
  padding: .4rem .75rem;
  margin-bottom: .375rem;
  margin-top: 1.25rem;
  border-radius: var(--radius-sm);
  outline: none;
  overflow: hidden;
  transition: background-color var(--motion-fast) ease, transform var(--motion-fast) ease,
              justify-content var(--motion-med) ease, padding var(--motion-med) ease;
}
.sidebar-section-toggle:first-of-type { margin-top: 0; }
.sidebar-section-toggle:focus,
.sidebar-section-toggle:active { background: transparent; outline: none; box-shadow: none; }
.sidebar-section-toggle:focus-visible {
  box-shadow: 0 0 0 2px var(--sidebar-bg), 0 0 0 4px var(--sidebar-active-border);
}
.sidebar-section-icon {
  flex-shrink: 0; opacity: .7; color: var(--sidebar-section-label);
  transition: color var(--motion-fast), opacity var(--motion-fast);
}
.sidebar-section-toggle:hover .sidebar-section-icon { opacity: 1; color: var(--sidebar-text); }
.sidebar-section-toggle[aria-expanded="true"] .sidebar-section-icon { color: var(--sidebar-section-label-hover); opacity: .9; }
.sidebar-section-label {
  font-size: .65rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--sidebar-section-label);
  flex: 1; text-align: left;
  max-width: 999px; opacity: 1; overflow: hidden; white-space: nowrap;
  transition: color var(--motion-fast), max-width var(--motion-med) ease,
              opacity var(--motion-fast) ease, margin var(--motion-med) ease;
}
.sidebar-section-chevron {
  color: var(--sidebar-section-label); flex-shrink: 0; opacity: 1; max-width: 1rem;
  transition: transform var(--motion-fast) ease, color var(--motion-fast),
              opacity var(--motion-fast) ease, max-width var(--motion-med) ease;
}
.sidebar-section-toggle.collapsed .sidebar-section-chevron { transform: rotate(-90deg); }
.sidebar-section-body { display: block; }
/* Expanded group stays transparent (matches the sidebar's own background —
   no colored "box" around the links) and is marked instead by a slim accent
   rule down the left edge, a common modern-dashboard convention (VS Code's
   tree view, GitHub's side nav) that reads as "open" without adding visual
   weight or clashing with link hover/active states inside it.

   The accent border/indent is applied to BOTH .collapsing (Bootstrap's
   in-progress height-transition state) and .show (the final state) —
   applying it only to .show made the indent pop in abruptly the instant
   Bootstrap's own height animation finished, on top of which a separate
   fade+slide animation re-triggered every time .show was added, so you'd
   see the box grow, THEN its content visibly snap into an indent and
   fade in afterward — two uncoordinated motions instead of one smooth
   expand. Bootstrap's height transition is now the only animation. */
.sidebar-section-body.collapsing,
.sidebar-section-body.show {
  background: transparent;
  border-left: 2px solid var(--sidebar-group-accent);
  padding-left: .5rem;
  margin-left: .25rem;
}
.sidebar-section-toggle[aria-expanded="true"] .sidebar-section-label { color: var(--sidebar-section-label-hover); }
.sidebar-section-toggle:hover { background-color: var(--sidebar-toggle-hover-bg); transform: translateX(1px); }
.sidebar-section-toggle:hover .sidebar-section-label { color: var(--sidebar-section-label-hover); }
.sidebar-section-toggle:hover .sidebar-section-chevron { color: var(--sidebar-text); }
.sidebar-section-toggle:active { transform: translateX(1px) scale(.98); }
/* ── Collapsed = icon rail (desktop only) ────────────────────────────────
   Scoped to ≥768px on purpose: below that the sidebar is an off-canvas
   drawer that always shows itself in full, collapse preference or not (see
   the mobile block at the end of this section), so none of the hiding or
   centring belongs there.

   Every row here is one box — `--rail-item-h` tall, full rail width, icon
   centred — because at 64px wide the only thing carrying structure is the
   position of the icons. Anything that nudges one icon off the centre line,
   or makes one row taller than its neighbour, is immediately visible as
   damage. Both used to happen; the comments below name which was which. */
@media (min-width: 768px) {
  /* THE cause of the ragged spacing. Hiding a label with `max-width: 0`
     zeroes its WIDTH but not its CONTENT: a multi-word label ("Kiosk setup
     presets & remote enrolment") re-wrapped to one word per line inside that
     zero-width box, and the flex row grew to fit it. Measured on the HR
     sidebar before this: rows came out 25.8 / 44.6 / 63.4 / 82.2px tall —
     one step per line of text nobody could see. `nowrap` keeps the hidden
     label on a single line, where it is always shorter than the icon. */
  .hrms-sidebar.sidebar-collapsed .sidebar-link-text {
    max-width: 0; opacity: 0; margin: 0; white-space: nowrap;
  }
  .hrms-sidebar.sidebar-collapsed .sidebar-section-label {
    max-width: 0; opacity: 0; margin: 0; overflow: hidden; white-space: nowrap;
    transition: max-width var(--motion-med) ease, opacity var(--motion-fast) ease, margin var(--motion-med) ease;
  }

  .hrms-sidebar.sidebar-collapsed .sidebar-link {
    justify-content: center;
    gap: 0;
    padding: 0;
    min-height: var(--rail-item-h);
    margin: 0 0 var(--rail-gap);
    border-radius: .55rem;
    /* The 2px transparent left border that reserves room for the active
       accent when expanded pushed every icon 1px off the rail's centre line
       (33px against the group icons' 32px), so it goes… */
    border-left-width: 0;
  }
  /* …and the accent comes back as a painted bar instead. A pseudo-element
     rather than an inset box-shadow because several presets style
     `.sidebar-link.active`'s box-shadow as their whole active idiom
     (neumorphism sinks the item into the surface); overriding that here
     would win on specificity and quietly flatten the preset — but only in
     the collapsed state, which is the hardest kind of difference to spot. */
  .hrms-sidebar.sidebar-collapsed .sidebar-link.active { position: relative; }
  .hrms-sidebar.sidebar-collapsed .sidebar-link.active::before {
    content: ''; position: absolute; left: 0; top: 50%;
    width: 2px; height: 60%; transform: translateY(-50%);
    border-radius: 0 2px 2px 0;
    background: var(--sidebar-active-border);
  }
  /* A centred icon that slides sideways on hover reads as a wobble, not as
     feedback — the background pill is the feedback in this state. Two
     selectors because two rules push it: the base `.sidebar-link:hover`
     (2px), and the "Full" motion preset's
     `html[data-portal-motion="full"] .sidebar-link:hover:not(.active)` (3px),
     which is (0,4,1) and sits ~4,500 lines further down — a plain
     `.hrms-sidebar.sidebar-collapsed .sidebar-link:hover` is (0,4,0) and
     loses to it, so the icons kept drifting off the rail's centre line. */
  .hrms-sidebar.sidebar-collapsed .sidebar-link:hover,
  html .hrms-sidebar.sidebar-collapsed .sidebar-link:hover:not(.active) { transform: none; }
  .hrms-sidebar.sidebar-collapsed .sidebar-link svg { opacity: .85; }

  /* Group headers shrink to a centred icon (their text label hides, same
     pattern as .sidebar-link above) instead of disappearing entirely, so
     each section still has a visible, hoverable identifier — the chevron and
     the click-to-collapse affordance go, since every group is also forced
     open below and toggling would have no visible effect.
     The hairline above each one is what replaces the text label as the
     "a new section starts here" signal; without it the rail is one
     undifferentiated stack of 60-odd icons. */
  .hrms-sidebar.sidebar-collapsed .sidebar-section-toggle {
    display: flex; justify-content: center; cursor: default;
    min-height: 0;
    padding: .5rem 0 .15rem;
    margin: .45rem 0 var(--rail-gap);
    border-radius: 0;
    border-top: 1px solid var(--sidebar-border);
  }
  .hrms-sidebar.sidebar-collapsed .sidebar-section-toggle:first-of-type {
    margin-top: 0; padding-top: 0; border-top: 0;
  }
  .hrms-sidebar.sidebar-collapsed .sidebar-section-icon { display: flex; opacity: .85; }
  .hrms-sidebar.sidebar-collapsed .sidebar-section-chevron { opacity: 0; max-width: 0; }
  /* No hover background: in this state the header does nothing when clicked,
     and a pill that lights up under the cursor promises otherwise. The icon
     brightening (inherited from the base rule) plus its tooltip is the whole
     affordance. */
  .hrms-sidebar.sidebar-collapsed .sidebar-section-toggle:hover {
    background-color: transparent; transform: none;
  }
  .hrms-sidebar.sidebar-collapsed .sidebar-section-body {
    display: block !important; height: auto !important; visibility: visible !important;
  }
  /* The open-group accent (2px rule + 12px of indent) means something beside
     a column of text labels and nothing beside a column of 16px icons — all
     it did here was draw a stray vertical line down the rail and push every
     icon in an OPEN group 7px right of every icon in a closed one. */
  .hrms-sidebar.sidebar-collapsed .sidebar-section-body.show,
  .hrms-sidebar.sidebar-collapsed .sidebar-section-body.collapsing {
    border-left-width: 0; padding-left: 0; margin-left: 0;
  }
}
.sidebar-link {
  display: flex; align-items: center; gap: .625rem;
  padding: .6rem .75rem;
  border-radius: var(--radius-sm);
  font-size: var(--sidebar-font-size); font-weight: 500;
  color: var(--sidebar-text);
  border-left: 2px solid transparent;
  outline: none;
  overflow: hidden;
  transition: background-color var(--motion-fast) ease, color var(--motion-fast) ease,
              border-color var(--motion-fast) ease, transform var(--motion-fast) ease,
              justify-content var(--motion-med) ease, padding var(--motion-med) ease, gap var(--motion-med) ease;
  margin-bottom: .15rem;
}
.sidebar-link:hover {
  background: var(--sidebar-hover-bg);
  color: var(--sidebar-text-hover);
  transform: translateX(2px);
}
.sidebar-link:focus-visible {
  box-shadow: 0 0 0 2px var(--sidebar-bg), 0 0 0 4px var(--sidebar-active-border);
}
.sidebar-link.active {
  background: var(--sidebar-active-bg);
  color: var(--sidebar-text-active);
  border-left-color: var(--sidebar-active-border);
  font-weight: 600;
  box-shadow: var(--shadow-sm), inset 0 0 0 1px rgb(255 255 255 / .04);
}
.sidebar-link svg { flex-shrink: 0; opacity: .75; transition: opacity var(--motion-fast), transform var(--motion-fast); }
.sidebar-link:hover svg { opacity: .95; }
.sidebar-link.active svg { opacity: 1; color: var(--sidebar-icon-active); }

/* ── Quick find ── */
.sidebar-quickfind {
  display: flex; align-items: center; gap: .45rem;
  margin-bottom: 1rem; padding: .45rem .65rem;
  border-radius: .55rem;
  background: rgb(255 255 255 / .04);
  border: 1px solid var(--sidebar-border);
  color: var(--sidebar-section-label);
  max-height: 3rem; opacity: 1; overflow: hidden;
  transition: border-color var(--motion-fast), background-color var(--motion-fast),
              max-height var(--motion-med) ease, opacity var(--motion-fast) ease,
              margin var(--motion-med) ease, padding var(--motion-med) ease;
}
.sidebar-quickfind:has(input:focus) { border-color: var(--sidebar-active-border); background: rgb(255 255 255 / .06); }
.sidebar-quickfind svg { flex-shrink: 0; opacity: .7; }
.sidebar-quickfind input {
  flex: 1; min-width: 0; background: none; border: none; outline: none;
  color: var(--sidebar-text-hover); font-size: .8125rem; font-family: inherit;
}
.sidebar-quickfind input::placeholder { color: var(--sidebar-section-label); }
.sidebar-quickfind-clear {
  display: flex; align-items: center; justify-content: center;
  width: 1.1rem; height: 1.1rem; flex-shrink: 0;
  background: none; border: none; outline: none; cursor: pointer;
  color: var(--sidebar-section-label); border-radius: 50%;
  transition: color var(--motion-fast), background-color var(--motion-fast);
}
.sidebar-quickfind-clear:hover { color: var(--sidebar-text-hover); background: rgb(255 255 255 / .1); }
.sidebar-link-hidden { display: none; }
.hrms-sidebar.sidebar-collapsed .sidebar-quickfind {
  max-height: 0; opacity: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; border-width: 0;
}

/* ── Expand all / collapse all ── */
.sidebar-bulk-toggle {
  display: flex; gap: .4rem; margin-bottom: .65rem;
  max-height: 2rem; opacity: 1; overflow: hidden;
  transition: max-height var(--motion-med) ease, opacity var(--motion-fast) ease, margin var(--motion-med) ease;
}
.sidebar-bulk-toggle-btn {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: .35rem;
  padding: .35rem .5rem; border-radius: var(--radius-sm);
  background: rgb(255 255 255 / .04); border: 1px solid var(--sidebar-border);
  color: var(--sidebar-section-label); font-size: .72rem; font-weight: 500;
  cursor: pointer; white-space: nowrap;
  transition: border-color var(--motion-fast), background-color var(--motion-fast), color var(--motion-fast);
}
.sidebar-bulk-toggle-btn svg { flex-shrink: 0; }
.sidebar-bulk-toggle-btn:hover {
  border-color: var(--sidebar-active-border); background: rgb(255 255 255 / .08); color: var(--sidebar-text-hover);
}
.hrms-sidebar.sidebar-collapsed .sidebar-bulk-toggle {
  max-height: 0; opacity: 0; margin-bottom: 0;
}

/* ── Thin, themed scrollbar for the nav area ── */
/* Both navs, not just the HR one: a default 15px scrollbar eats a quarter of
   the 64px icon rail and shifts every icon off its centre line, and the
   employee portal's nav (#portal-sidebar-nav) was never on this list. */
#sidebar-nav,
#portal-sidebar-nav { scrollbar-width: thin; scrollbar-color: rgb(255 255 255 / .15) transparent; }
#sidebar-nav::-webkit-scrollbar,
#portal-sidebar-nav::-webkit-scrollbar { width: 5px; }
#sidebar-nav::-webkit-scrollbar-track,
#portal-sidebar-nav::-webkit-scrollbar-track { background: transparent; }
#sidebar-nav::-webkit-scrollbar-thumb,
#portal-sidebar-nav::-webkit-scrollbar-thumb { background: rgb(255 255 255 / .15); border-radius: 999px; }
#sidebar-nav::-webkit-scrollbar-thumb:hover,
#portal-sidebar-nav::-webkit-scrollbar-thumb:hover { background: rgb(255 255 255 / .25); }

/* ── Polished user footer card ── */
.sidebar-user-info > div {
  background: rgb(255 255 255 / .035);
  border: 1px solid var(--sidebar-border) !important;
  border-radius: var(--radius);
  padding: .75rem !important;
}

/* ── Mobile sidebar (< 768 px) ────────────────────────────────────────────── */
@media (max-width: 767.98px) {
  .hrms-sidebar {
    position: fixed !important;
    top: var(--nav-offset);
    left: 0;
    height: calc(100vh - var(--nav-offset));
    min-height: 0;
    z-index: 1045;
    transform: translateX(-100%);
    transition: transform .3s cubic-bezier(.4, 0, .2, 1);
    overflow-y: auto;
    box-shadow: none;
    width: 260px !important;
    min-width: 260px !important;
    padding: 1.25rem 1rem !important;
  }
  .hrms-sidebar.sidebar-mobile-open {
    transform: translateX(0);
    box-shadow: 4px 0 32px rgba(0, 0, 0, .45);
  }
  /* Mirror the off-canvas slide-in edge and its shadow when the sidebar is
     pinned to the right — otherwise it would still fly in from the left
     and land on the right, a jarring direction mismatch. */
  html[data-sidebar-position="right"] .hrms-sidebar {
    left: auto; right: 0;
    transform: translateX(100%);
  }
  html[data-sidebar-position="right"] .hrms-sidebar.sidebar-mobile-open {
    transform: translateX(0);
    box-shadow: -4px 0 32px rgba(0, 0, 0, .45);
  }
  /* On mobile always show full sidebar, ignore collapse state */
  .hrms-sidebar.sidebar-collapsed {
    width: 260px !important;
    min-width: 260px !important;
    padding: 1.25rem 1rem !important;
  }
  .hrms-sidebar.sidebar-collapsed .sidebar-section-label { display: block !important; }
  .hrms-sidebar.sidebar-collapsed .sidebar-section-toggle { display: flex !important; }
  .hrms-sidebar.sidebar-collapsed .sidebar-link-text { display: inline !important; }
  .hrms-sidebar.sidebar-collapsed .sidebar-link {
    justify-content: flex-start !important;
    gap: .625rem !important;
    padding: .6rem .75rem !important;
  }
  .hrms-sidebar.sidebar-collapsed .sidebar-user-info { display: block !important; }
  .hrms-sidebar.sidebar-collapsed .sidebar-user-avatar-only { display: none !important; }
  /* Hide the desktop collapse toggle on mobile */
  .hrms-sidebar .sidebar-toggle-btn { display: none !important; }
}
/* Backdrop overlay */
.sidebar-backdrop {
  display: none;
  position: fixed;
  /* Was a hardcoded `72px`, and the topbar above it measures 58. The result is
     a 14px band across the full width of the screen, directly under the
     header, that stays UNDIMMED and stays clickable while the drawer is open
     and `aria-modal` -- visible in ui_audit/shots/mobile-nav/signedin-*.png as
     a bright strip beside the panel.

     The `--portal-topbar-h` token exists precisely to stop this: see the
     "Header heights" block near the top of this file, whose own comment says
     these were "ten separate hardcoded values". Two of them survived the
     consolidation -- this one and `.portal-backdrop` below. */
  inset: var(--portal-topbar-h) 0 0 0;
  background: rgba(0, 0, 0, .5);
  z-index: 1044;
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}
.sidebar-backdrop.show { display: block; }

/* ── My Portal sidebar (layouts/portal.html) ──────────────────────────────
   Reuses .hrms-sidebar's look (same collapse/mobile behavior as the HR admin
   shell) but sits under base.html's public marketing nav instead of
   mgmt.html's .portal-topbar, which is a different height (85px vs 72px --
   85 = .hrms-nav's 76px container min-height + 8px of its own top/bottom
   padding + 1px border-bottom) — these overrides correct the sticky
   offset / min-height / mobile inset math that .hrms-sidebar hardcodes
   against the 72px admin topbar. */
.portal-sidebar {
  top: var(--portal-topbar-h);
  min-height: calc(100vh - var(--portal-topbar-h));
}
/* `85px` dated from when this layout sat under base.html's public marketing
   nav. It does not: layouts/portal.html renders its own `.portal-topbar`, the
   same 58px bar mgmt.html uses, so the override is both stale and redundant.
   Kept as an explicit statement rather than deleted, because the selector is
   the only place a reader looks to answer "does My Portal differ here". */
.portal-backdrop { inset: var(--portal-topbar-h) 0 0 0; }
@media (max-width: 767.98px) {
  .portal-sidebar {
    top: var(--portal-topbar-h);
    height: calc(100vh - var(--portal-topbar-h));
  }
}

/* Mobile portal header bar (hamburger row, hidden on desktop via d-none d-md-none Bootstrap classes) */
.portal-mobile-header {
  align-items: center;
  gap: .75rem;
  padding: .25rem 0 .875rem;
  margin-bottom: .25rem;
  border-bottom: 1px solid var(--border, rgba(148, 163, 184, .15));
}
.portal-mobile-menu-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-sm);
  background: rgba(99, 102, 241, .1);
  border: 1px solid rgba(99, 102, 241, .2);
  color: var(--primary, #6366f1);
  cursor: pointer;
  transition: background .15s, border-color .15s;
  flex-shrink: 0;
  padding: 0;
}
.portal-mobile-menu-btn:hover,
.portal-mobile-menu-btn:focus-visible { background: rgba(99, 102, 241, .2); outline: none; }

/* ── Tables ── */
.hrms-table { width: 100%; border-collapse: separate; border-spacing: 0; }
.hrms-table thead th {
  background: var(--bg);
  font-size: .75rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
  padding: .75rem 1rem;
  border-bottom: 2px solid var(--border);
}
.hrms-table tbody tr { transition: background .1s; }
.hrms-table tbody tr:hover { background: var(--primary-light); }
.hrms-table tbody td {
  padding: .875rem 1rem;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

/* ── Avatar ── */
.avatar {
  width: 2.25rem; height: 2.25rem;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: .8rem; font-weight: 700;
  flex-shrink: 0;
}
.avatar-lg { width: 3.5rem; height: 3.5rem; font-size: 1.1rem; }
.avatar-xl { width: 5rem; height: 5rem; font-size: 1.5rem; }
.avatar-primary { background: var(--primary-light); color: var(--primary); }
.avatar-success { background: #d1fae5; color: var(--success); }
.avatar-warning { background: #fef3c7; color: var(--warning); }
.avatar-danger  { background: #fee2e2; color: var(--danger); }

/* ── Badges ── */
.badge-dot {
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: .75rem; font-weight: 600;
  padding: .3rem .7rem;
  border-radius: 999px;
}
.badge-dot::before { content: ''; width: .5rem; height: .5rem; border-radius: 50%; background: currentColor; }
.badge-present  { background: #d1fae5; color: #065f46; }
.badge-late     { background: #fef3c7; color: #92400e; }
.badge-absent   { background: #fee2e2; color: #991b1b; }
.badge-leave    { background: #dbeafe; color: #1e40af; }
.badge-pending  { background: #fef9c3; color: #854d0e; }
.badge-approved { background: #d1fae5; color: #065f46; }
.badge-rejected { background: #fee2e2; color: #991b1b; }
/* Sits between .badge-pending (warning) and .badge-rejected (critical) on
   the audit trail's severity badge -- orange reads as more urgent than the
   yellow warning tone without reusing critical's red, so the three remain
   visually distinct at a glance. */
.badge-error    { background: #ffedd5; color: #9a3412; }
/* Same tint-of-itself trap as .accordion-button above: --primary on
   --primary-light measured 4.10:1. .badge-employee pairs --muted with --bg,
   which is the muted token's dimmest legitimate ground. */
.badge-hr       { background: var(--primary-light); color: var(--primary-text); }
.badge-admin    { background: #ede9fe; color: #5b21b6; }
.badge-employee { background: var(--bg); color: var(--text); }

/* ── Buttons ──
   .btn-primary/.btn-outline-primary already read --primary — the two
   below (secondary/outline-secondary/light) previously fell through to
   Bootstrap's own compiled gray with NO base override at all (only a
   dark-mode-only override existed), so on every OTHER preset they looked
   identical regardless of which theme was active. Tokenized so they pick
   up each preset's own surface/border/text identity too. */
.btn-primary { background: var(--primary); border-color: var(--primary); font-weight: 600; color: var(--on-primary); }
.btn-primary:hover { background: var(--primary-hover, var(--primary-dark)); border-color: var(--primary-hover, var(--primary-dark)); color: var(--on-primary); }
/* The LABEL takes --primary-text; the BORDER keeps --primary. The two tokens
   answer different questions -- --primary is the fill colour, --primary-text is
   the same hue solved to carry text -- and an outline button has no fill, so
   its label is simply text sitting on the page.

   Measured on /about/our-products-and-services/ under Flat: --primary
   (#12856E) on the page canvas --bg (#ECF0F1) is 3.97:1, under the 4.5 a 15px
   label needs; --primary-text (#0E6E5B) clears it. The border is non-text and
   answers to 1.4.11's 3:1, which --primary already meets.

   Worth noting for the palette owner: preset_audit.py reports 0 failures
   across 588 checks because it pairs the *-text tokens against --surface
   (#FFFFFF). This button sits on --bg, which is darker, and that is the gap --
   the same "check the real ground" trap recorded elsewhere in this file. */
.btn-outline-primary { color: var(--primary-text, var(--primary)); border-color: var(--primary); font-weight: 600; }
.btn-outline-primary:hover { background: var(--primary); border-color: var(--primary); color: var(--on-primary); }
.btn-secondary, .btn-outline-secondary {
  color: var(--text); border-color: var(--border); background: var(--surface);
}
.btn-secondary:hover, .btn-outline-secondary:hover {
  background: var(--bg); border-color: var(--muted); color: var(--text);
}
.btn-light { background: var(--bg); border-color: var(--border); color: var(--text); }
.btn-light:hover { background: var(--border); }

/* ── Dropdown menus — Bootstrap's own component vars (--bs-dropdown-*)
   aren't part of the preset mirror set, so this always rendered as stock
   white-bg/dark-text regardless of preset (fine on a pure-white surface,
   visibly mismatched on any colour-tinted one). Reads the same tokens as
   .card so a dropdown always looks like it belongs to the page it's on. */
/* Driven through Bootstrap's own --bs-dropdown-* variables rather than a rule
   per element, so every component built on .dropdown-menu inherits the
   treatment instead of each one being restyled separately. Declared on the
   preset element as well as :root — same substitution rule as the radius
   scale, see the derived block near the top of this file. */
:root,
body:is(.portal-page,.public-cms-page,.themed-page),
body.auth-page {
  --bs-dropdown-bg:                  var(--surface);
  --bs-dropdown-border-color:        var(--border);
  --bs-dropdown-border-width:        var(--border-width);
  --bs-dropdown-border-radius:       var(--radius-sm);
  --bs-dropdown-inner-border-radius: var(--radius-xs);
  --bs-dropdown-color:               var(--text);
  --bs-dropdown-link-color:          var(--text);
  --bs-dropdown-link-hover-color:    var(--text);
  --bs-dropdown-link-hover-bg:       var(--bg);
  --bs-dropdown-link-active-color:   var(--on-primary, #fff);
  --bs-dropdown-link-active-bg:      var(--primary);
  --bs-dropdown-header-color:        var(--muted);
  --bs-dropdown-divider-bg:          var(--border);
  --bs-dropdown-padding-y:           .35rem;
  --bs-dropdown-padding-x:           .35rem;
  --bs-dropdown-item-padding-y:      .45rem;
  --bs-dropdown-item-padding-x:      .7rem;
  --bs-dropdown-spacer:              .35rem;
}

.dropdown-menu {
  background: var(--surface);
  color: var(--text);
  /* A floating surface needs an EDGE, not only elevation. Under Flat
     --shadow is `none`, so a shadow-only dropdown has no boundary against the
     page at all — the same defect the public nav's hover menu had. */
  border: var(--border-width) solid var(--border);
  box-shadow: var(--shadow-lg);
  min-width: 11rem;
  line-height: 1.45;
}

.dropdown-item {
  color: var(--text);
  border-radius: var(--radius-xs);
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: .55rem;
  transition: background-color .12s ease, color .12s ease;
}
/* Leading icons sit quiet until the row is the one you mean. */
.dropdown-item > svg,
.dropdown-item > .bi { flex: none; opacity: .75; }
.dropdown-item:hover > svg,
.dropdown-item:focus > svg,
.dropdown-item.active > svg { opacity: 1; }

.dropdown-item:hover, .dropdown-item:focus { background: var(--bg); color: var(--text); }
.dropdown-item.active, .dropdown-item:active {
  background: var(--primary); color: var(--on-primary, #fff);
}
/* Bootstrap's default focus styling is the same background as :hover, which
   tells a keyboard user nothing about where they are once the pointer has
   also been somewhere. */
.dropdown-item:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--primary);
}
.dropdown-item:disabled, .dropdown-item.disabled { color: var(--muted); opacity: .65; }

/* A section label, not another row: quieter, smaller, and spaced so the eye
   reads it as a heading rather than a disabled item. */
.dropdown-header {
  font-size: var(--fs-3xs, .68rem);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--muted);
  padding: .55rem .7rem .3rem;
}

.dropdown-divider {
  border-color: var(--border);
  margin: .3rem .35rem;
  opacity: 1;
}

/* ── Forms ── */
.form-control, .form-select {
  border-color: var(--border);
  border-radius: var(--radius-sm);
  padding: .625rem .875rem;
  font-size: .9375rem;
  transition: border-color .15s, box-shadow .15s;
}
.form-control:focus, .form-select:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(79,70,229,.12);
}
.form-label { font-weight: 600; font-size: .875rem; color: var(--text); margin-bottom: .375rem; }

/* Suppress Edge/IE's built-in password-reveal ("eye") and clear-field
   icons on every password input site-wide (login, register, change/reset
   password). Without this, Edge renders its own native reveal icon on top
   of/right next to our custom eye-toggle button once the field has text —
   the "two eye icons" bug. Chrome/Firefox never had this icon; this rule
   is a no-op there. */
input[type="password"]::-ms-reveal,
input[type="password"]::-ms-clear {
  display: none;
  width: 0;
  height: 0;
}

/* ── Login page ── */
.login-wrapper {
  min-height: calc(100vh - var(--portal-topbar-h));
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1rem;
}
.login-card {
  background: var(--surface);
  border: var(--border-width, 1px) solid var(--border);
  border-radius: var(--radius, 1rem);
  /* Colored halo layered under the neutral elevation shadow — built from
     --primary, so it automatically shifts hue per Appearance preset
     (indigo/violet/navy/win7-blue/etc.) instead of a fixed gray card
     shadow every preset would otherwise share. */
  box-shadow:
    var(--shadow-lg),
    0 0 0 1px color-mix(in srgb, var(--primary) 14%, transparent),
    0 22px 60px -14px color-mix(in srgb, var(--primary) 45%, transparent);
  padding: 2.5rem;
  width: 100%;
  max-width: 420px;
  transition: box-shadow .3s var(--motion-easing, ease);
}
.login-card:focus-within {
  box-shadow:
    var(--shadow-lg),
    0 0 0 1px color-mix(in srgb, var(--primary) 28%, transparent),
    0 26px 70px -14px color-mix(in srgb, var(--primary) 55%, transparent);
}

/* ── Footer ── */
/* One themed footer for the whole product, public site included.
   It used to be two: a fixed dark-navy marketing footer (#1e293b + fixed
   #94a3b8/#cbd5e1 text) and a `body.portal-page` copy re-pointed at the
   sidebar palette so it would follow the preset. That split is what made a
   themed portal sit under an unthemed public footer.

   The sidebar palette is the right source for both: it is the product's
   "frame" colour, every preset states it explicitly, and every value in it
   already clears AA against its own --sidebar-bg (several presets make that
   background LIGHT -- winxp is #F4F2E8 -- which is why the text tokens must
   come from the same palette as the background rather than being fixed
   light-on-dark values; at #F4F2E8 the old fixed #94a3b8 measured 1.10:1 and
   the "Platform / Support / Legal & Privacy" headings were invisible).

   The fallbacks are the previous hardcoded values, and :root's --sidebar-bg
   is #1e293b, so a page with no preset active renders byte-identically to
   before. They are tokens at all because base.html once inlined #64748b and
   #475569 here -- 3.07:1 and 1.93:1, the latter effectively invisible, on
   every page of the site. Found by the real-page axe scan in
   apps/public/tests/test_page_visual.py. */
.hrms-footer {
  --footer-text:        var(--sidebar-text, #94a3b8);
  --footer-text-strong: var(--sidebar-text-hover, #cbd5e1);
  background: var(--sidebar-bg, #1e293b);
  color: var(--footer-text);
  padding: 4rem 0 2.5rem;
  margin-top: auto;
}
.hrms-footer a { color: var(--footer-text); transition: color .15s; }
.hrms-footer a:hover { color: var(--footer-text-strong); }
/* WCAG 2.2 target size (2.5.8) wants 24x24 CSS px. These sit in `.small` lists
   and measured 17px tall on every one of the 91 public pages -- the single
   most repeated defect on the site, and the easiest to miss because nothing
   about it LOOKS wrong. Padding rather than a bare min-height: an inline
   anchor ignores height, so the box has to be grown by making it a block-level
   flex line, which also keeps the hit area the full width of the label.
   The list keeps its visual rhythm because the padding replaces `.mb-1`'s
   gap rather than adding to it. */
.hrms-footer ul li > a,
.hrms-footer ul li > button {
  display: flex;
  align-items: center;
  min-height: 24px;
  padding-block: .1875rem;
}
.hrms-footer ul li.mb-1 { margin-bottom: 0 !important; }
.hrms-footer .footer-brand {
  font-weight: 800; font-size: 1.05rem; letter-spacing: -.02em;
  color: var(--sidebar-text-hover, #fff); margin-bottom: .65rem;
}
.hrms-footer .footer-divider { border-color: var(--sidebar-border, #334155); }

/* Win7 preset — the footer picks up the exact same Aero-glass gradient as
   the sidebar (site.css ~line 460, html[data-portal-preset="win7"]
   .hrms-sidebar) instead of just a flat colour, so it reads as one
   continuous "frame" the way Explorer's own chrome does. */
html[data-portal-preset="win7"] body:is(.portal-page,.public-cms-page,.themed-page) .hrms-footer {
  background: linear-gradient(180deg, #1F3E72 0%, #162E5A 55%, #1A3466 100%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
}

/* ── Cookie consent banner ──────────────────────────────────────────────
   A floating card (not an edge-to-edge bar) — the modern convention: it
   reads as a deliberate prompt rather than a strip stealing viewport height
   from every page while it's up. Full-width on mobile, where a floating
   card would eat too much side margin to be worth it. */
.hrms-cookie-banner {
  position: fixed;
  left: 1.25rem; bottom: 1.25rem;
  /* Was 10500 -- above EVERY overlay layer Bootstrap defines (offcanvas 1045,
     modal 1055, popover 1070) and above this file's own toast stack at 1090.
     On a phone the banner is pinned across the full width of the bottom edge,
     so it painted over the drawer's "Sign in", over the footer buttons of any
     modal opened before consent, and over toasts. It also stayed on top of
     the offcanvas backdrop while being un-focusable behind an `aria-modal`
     drawer -- visible, and unreachable.

     1029 is deliberate -- one below the sticky header at 1030, not one above
     the offcanvas backdrop at 1040. Bootstrap appends that backdrop to the
     offcanvas's own PARENT (`rootElement: this._element.parentNode`), so for
     a drawer living inside `.hrms-nav` both the backdrop AND the drawer are
     trapped in the header's z-index-1030 stacking context; nothing the banner
     can be set to between 1030 and 1040 gets underneath them. Sitting below
     the header costs the banner nothing, because it is pinned to the BOTTOM
     edge and the header is at the top -- they never overlap. Everything a
     visitor deliberately opens (drawer 1030-context, modal 1055, toasts 1090)
     now wins, which is the right order: a consent notice is persistent and
     does not need to outrank the dialog somebody just asked for. */
  z-index: 1029;
  width: min(420px, calc(100vw - 2.5rem));
  background: #0f172a;
  color: #e2e8f0;
  border: 1px solid rgba(255,255,255,.08);
  border-radius: 1rem;
  box-shadow: 0 20px 48px -12px rgba(0,0,0,.5), 0 0 0 1px rgba(0,0,0,.15);
  backdrop-filter: blur(8px);
}
.hrms-cookie-banner[hidden] { display: none; }
.hrms-cookie-banner-inner {
  padding: 1.25rem 1.35rem 1.1rem;
  display: flex;
  flex-direction: column;
  gap: .85rem;
}
.hrms-cookie-banner-icon {
  width: 2.25rem; height: 2.25rem;
  border-radius: .65rem;
  display: flex; align-items: center; justify-content: center;
  background: rgba(34, 211, 238, .14);
  color: #22d3ee;
  flex-shrink: 0;
}
.hrms-cookie-banner-body { display: flex; flex-direction: column; gap: .3rem; }
.hrms-cookie-banner-title {
  margin: 0;
  font-size: .88rem;
  font-weight: 700;
  letter-spacing: -.01em;
  color: #f8fafc;
}
.hrms-cookie-banner-text {
  margin: 0;
  font-size: .8rem;
  line-height: 1.55;
  color: #94a3b8;
}
.hrms-cookie-banner-actions {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  padding-top: .15rem;
}
.hrms-cookie-btn {
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: .42rem .85rem;
  font-size: .78rem;
  font-weight: 600;
  cursor: pointer;
  transition: background .14s ease, border-color .14s ease, color .14s ease, transform .1s ease;
  white-space: nowrap;
}
.hrms-cookie-btn:active { transform: scale(.97); }
/* Ghost — lowest visual weight, "manage" is an escape hatch, not the ask */
.hrms-cookie-btn-ghost {
  background: transparent;
  color: #94a3b8;
  margin-right: auto;
}
.hrms-cookie-btn-ghost:hover { color: #e2e8f0; text-decoration: underline; }
/* Outline — secondary action */
.hrms-cookie-btn-outline {
  background: transparent;
  border-color: rgba(255,255,255,.18);
  color: #cbd5e1;
}
.hrms-cookie-btn-outline:hover { background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.3); }
/* Primary — the intended action, given the strongest visual weight */
.hrms-cookie-btn-primary {
  background: #22d3ee;
  color: #04252b;
}
.hrms-cookie-btn-primary:hover { background: #67e3f5; }
.hrms-cookie-btn:focus-visible {
  outline: 2px solid #22d3ee;
  outline-offset: 2px;
}
.hrms-cookie-cat {
  padding: .65rem 0;
  border-bottom: 1px solid var(--border, #e2e8f0);
}
.hrms-cookie-cat:last-of-type { border-bottom: none; }
@media (max-width: 575.98px) {
  .hrms-cookie-banner { left: .6rem; right: .6rem; bottom: .6rem; width: auto; }
  /* Three full-width stacked buttons made the banner 354px tall — 42% of a
     320x844 phone, measured on all 30 public pages. A consent bar has to be
     readable, but it must not own half the screen before the visitor has seen
     anything. "Reject" and "Accept All" are two words each and sit side by
     side comfortably; "Manage Settings" keeps a full-width row because it is
     the longest label and the least-used action. 42% -> 35% at 320px and 33%
     at 390px, verified against production. */
  .hrms-cookie-banner-inner { padding: 1rem 1rem .9rem; gap: .7rem; }
  .hrms-cookie-banner-actions {
    flex-direction: row; flex-wrap: wrap; align-items: stretch; gap: .45rem;
  }
  .hrms-cookie-btn { width: auto; flex: 1 1 0; text-align: center; }
  .hrms-cookie-btn-ghost { margin-right: 0; order: 3; flex: 1 0 100%; }
}
@media (prefers-reduced-motion: no-preference) {
  .hrms-cookie-banner { animation: hrms-cookie-rise .3s cubic-bezier(.16,1,.3,1); }
}
@keyframes hrms-cookie-rise {
  from { opacity: 0; transform: translateY(12px) scale(.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* ── Accessibility (WCAG 2.2 AA) ──────────────────────────────────────── */

/* Skip link -- visually hidden until it receives keyboard focus, then
   appears as the very first thing a keyboard/screen-reader user encounters,
   letting them jump past the nav straight to <main id="main-content">. */
.hrms-skip-link {
  position: absolute;
  top: -3rem;
  left: .75rem;
  z-index: 100000;
  background: #111827;
  color: #fff;
  padding: .6rem 1rem;
  border-radius: var(--radius-sm);
  font-size: .85rem;
  font-weight: 600;
  text-decoration: none;
  transition: top .15s ease;
}
.hrms-skip-link:focus {
  top: .75rem;
  color: #fff;
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* Visible keyboard-focus indicator everywhere (SC 2.4.7 / 2.4.11), even on
   elements that set outline:none for the mouse-click state elsewhere --
   :focus-visible only matches keyboard/programmatic focus in modern
   browsers, so a mouse click never triggers this ring. */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible, summary:focus-visible, [tabindex]:focus-visible,
.btn:focus-visible, .form-check-input:focus-visible {
  outline: 2px solid var(--primary, #4f46e5) !important;
  outline-offset: 2px !important;
}

/* Respect the OS-level "reduce motion" preference (SC 2.3.3) -- shortens
   or removes decorative animation/transition instead of stripping it
   outright, so state changes are still perceivable. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── Page header ── */
.page-header {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 1.5rem 0 1.25rem;
  margin-bottom: 1.5rem;
}
.page-header h1 { font-size: 1.5rem; font-weight: 700; letter-spacing: -.02em; margin: 0; }

/* ── Accordion ── */
.accordion-item { border: 1px solid var(--border); border-radius: var(--radius) !important; margin-bottom: .5rem; overflow: hidden; }
.accordion-button { font-weight: 600; font-size: .9375rem; }
/* --primary-text, not --primary: the ground here is --primary-light, a TINT of
   --primary, so the two are close by construction -- measured 4.10:1 on Flat,
   under AA, on every FAQ page. --primary-text is the token that exists to be
   legible on a light ground and clears 5.56:1 on this exact tint. */
.accordion-button:not(.collapsed) { background: var(--primary-light); color: var(--primary-text); box-shadow: none; }
.accordion-button:focus { box-shadow: 0 0 0 3px rgba(79,70,229,.12); }

/* ── Tech stack badges ── */
.tech-badge {
  display: inline-flex; align-items: center; gap: .4rem;
  /* Third instance of the tint-of-itself trap, after .accordion-button and
     .badge-hr: --primary on --primary-light is 4.10:1 because the ground is
     made FROM the foreground. --primary-text is the legible-on-light role. */
  background: var(--primary-light); color: var(--primary-text);
  /* Was a hardcoded indigo that ignored the preset entirely -- it stayed
     lavender on the teal Flat palette while the fill and text both moved. */
  border: 1px solid color-mix(in srgb, var(--primary) 30%, transparent);
  padding: .35rem .85rem;
  border-radius: 999px;
  font-size: .8rem; font-weight: 600;
}

/* ── Tabs ── */
.nav-tabs { border-bottom: 2px solid var(--border); }
.nav-tabs .nav-link {
  font-weight: 600; font-size: .875rem;
  color: var(--muted); border: none;
  padding: .75rem 1.25rem;
  border-radius: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  transition: color .15s;
}
.nav-tabs .nav-link:hover { color: var(--primary); border-bottom-color: var(--border); }
.nav-tabs .nav-link.active { color: var(--primary); border-bottom-color: var(--primary); }
.tab-content { background: var(--surface); border: 1px solid var(--border); border-top: none; border-radius: 0 0 var(--radius) var(--radius); }

/* ── Timeline ── */
/* ── Timeline / stepper ───────────────────────────────────────────────────
   The rail is drawn PER ITEM, not once down the container, and that is the
   whole design:

   * A container rail (`.timeline::before` with `top:0; bottom:0`) runs the
     full height, so it overshoots below the final dot into empty space. The
     workaround that appeared in CMS content was an authored
     `style="padding-bottom:0"` on the last item — applied to the last TWO on
     /customer-story/, which collapsed the gap between them: measured
     dot-to-dot 75 / 74 / 75 / **50**px, so the last step read as belonging to
     the one above it.
   * A per-item connector simply is not drawn on `:last-child`, so it ends at
     the last dot with nothing to work around.

   `padding-bottom` is `!important` for the same reason: the component owns its
   own rhythm, and authored inline spacing on one item must not be able to
   break the sequence. After: 75 / 74 / 75 / 74. */
.timeline { position: relative; padding-left: 1.5rem; }
.timeline-item { position: relative; padding-bottom: 1.5rem !important; }
.timeline-item:last-child { padding-bottom: 0 !important; }
.timeline-item::before {
  content: ''; position: absolute; left: -1.1rem; top: .3rem;
  width: .625rem; height: .625rem;
  border-radius: 50%; background: var(--primary);
  border: 2px solid var(--surface);
  box-shadow: 0 0 0 2px var(--primary);
}
/* Centred on the dot (dot left + half its width), from just under this dot to
   just above the next one. */
.timeline-item::after {
  content: ''; position: absolute;
  left: calc(-1.1rem + .3125rem - 1px);
  top: calc(.3rem + .625rem + .25rem);
  bottom: -.3rem;
  width: 2px; background: var(--border);
}
.timeline-item:last-child::after { display: none; }

/* ── News card ── */
.news-date { font-size: .75rem; font-weight: 700; color: var(--primary); text-transform: uppercase; letter-spacing: .06em; }

/* ── Utilities ── */
.text-primary { color: var(--primary) !important; }
/* .text-dark means "strongest ink", not "near-black" -- and on a dark
   surface Bootstrap's literal renders at 1.15:1, i.e. invisible. 16 nodes.

   But .text-dark is used for TWO different things, and this rule alone made
   the second one worse: as ink on a themed surface (what it fixes), and as
   the deliberately-dark label on a LIGHT semantic fill such as
   `.text-bg-info` or `.bg-warning` (which Bootstrap gives dark text on
   purpose). Following the theme there flipped that text light on a light
   cyan -- 1.52:1, measured, worse than the 1.15:1 it replaced. Restored
   below for exactly those fills, which are light in every theme because they
   are Bootstrap literals. */
.text-dark { color: var(--text) !important; }
/* Same split as --success/--success-text: the vivid tone is a FILL colour,
   the -text variant is the one solved for reading. .text-success measured
   3.99:1 using the fill value. */
.text-success { color: var(--success-text) !important; }
.text-danger  { color: var(--danger-text)  !important; }
.text-warning { color: var(--warning-text) !important; }
.text-info    { color: var(--info-text)    !important; }
/* A --primary FILL carries --on-primary text, exactly as .btn-primary does.
   Without this, .badge.bg-primary and friends kept Bootstrap's hardcoded
   white and measured 2.98:1 on the presets whose accent lightens for a
   dark page -- 90 nodes, the single largest block of contrast failures
   left after the button itself was fixed. Same token, same reason; the
   utility was simply missed because it sets only a background. */
.bg-primary  { background: var(--primary) !important; color: var(--on-primary); }
.badge.bg-primary, .text-bg-primary { color: var(--on-primary) !important; }
.bg-surface  { background: var(--surface); }
.rounded-xl  { border-radius: var(--radius); }
.shadow-sm   { box-shadow: var(--shadow-sm) !important; }
.shadow      { box-shadow: var(--shadow) !important; }
.shadow-lg   { box-shadow: var(--shadow-lg) !important; }
.fw-semibold { font-weight: 600; }
.fw-extrabold { font-weight: 800; }
/* Type-scale utilities (Phase 4 of the UI audit). .fs-sm/.fs-xs predate the
   scale and are used 745/439 times; they are re-expressed via the anchored
   tokens so they render at exactly the same .875rem/.75rem as before while
   becoming responsive to the tenant's font setting. Do not "fix" .fs-sm to
   point at --fs-sm -- that would shrink 745 call sites. */
.fs-sm       { font-size: var(--fs-md); }
.fs-xs       { font-size: var(--fs-xs); }
/* Digits that sit in a column -- timestamps, counts, card UIDs -- so they
   align vertically instead of jittering as values change on a live-updating
   table. */
.num         { font-variant-numeric: tabular-nums; }
.fs-4xs      { font-size: var(--fs-4xs); }
.fs-3xs      { font-size: var(--fs-3xs); }
.fs-2xs      { font-size: var(--fs-2xs); }
.fs-dense    { font-size: var(--fs-sm); }
.fs-base     { font-size: var(--fs-base); }
.fs-lg       { font-size: var(--fs-lg); }
.fs-xl       { font-size: var(--fs-xl); }
.fs-2xl      { font-size: var(--fs-2xl); }
.text-muted  { color: var(--muted) !important; }
/* Not a real Bootstrap 5 utility (despite the Bootstrap-style name, which
   several templates already assumed existed) — a flex child's default
   min-width is auto (= its content size), which stops text-truncate /
   wrapping from working inside a flex row on narrow screens. */
.min-width-0 { min-width: 0; }

/* Lucide icon sizing helper */
.icon-sm  { width: 1rem;    height: 1rem;    }
.icon-md  { width: 1.25rem; height: 1.25rem; }
.icon-lg  { width: 1.5rem;  height: 1.5rem;  }

/* ── Responsive sidebar ── */
@media (max-width: 767.98px) {
  .hrms-sidebar { min-height: auto; position: static; margin-bottom: 1.5rem; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   DARK MODE  —  Bootstrap 5.3+ data-bs-theme="dark" + custom overrides
   All custom CSS vars are re-declared; Bootstrap handles its own tokens.
═══════════════════════════════════════════════════════════════════════════ */

/* ── Theme toggle button (shared between navbar & sidebar footer) ── */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.1rem; height: 2.1rem;
  border-radius: .5rem;
  background: transparent;
  border: 1px solid var(--border);
  cursor: pointer;
  color: var(--muted);
  transition: background .15s, color .15s, border-color .15s;
  flex-shrink: 0;
  padding: 0;
}
.theme-toggle:hover { background: var(--primary-light); color: var(--primary); border-color: var(--primary); }
.theme-toggle .icon-sun  { display: none; }
.theme-toggle .icon-moon { display: block; }
[data-bs-theme="dark"] .theme-toggle .icon-sun  { display: block; }
[data-bs-theme="dark"] .theme-toggle .icon-moon { display: none; }

/* ── Dark custom properties ── */
[data-bs-theme="dark"] {
  --bg:            #0f172a;
  --surface:       #1e293b;
  --text:          #e2e8f0;
  --muted:         #94a3b8;
  --border:        #334155;
  --primary-light: rgba(99,102,241,.15);
  /* Accessible text variants, re-solved for a dark surface. On dark, success/
     warning/info already clear AA at their vivid values, so they revert to
     those -- darkening them here (as the light theme does) would make them
     WORSE, which is exactly why these are tokens per theme rather than one
     fixed "safe" colour. Only danger needed lightening (3.51:1 -> 4.55:1
     against its own tint). Verified in
     apps/system_settings/tests/test_preset_contrast.py. */
  --primary-text: #a5b4fc;
  --success-text: var(--success);
  --warning-text: var(--warning);
  /* info and danger both need lightening on dark: at their vivid values they
     measure 4.38:1 and 3.51:1 against their own tints, just under AA. */
  --info-text:    #15a8ea;
  --danger-text:  #f36d6d;
  color-scheme: dark;
}

/* ── Base ── */
[data-bs-theme="dark"] body  { background: var(--bg); color: var(--text); }
/* :where(...) wraps ONLY the attribute selector so it contributes ZERO
   specificity, leaving the bare `a`/`a:hover` type-selector specificity
   ((0,0,1) / (0,1,1)) intact — enough to beat the equivalent light-mode
   `a`/`a:hover` rules above (same specificity, later source order wins)
   but, critically, LOWER than any single class selector. Previously this
   was plain `[data-bs-theme="dark"] a`, an attribute+tag selector at
   (0,1,1)/(0,2,1) — HIGHER than a lone class like `.btn-primary` (0,1,0)
   — so it silently beat every class-styled anchor that doesn't happen to
   carry its own dark-mode color override: .btn-primary, .msgc-compose-btn,
   .btn-nav-cta, .profile-section-link, .portal-subnav-link, and any future
   one-off "anchor styled as a button/nav-item/card" component, each
   rendering invisible (indigo-on-indigo) in dark mode until individually
   excluded. Rather than keep chasing that allowlist one class at a time,
   :where() makes this rule structurally unable to ever outrank a class
   selector again — it only colors anchors with no styling opinion of
   their own (genuine inline prose links), which is what a "plain link
   color" reset is supposed to do. */
:where([data-bs-theme="dark"]) a       { color: #818cf8; }
:where([data-bs-theme="dark"]) a:hover { color: #a5b4fc; }

/* ── Navbar ──
   Only the two rules that a token cannot express are left here. The other
   five were `background: #0f172a`, `border-bottom-color: #334155` and three
   nav-link colours -- literals that duplicated what .hrms-nav already reads
   from --surface/--border/--muted/--text, and duplicated them WRONGLY once
   the public site started following the preset: on the Dark preset --surface
   is #1A1D27, so the public header rendered a different navy from every card
   beneath it, and on the light presets these never applied at all. The one
   real difference on a dark surface is .nav-link.active, where --primary is
   a fill colour rather than a legible text colour -- that is what
   --primary-text exists for. */
[data-bs-theme="dark"] .hrms-nav .nav-link.active { color: var(--primary-text); }

/* ── Sidebar ──
   Reads var(--sidebar-bg) rather than a hardcoded hex so that toggling dark
   mode on top of a non-dark UI preset (Red/Blue/Simple/...) doesn't stomp
   that preset's own --sidebar-bg back to an unrelated navy-black. */
[data-bs-theme="dark"] .hrms-sidebar { background: var(--sidebar-bg, #080f1e); }

/* ── Cards ── */
[data-bs-theme="dark"] .card { background: var(--surface); border-color: var(--border); }
[data-bs-theme="dark"] .card-header { background: transparent; border-bottom-color: var(--border); color: var(--muted); }
[data-bs-theme="dark"] .card-body { color: var(--text); }

/* ── Tables ── */
[data-bs-theme="dark"] .hrms-table thead th { background: #0f172a; color: var(--muted); border-bottom-color: var(--border); }
[data-bs-theme="dark"] .hrms-table tbody tr:hover { background: rgba(99,102,241,.07); }
[data-bs-theme="dark"] .hrms-table tbody td { border-bottom-color: var(--border); color: var(--text); }

/* ── Forms ── */
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select {
  background-color: #1e293b; color: var(--text); border-color: #334155;
  color-scheme: dark;
}
[data-bs-theme="dark"] .form-control::placeholder { color: #64748b; }
[data-bs-theme="dark"] .form-control:focus,
[data-bs-theme="dark"] .form-select:focus {
  background-color: #1e293b; color: var(--text);
  border-color: var(--primary); box-shadow: 0 0 0 3px rgba(99,102,241,.2);
}
[data-bs-theme="dark"] .form-label { color: var(--text); }
[data-bs-theme="dark"] .form-check-input { background-color: #334155; border-color: #475569; }
/* var(--muted), not a literal. #64748b measured 3.97:1 on the dark surface —
   axe flagged .form-text on 42 pages, and it is the text that explains what a
   field expects, so it is exactly the wrong place to save contrast.
   --muted is audited against surface and bg for all 23 presets
   (apps/system_settings/preset_audit.py), so this now tracks whatever the
   active theme guarantees instead of pinning one value that no check covered. */
[data-bs-theme="dark"] .form-text  { color: var(--muted); }
[data-bs-theme="dark"] .input-group-text { background-color: #334155; border-color: #334155; color: var(--muted); }

/* ── Buttons ──
   .btn-outline-secondary/.btn-light are now token-driven year-round (see
   the base rules above), so Dark preset's own --surface/--border/--text
   already produce the right look here without a hardcoded override —
   keeping one would just fight the token values with stale hex. */
[data-bs-theme="dark"] .btn-link { color: #818cf8; }
[data-bs-theme="dark"] .btn-link:hover { color: #a5b4fc; }

/* ── Badges (semantic) ── */
[data-bs-theme="dark"] .badge-present  { background: rgba(16,185,129,.2);  color: #6ee7b7; }
[data-bs-theme="dark"] .badge-late     { background: rgba(245,158,11,.2);  color: #fcd34d; }
[data-bs-theme="dark"] .badge-absent   { background: rgba(239,68,68,.2);   color: #fca5a5; }
[data-bs-theme="dark"] .badge-leave    { background: rgba(14,165,233,.2);  color: #7dd3fc; }
[data-bs-theme="dark"] .badge-pending  { background: rgba(245,158,11,.2);  color: #fcd34d; }
[data-bs-theme="dark"] .badge-approved { background: rgba(16,185,129,.2);  color: #6ee7b7; }
[data-bs-theme="dark"] .badge-rejected { background: rgba(239,68,68,.2);   color: #fca5a5; }
[data-bs-theme="dark"] .badge-error    { background: rgba(249,115,22,.2);  color: #fdba74; }
[data-bs-theme="dark"] .badge-hr       { background: rgba(99,102,241,.2);  color: #a5b4fc; }
[data-bs-theme="dark"] .badge-admin    { background: rgba(139,92,246,.2);  color: #c4b5fd; }
[data-bs-theme="dark"] .badge-employee { background: rgba(148,163,184,.1); color: #cbd5e1; }

/* ── Alerts ── */
[data-bs-theme="dark"] .alert-info    { background: rgba(14,165,233,.15);  color: #7dd3fc; border-color: rgba(14,165,233,.3); }
[data-bs-theme="dark"] .alert-success { background: rgba(16,185,129,.15);  color: #6ee7b7; border-color: rgba(16,185,129,.3); }
[data-bs-theme="dark"] .alert-warning { background: rgba(245,158,11,.15);  color: #fcd34d; border-color: rgba(245,158,11,.3); }
[data-bs-theme="dark"] .alert-danger  { background: rgba(239,68,68,.15);   color: #fca5a5; border-color: rgba(239,68,68,.3); }
[data-bs-theme="dark"] .alert-primary { background: rgba(99,102,241,.15);  color: #a5b4fc; border-color: rgba(99,102,241,.3); }

/* ── Employee registration workspace ── */
.emp-register-shell {
  display: grid;
  gap: 1.25rem;
}
.emp-register-hero {
  background:
    radial-gradient(ellipse at 80% 0%, rgba(99,102,241,.12) 0%, transparent 50%),
    radial-gradient(ellipse at 20% 100%, rgba(14,165,233,.08) 0%, transparent 40%),
    linear-gradient(135deg, var(--surface) 0%, rgba(241,245,249,.55) 100%);
  border: 1px solid var(--border);
  border-radius: 1.25rem;
  box-shadow: 0 2px 12px rgba(0,0,0,.06), 0 0 0 1px rgba(255,255,255,.5) inset;
  padding: 1.5rem 1.5rem 1.25rem;
  position: relative;
  overflow: hidden;
}
.emp-register-hero::before {
  content: '';
  position: absolute;
  top: -2rem; right: -2rem;
  width: 12rem; height: 12rem;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(99,102,241,.08) 0%, transparent 70%);
  pointer-events: none;
}
.emp-register-title {
  font-size: clamp(1.4rem, 2.2vw, 1.9rem);
  font-weight: 800;
  letter-spacing: -.035em;
  margin-bottom: .3rem;
  background: linear-gradient(135deg, var(--text) 0%, rgba(var(--bs-primary-rgb),.75) 100%);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text;
}
.emp-register-subtitle {
  color: var(--muted);
  margin-bottom: 0;
  max-width: 58rem;
  font-size: .88rem;
}
.emp-stat-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: .75rem;
}
.emp-stat-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: .9rem;
  padding: .9rem 1rem;
  box-shadow: var(--shadow-sm);
}
.emp-stat-label {
  color: var(--muted);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.emp-stat-value {
  font-size: 1.35rem;
  font-weight: 800;
  letter-spacing: -.03em;
  color: var(--text);
  line-height: 1.1;
  margin-top: .15rem;
}
.emp-panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: .875rem;
  box-shadow: 0 1px 4px rgba(0,0,0,.06), 0 4px 16px rgba(0,0,0,.04);
  overflow: hidden;
  transition: box-shadow .2s;
}
.emp-panel:hover { box-shadow: 0 2px 8px rgba(0,0,0,.08), 0 8px 24px rgba(0,0,0,.05); }
.emp-panel-header {
  background: linear-gradient(135deg, var(--surface) 0%, rgba(241,245,249,.45) 100%);
  border-bottom: 1px solid var(--border);
  padding: 1.1rem 1.25rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  flex-wrap: wrap;
}
.emp-panel-title {
  font-weight: 800;
  font-size: 1.05rem;
  margin: 0;
  letter-spacing: -.025em;
}
.emp-panel-copy {
  color: var(--muted);
  margin: .2rem 0 0;
  font-size: .83rem;
  line-height: 1.5;
}
.emp-import-list {
  margin: 0;
  padding-left: 1.15rem;
  color: var(--muted);
  font-size: .875rem;
}
.emp-help-card {
  background: linear-gradient(135deg, color-mix(in srgb, var(--primary-light) 72%, var(--surface) 28%) 0%, var(--surface) 100%);
  border: 1px solid color-mix(in srgb, var(--primary-light) 64%, var(--border) 36%);
  border-radius: 1rem;
  padding: 1rem;
}
.emp-result-badge {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  border-radius: 999px;
  padding: .3rem .65rem;
  font-weight: 700;
  font-size: .75rem;
  letter-spacing: .02em;
}
.emp-result-badge.created { background: #dcfce7; color: #166534; }
.emp-result-badge.failed { background: #fee2e2; color: #991b1b; }
.emp-result-badge.blocked { background: #fef3c7; color: #92400e; }
.emp-upload-summary {
  display: flex;
  gap: .5rem;
  flex-wrap: wrap;
}

/* Dark mode: register workspace */
[data-bs-theme="dark"] .emp-register-hero,
[data-bs-theme="dark"] .emp-stat-card,
[data-bs-theme="dark"] .emp-panel {
  background: var(--surface);
  border-color: var(--border);
}
[data-bs-theme="dark"] .emp-register-hero {
  background:
    radial-gradient(circle at top right, rgba(14,165,233,.16), transparent 34%),
    linear-gradient(135deg, #111827 0%, #0f172a 100%);
}
[data-bs-theme="dark"] .emp-panel-header {
  background: linear-gradient(180deg, #1e293b 0%, #111827 100%);
}
[data-bs-theme="dark"] .emp-help-card {
  background: linear-gradient(135deg, rgba(99,102,241,.12) 0%, #111827 100%);
  border-color: rgba(99,102,241,.25);
}
[data-bs-theme="dark"] .emp-result-badge.created { background: rgba(16,185,129,.18); color: #6ee7b7; }
[data-bs-theme="dark"] .emp-result-badge.failed { background: rgba(239,68,68,.18); color: #fca5a5; }
[data-bs-theme="dark"] .emp-result-badge.blocked { background: rgba(245,158,11,.18); color: #fcd34d; }

/* ── Employee registration: industry-standard stepper ── */
.emp-stepper {
  list-style: none;
  margin: 0 0 2rem;
  padding: .5rem 0;
  display: flex;
  position: relative;
}
.emp-step {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
  cursor: pointer;
  padding: 0;
  background: transparent;
  border: none;
  user-select: none;
}
/* Connector line */
.emp-step:not(:last-child)::after {
  content: '';
  position: absolute;
  top: calc(1.4rem - 1px);
  left: calc(50% + 1.5rem);
  width: calc(100% - 3rem);
  height: 2px;
  background: var(--border);
  z-index: 0;
  transition: background .4s ease;
  border-radius: 999px;
}
.emp-step.is-done:not(:last-child)::after {
  background: linear-gradient(90deg, var(--success), rgba(34,197,94,.5));
}

.emp-step-circle {
  width: 2.8rem;
  height: 2.8rem;
  border-radius: 50%;
  border: 2px solid var(--border);
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: .95rem;
  color: var(--muted);
  position: relative;
  z-index: 1;
  transition: border-color .3s, background .3s, box-shadow .3s, color .3s, transform .2s;
  margin-bottom: .5rem;
  box-shadow: 0 1px 3px rgba(0,0,0,.06);
}
.emp-step-num { line-height: 1; }
.emp-step-check { display: none; }
.emp-step.is-done .emp-step-num  { display: none; }
.emp-step.is-done .emp-step-check { display: block; }
.emp-step.is-active .emp-step-circle {
  border-color: transparent;
  background: linear-gradient(135deg, var(--primary) 0%, rgba(79,70,229,.8) 100%);
  color: #fff;
  box-shadow: 0 0 0 4px rgba(79,70,229,.15), 0 4px 12px rgba(79,70,229,.3);
  transform: scale(1.08);
}
.emp-step.is-done .emp-step-circle {
  border-color: transparent;
  background: linear-gradient(135deg, #16a34a 0%, #22c55e 100%);
  color: #fff;
  box-shadow: 0 2px 8px rgba(34,197,94,.25);
}
.emp-step-label {
  font-size: .78rem;
  font-weight: 600;
  color: var(--muted);
  text-align: center;
  white-space: nowrap;
  transition: color .25s, font-weight .15s;
  letter-spacing: .01em;
}
.emp-step.is-active .emp-step-label { color: var(--primary); font-weight: 700; }
.emp-step.is-done   .emp-step-label { color: #16a34a; font-weight: 600; }
.emp-step-panel { display: none; }
.emp-step-panel.is-active { display: block; }

/* Camera */
.emp-camera-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  background: #0f172a;
  border-radius: 1rem;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #cbd5e1;
}
.emp-camera-video, .emp-camera-preview {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.emp-camera-canvas { display: none; }
.emp-camera-idle {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 1rem;
  color: #94a3b8;
}

/* Step-2 mini card */
.emp-card-mini {
  background: linear-gradient(135deg, var(--surface) 0%, rgba(241,245,249,.6) 100%);
  border: 1px solid var(--border);
  border-radius: .85rem;
  padding: .8rem 1rem;
}
.emp-card-mini-label { font-size: .7rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); font-weight: 700; }
.emp-card-mini-value { font-weight: 700; font-size: 1.05rem; margin-top: .1rem; }
.emp-card-mini-sub { color: var(--muted); font-size: .85rem; }

/* Step-3 summary card */
.emp-summary-card {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: .75rem;
  border: 1px solid var(--border);
  border-radius: 1rem;
  padding: 1rem;
  background: linear-gradient(135deg, var(--surface) 0%, rgba(241,245,249,.6) 100%);
}
.emp-summary-photo {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  border-radius: .85rem;
  overflow: hidden;
  background: #e2e8f0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.emp-summary-photo img { width: 100%; height: 100%; object-fit: cover; }
.emp-summary-photo-placeholder { color: #64748b; font-size: .85rem; font-weight: 600; }
.emp-summary-name { font-weight: 800; font-size: 1.1rem; }
.emp-summary-number { font-weight: 700; color: var(--primary); font-size: .9rem; letter-spacing: .04em; }
.emp-summary-meta { color: var(--muted); font-size: .85rem; }

/* Tooltip badge in dropzone */
.emp-tooltip-badge {
  position: absolute;
  top: .6rem;
  right: .6rem;
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .22rem .5rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--primary-light) 80%, var(--surface));
  color: var(--primary);
  border: 1px solid color-mix(in srgb, var(--primary-light) 60%, var(--border));
  font-size: .72rem;
  font-weight: 700;
  cursor: help;
  z-index: 2;
  transition: background .15s;
}
.emp-tooltip-badge:hover { background: var(--primary-light); }

/* ── Enhanced CSV drop zone ── */
.emp-dropzone {
  border: 1.5px dashed color-mix(in srgb, var(--border) 70%, var(--primary) 30%);
  background: linear-gradient(180deg, color-mix(in srgb, var(--primary-light) 8%, var(--surface)) 0%, var(--surface) 100%);
  border-radius: 1rem;
  padding: .75rem;
  transition: border-color .2s, background .2s;
}
.emp-dropzone:focus-within { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,.1); }
.emp-drop-area {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .3rem;
  padding: 1.75rem 1rem;
  border-radius: .75rem;
  cursor: pointer;
  text-align: center;
  color: var(--muted);
  transition: background .2s;
}
.emp-drop-area:hover, .emp-drop-area.has-file { background: color-mix(in srgb, var(--primary-light) 18%, var(--surface)); }
.emp-drop-area.is-dragover {
  background: color-mix(in srgb, var(--primary-light) 30%, var(--surface));
  outline: 2px dashed var(--primary);
  outline-offset: -4px;
}
.emp-drop-icon { color: var(--primary); opacity: .75; margin-bottom: .2rem; flex-shrink: 0; }
.emp-drop-title { font-weight: 700; font-size: .9rem; color: var(--text); }
.emp-drop-hint { font-size: .8rem; color: var(--muted); }
.emp-drop-link { color: var(--primary); font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }

/* File selection info card */
.emp-file-info {
  display: flex;
  align-items: center;
  gap: .65rem;
  padding: .6rem .85rem;
  margin-top: .5rem;
  background: color-mix(in srgb, var(--success) 8%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--success) 28%, var(--border));
  border-radius: .75rem;
}
.emp-file-info-icon { color: var(--success); flex-shrink: 0; }
.emp-file-info-body { flex: 1; min-width: 0; }
.emp-file-info-name { font-weight: 700; font-size: .85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.emp-file-info-size { font-size: .74rem; color: var(--muted); margin-top: .05rem; }
.emp-file-clear-btn {
  background: none;
  border: none;
  padding: .3rem;
  color: var(--muted);
  cursor: pointer;
  border-radius: .35rem;
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  transition: background .15s, color .15s;
}
.emp-file-clear-btn:hover { background: rgba(239,68,68,.12); color: #dc2626; }

/* Bulk progress */
.emp-bulk-progress {
  border: 1px solid var(--border);
  border-radius: .9rem;
  padding: .9rem 1rem;
  background: var(--surface);
}
.emp-bulk-log {
  margin-top: .65rem;
  max-height: 8rem;
  overflow-y: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .72rem;
  color: var(--muted);
  border-top: 1px solid var(--border);
  padding-top: .5rem;
}
.emp-bulk-log-line.is-fail { color: #b91c1c; }

/* Dark mode: stepper, camera, dropzone, progress */
[data-bs-theme="dark"] .emp-step-circle { border-color: #334155; background: #1e293b; color: #94a3b8; }
[data-bs-theme="dark"] .emp-step.is-active .emp-step-circle {
  border-color: var(--primary);
  background: var(--primary);
  color: #fff;
}
[data-bs-theme="dark"] .emp-step.is-done .emp-step-circle { border-color: var(--success); background: var(--success); color: #fff; }
[data-bs-theme="dark"] .emp-step-label { color: #64748b; }
[data-bs-theme="dark"] .emp-step.is-active .emp-step-label { color: #a5b4fc; }
[data-bs-theme="dark"] .emp-step.is-done .emp-step-label { color: #6ee7b7; }
[data-bs-theme="dark"] .emp-step:not(:last-child)::after { background: #334155; }
[data-bs-theme="dark"] .emp-step.is-done:not(:last-child)::after { background: var(--success); }
[data-bs-theme="dark"] .emp-card-mini,
[data-bs-theme="dark"] .emp-summary-card { background: linear-gradient(135deg, #111827 0%, #0f172a 100%); border-color: #334155; }
[data-bs-theme="dark"] .emp-summary-photo { background: #1e293b; }
[data-bs-theme="dark"] .emp-summary-photo-placeholder { color: #94a3b8; }
[data-bs-theme="dark"] .emp-tooltip-badge { background: rgba(99,102,241,.18); color: #a5b4fc; border-color: rgba(99,102,241,.35); }
[data-bs-theme="dark"] .emp-dropzone { background: linear-gradient(180deg, rgba(99,102,241,.05) 0%, #0f172a 100%); border-color: rgba(99,102,241,.28); }
[data-bs-theme="dark"] .emp-drop-area:hover,
[data-bs-theme="dark"] .emp-drop-area.has-file { background: rgba(99,102,241,.1); }
[data-bs-theme="dark"] .emp-drop-area.is-dragover { background: rgba(99,102,241,.18); }
[data-bs-theme="dark"] .emp-file-info { background: rgba(16,185,129,.07); border-color: rgba(16,185,129,.22); }
[data-bs-theme="dark"] .emp-bulk-progress { background: #0f172a; border-color: #334155; }
[data-bs-theme="dark"] .emp-bulk-log { border-top-color: #334155; }
[data-bs-theme="dark"] .emp-bulk-log-line.is-fail { color: #fca5a5; }

/* ── Register page: mobile / responsive improvements ─────────────────── */
@media (max-width: 767px) {
  /* Tighten hero + panel padding on small screens */
  .emp-register-hero { padding: .9rem 1rem .75rem; }
  .emp-panel-header   { padding: .85rem 1rem; }
  /* Prevent horizontal overflow */
  .emp-register-shell { overflow-x: clip; }
  /* Camera stage can be slightly shorter on phones */
  .emp-camera-stage   { aspect-ratio: 16 / 10; }
  /* HID hint text can be hidden on very narrow viewports to save space */
  #empHidHint { display: none; }
}
@media (max-width: 575px) {
  .emp-stepper { gap: .5rem; }
  .emp-step-label { font-size: .72rem; }
  .emp-step-circle { width: 2rem; height: 2rem; font-size: .78rem; }
  .emp-step:not(:last-child)::after { top: calc(1rem - 1px); left: calc(50% + 1.1rem); width: calc(100% - 2.2rem); }
  /* Even tighter padding on xs */
  .emp-register-hero { padding: .75rem .85rem .65rem; }
  .emp-panel-header   { padding: .75rem .85rem; }
  /* Allow camera to breathe on small phones */
  .emp-camera-stage   { aspect-ratio: 4 / 3; }
  /* QR code thumbnail: smaller on xs to avoid layout push */
  #empQrImg, #empQrPlaceholder { width: 80px !important; height: 80px !important; }
}

/* ── Modals & dropdowns — token-driven year-round now (see the base
   .dropdown-menu/.dropdown-item rules above), not just under dark mode. ── */
.modal-content { background: var(--surface); border-color: var(--border); }
.modal-header  { border-bottom-color: var(--border); }
.modal-footer  { border-top-color: var(--border); }
[data-bs-theme="dark"] .dropdown-item:hover { background: rgba(99,102,241,.15); color: #a5b4fc; }
[data-bs-theme="dark"] .dropdown-divider { border-color: #334155; }

/* ── List groups ── */
[data-bs-theme="dark"] .list-group-item { background: var(--surface); border-color: var(--border); color: var(--text); }
[data-bs-theme="dark"] .list-group-item:hover { background: rgba(99,102,241,.07); }

/* ── Tabs ── */

/* ══════════════════════════════════════════════
   Toast Notification System (config-driven)
   ══════════════════════════════════════════════ */
#hrms-toast-container { max-width: 420px; }
.hrms-toast-wrap {
  pointer-events: all;
  animation: hrmsToastIn .3s cubic-bezier(.34,1.4,.64,1);
  will-change: transform, opacity;
  transform-origin: right center;
}
#hrms-toast-container[data-pos-type="bottom"] .hrms-toast-wrap {
  animation-name: hrmsToastInBottom;
  transform-origin: right bottom;
}
.hrms-toast-wrap.hrms-toast-out {
  animation: hrmsToastOut .28s ease forwards !important;
  pointer-events: none;
}
#hrms-toast-container[data-pos-type="bottom"] .hrms-toast-wrap.hrms-toast-out {
  animation-name: hrmsToastOutBottom !important;
}
@keyframes hrmsToastIn        { from { opacity:0; transform:translateX(16px) scale(.95); } to { opacity:1; transform:none; } }
@keyframes hrmsToastInBottom  { from { opacity:0; transform:translateY(10px) scale(.95); } to { opacity:1; transform:none; } }
@keyframes hrmsToastOut        { from { opacity:1; max-height:200px; } to { opacity:0; transform:translateX(16px) scale(.94); max-height:0; margin-bottom:0; overflow:hidden; } }
@keyframes hrmsToastOutBottom  { from { opacity:1; max-height:200px; } to { opacity:0; transform:translateY(8px) scale(.94); max-height:0; margin-bottom:0; overflow:hidden; } }
.hrms-toast {
  display: flex;
  border-radius: .9rem;
  box-shadow: 0 4px 24px rgba(0,0,0,.12), 0 1px 4px rgba(0,0,0,.05);
  overflow: hidden;
  min-width: 280px;
  max-width: 420px;
}
.hrms-toast-strip  { width: 4px; flex-shrink: 0; }
.hrms-toast-inner  { flex: 1; padding: .8rem 1rem; }
.hrms-toast-inner.filled { padding: .8rem 1rem; }
.hrms-toast-header { display: flex; align-items: center; gap: .45rem; }
.hrms-toast-icon   { display: flex; align-items: center; flex-shrink: 0; }
.hrms-toast-title  { font-weight: 700; font-size: .875rem; flex: 1; min-width: 0; }
.hrms-toast-close  {
  background: none; border: none; padding: .25rem; cursor: pointer; line-height: 1;
  border-radius: .35rem; display: inline-flex; align-items: center; flex-shrink: 0;
  opacity: .6; transition: opacity .15s, background .15s;
  /* Measured 21x21 at every width tested (320-1440) on 15 pages — under WCAG
     2.2 SC 2.5.8's 24px floor, which is not a mobile rule: a toast is
     transient, so the one control that dismisses it before it disappears has
     to be easy to hit with a mouse too. min-* keeps the 16px glyph and its
     padding exactly as they are; only the hit area grows. */
  min-width: 24px; min-height: 24px; justify-content: center;
}
.hrms-toast-close:hover { opacity: 1; background: rgba(0,0,0,.08); }
.hrms-toast-body   { font-size: .82rem; line-height: 1.45; margin-top: .3rem; }

/* ── Toast settings page: position picker ── */
.toast-pos-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: .35rem;
  max-width: 210px;
}
.toast-pos-btn {
  aspect-ratio: 16 / 9;
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  padding: .28rem .32rem;
  transition: border-color .15s, background .15s;
}
.toast-pos-btn[data-pos^="bottom"] { align-items: flex-end; }
.toast-pos-btn[data-pos*="center"] { justify-content: center; }
.toast-pos-btn[data-pos*="start"]  { justify-content: flex-start; }
.toast-pos-dot {
  width: 11px; height: 7px;
  background: var(--muted);
  border-radius: 2px;
  transition: background .15s;
}
.toast-pos-btn.is-active             { border-color: var(--primary); background: color-mix(in srgb, var(--primary-light) 28%, var(--surface)); }
.toast-pos-btn.is-active .toast-pos-dot { background: var(--primary); }
.toast-pos-btn:hover:not(.is-active) { border-color: color-mix(in srgb, var(--primary) 40%, var(--border)); }

/* ── Toast style selector cards ── */
.toast-style-cards { display: flex; gap: .6rem; }
.toast-style-card {
  flex: 1; border: 1.5px solid var(--border); border-radius: .75rem;
  padding: .7rem .75rem; cursor: pointer; transition: border-color .15s, background .15s; text-align: center;
}
.toast-style-card.is-active { border-color: var(--primary); background: color-mix(in srgb, var(--primary-light) 22%, var(--surface)); }
.toast-style-card:hover:not(.is-active) { border-color: color-mix(in srgb, var(--primary) 40%, var(--border)); }
.toast-style-preview {
  border-radius: .45rem; height: 36px; margin-bottom: .45rem;
  display: flex; align-items: center; overflow: hidden;
  border: 1px solid var(--border); background: var(--surface);
}
.toast-style-preview-strip { width: 4px; height: 100%; background: #4f46e5; flex-shrink: 0; }
.toast-style-preview-body  { flex: 1; padding: .4rem .6rem; }
.toast-style-preview-bar   { background: var(--border); border-radius: 2px; }
.toast-style-preview.filled { background: #4f46e5; border-color: transparent; }
.toast-style-preview.filled .toast-style-preview-bar { background: rgba(255,255,255,.5); }

/* ── Toast color rows ── */
.toast-color-row {
  display: flex; align-items: center; gap: .65rem;
  padding: .55rem .8rem;
  border: 1px solid var(--border); border-radius: .65rem; background: var(--surface);
}
.toast-color-swatch { width: .9rem; height: .9rem; border-radius: 50%; flex-shrink: 0; border: 1px solid rgba(0,0,0,.1); }
.toast-color-label  { font-size: .82rem; font-weight: 600; flex: 1; }
.toast-color-hex    { font-size: .75rem; color: var(--muted); font-family: ui-monospace, monospace; }
input[type="color"].toast-color-input {
  width: 2.1rem; height: 2.1rem; border-radius: .4rem;
  padding: .1rem; cursor: pointer; border: 1px solid var(--border);
}

/* Dark mode overrides for toast settings */
[data-bs-theme="dark"] .toast-pos-btn     { background: #111827; border-color: #334155; }
[data-bs-theme="dark"] .toast-pos-btn.is-active { background: rgba(99,102,241,.18); border-color: #818cf8; }
[data-bs-theme="dark"] .toast-style-card  { background: #111827; border-color: #334155; }
[data-bs-theme="dark"] .toast-style-card.is-active { background: rgba(99,102,241,.18); border-color: #818cf8; }
[data-bs-theme="dark"] .toast-style-preview { background: #1e293b; border-color: #334155; }
[data-bs-theme="dark"] .toast-color-row   { background: #111827; border-color: #334155; }
[data-bs-theme="dark"] .nav-tabs { border-bottom-color: var(--border); }
[data-bs-theme="dark"] .nav-tabs .nav-link { color: var(--muted); }
[data-bs-theme="dark"] .nav-tabs .nav-link:hover { color: #a5b4fc; border-bottom-color: #334155; }
[data-bs-theme="dark"] .nav-tabs .nav-link.active { color: #818cf8; border-bottom-color: #818cf8; }
[data-bs-theme="dark"] .tab-content { background: var(--surface); border-color: var(--border); }

/* ── Accordion ── */
[data-bs-theme="dark"] .accordion-item { background: var(--surface); border-color: var(--border); }
[data-bs-theme="dark"] .accordion-button { background: var(--surface); color: var(--text); }
[data-bs-theme="dark"] .accordion-button:not(.collapsed) { background: rgba(99,102,241,.15); color: #a5b4fc; box-shadow: none; }
[data-bs-theme="dark"] .accordion-button::after { filter: invert(1) hue-rotate(180deg); }
[data-bs-theme="dark"] .accordion-body { background: var(--surface); color: var(--text); }

/* ── Page header ── */
[data-bs-theme="dark"] .page-header { background: var(--surface); border-bottom-color: var(--border); }
[data-bs-theme="dark"] .page-header h1 { color: var(--text); }

/* ── Login card ── */
[data-bs-theme="dark"] .login-card { background: var(--surface); border-color: var(--border); }

/* ── Feature cards ── */
[data-bs-theme="dark"] .feature-card { background: var(--surface); border-color: var(--border); }
[data-bs-theme="dark"] .feature-icon { background: rgba(99,102,241,.15); }
[data-bs-theme="dark"] .feature-icon svg { color: #818cf8; }

/* ── Tech badges ── */
[data-bs-theme="dark"] .tech-badge { background: rgba(99,102,241,.15); color: #a5b4fc; border-color: rgba(99,102,241,.3); }

/* ── Timeline ── */
/* The rail moved onto .timeline-item::after; this targeted the container rule
   that no longer exists. Both already read var(--border), so the dark-mode
   override was a no-op even before the move. */
[data-bs-theme="dark"] .timeline-item::after { background: var(--border); }
[data-bs-theme="dark"] .timeline-item::before { background: #818cf8; border-color: var(--surface); box-shadow: 0 0 0 2px #818cf8; }

/* ── Pagination ── */
/* Unconditional base rules, bound directly to the app's own design tokens
   (--surface/--text/--border/--primary/--muted) instead of leaning on
   Bootstrap's separate --bs-pagination-* variable chain. That chain only
   gets a real second look from this stylesheet for the dark-MODE toggle
   (below) -- it's never been pinned for the 5 UI presets on their own, so
   a page-link could inherit stock Bootstrap blue-on-white regardless of
   which preset (light/classic/modern/win7) was actually active, clashing
   with -- or in some preset/dark-mode combinations, nearly disappearing
   against -- the surrounding themed page. Binding straight to the same
   tokens every other themed component already uses removes that gap. */
.page-link { background: var(--surface); border-color: var(--border); color: var(--text); }
.page-link:hover { background: var(--bg); color: var(--primary); }
.page-item.active .page-link { background: var(--primary); border-color: var(--primary); color: #fff; }
.page-item.disabled .page-link { background: var(--surface); color: var(--muted); border-color: var(--border); opacity: .6; }

[data-bs-theme="dark"] .page-link { background: var(--surface); border-color: var(--border); color: #818cf8; }
[data-bs-theme="dark"] .page-link:hover { background: rgba(99,102,241,.15); color: #a5b4fc; }
[data-bs-theme="dark"] .page-item.active .page-link { background: var(--primary); border-color: var(--primary); }
[data-bs-theme="dark"] .page-item.disabled .page-link { background: var(--surface); color: #475569; border-color: var(--border); }

/* ── Breadcrumb ── */
/* ── Breadcrumb ───────────────────────────────────────────────────────────
   One trail per page, rendered by the layout (see templates/includes/
   _breadcrumb.html). Themed rather than left on Bootstrap's defaults, whose
   link colour is a fixed blue that reads as an accident on every preset whose
   --primary is not blue.

   The separator is set here rather than left to --bs-breadcrumb-divider,
   which Bootstrap ships UNDEFINED: `content: var(--bs-breadcrumb-divider, "/")`
   silently falls back to a slash and no preset can reach it. */
.hrms-breadcrumb { margin-bottom: .65rem; }
.hrms-breadcrumb .breadcrumb { margin-bottom: 0; flex-wrap: wrap; row-gap: .15rem; }
.hrms-breadcrumb .breadcrumb-item { color: var(--muted); }
.hrms-breadcrumb .breadcrumb-item + .breadcrumb-item::before {
  content: "/";
  color: var(--border);
}
.hrms-breadcrumb .breadcrumb-item > a {
  color: var(--muted);
  text-decoration: none;
  border-radius: var(--radius-xs);
  /* WCAG 2.2 target size: these measured 20px tall. A breadcrumb is standalone
     navigation, not a link inside a sentence, so 2.5.8's inline exception does
     not cover it. Inline-flex + min-height rather than padding, so the trail
     keeps its baseline and the separators stay aligned. */
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}
.hrms-breadcrumb .breadcrumb-item > a:hover,
.hrms-breadcrumb .breadcrumb-item > a:focus-visible {
  color: var(--primary-text, var(--primary));
  text-decoration: underline;
}
/* The current page: the one crumb that is not a link, so it carries the
   emphasis rather than the accent. */
.hrms-breadcrumb .breadcrumb-item.active { color: var(--text); font-weight: 600; }

[data-bs-theme="dark"] .breadcrumb-item { color: var(--muted); }
[data-bs-theme="dark"] .breadcrumb-item + .breadcrumb-item::before { color: #475569; }
[data-bs-theme="dark"] .breadcrumb-item.active { color: var(--text); }

/* ── KPI cards ── */
[data-bs-theme="dark"] .kpi-card { color: inherit; }

/* ── Semantic KPI card color variants (work across all layouts) ── */
.kpi-green  { background: #d1fae5; color: #065f46; }
.kpi-yellow { background: #fef9c3; color: #854d0e; }
.kpi-red    { background: #fee2e2; color: #991b1b; }
.kpi-indigo { background: #e0e7ff; color: #3730a3; }
.kpi-indigo2{ background: #eef2ff; color: #3730a3; }
.kpi-slate  { background: #f1f5f9; color: #0f172a; }
.kpi-cyan   { background: #cffafe; color: #155e75; }
.kpi-pink   { background: #fce7f3; color: #9d174d; }
[data-bs-theme="dark"] .kpi-green  { background: rgba(16,185,129,.2);  color: #6ee7b7; }
[data-bs-theme="dark"] .kpi-yellow { background: rgba(245,158,11,.2);  color: #fcd34d; }
[data-bs-theme="dark"] .kpi-red    { background: rgba(239,68,68,.2);   color: #fca5a5; }
[data-bs-theme="dark"] .kpi-indigo { background: rgba(99,102,241,.2);  color: #a5b4fc; }
[data-bs-theme="dark"] .kpi-indigo2{ background: rgba(99,102,241,.15); color: #a5b4fc; }
[data-bs-theme="dark"] .kpi-slate  { background: rgba(100,116,139,.2); color: #cbd5e1; }
[data-bs-theme="dark"] .kpi-cyan   { background: rgba(6,182,212,.2);   color: #67e8f9; }
[data-bs-theme="dark"] .kpi-pink   { background: rgba(236,72,153,.2);  color: #f9a8d4; }

/* ── Avatar (dark accent colours) ── */
[data-bs-theme="dark"] .avatar-primary  { background: rgba(99,102,241,.2);  color: #a5b4fc; }
[data-bs-theme="dark"] .avatar-success  { background: rgba(16,185,129,.2);  color: #6ee7b7; }
[data-bs-theme="dark"] .avatar-warning  { background: rgba(245,158,11,.2);  color: #fcd34d; }
[data-bs-theme="dark"] .avatar-danger   { background: rgba(239,68,68,.2);   color: #fca5a5; }

/* ── hr element ── */
[data-bs-theme="dark"] hr { border-color: var(--border); opacity: 1; }

/* ── Table Bootstrap default ── */
[data-bs-theme="dark"] .table { color: var(--text); --bs-table-bg: transparent; border-color: var(--border); }
[data-bs-theme="dark"] .table-hover > tbody > tr:hover > * { background: rgba(99,102,241,.07); }
[data-bs-theme="dark"] .table thead th { border-bottom-color: var(--border); }
[data-bs-theme="dark"] .table-striped > tbody > tr:nth-of-type(odd) > * { background: rgba(255,255,255,.03); }

/* ── Code / pre ── */
[data-bs-theme="dark"] code { color: #f472b6; background: rgba(244,114,182,.08); padding: .1em .35em; border-radius: .25em; }
[data-bs-theme="dark"] pre  { background: #080f1e; color: #e2e8f0; border: 1px solid var(--border); }


/* ── Employee ID Card ──────────────────────────────────────────────────── */
.emp-id-card {
  width: 324px; height: 204px; border-radius: 8px;
  border: 1px solid #c8d9ea; overflow: hidden; position: relative;
  background: #fff; box-shadow: 0 2px 14px rgba(0,0,0,.18);
  font-family: Inter, system-ui, sans-serif; user-select: none;
}
.emp-id-front { display: flex; flex-direction: column; }
.emp-id-band { background: linear-gradient(135deg,#1e40af 0%,#2563eb 60%,#0ea5e9 100%); padding: 6px 10px; flex-shrink: 0; }
.emp-id-band-inner { display: flex; align-items: center; gap: 8px; }
.emp-id-logo-box { width: 28px; height: 28px; background: rgba(255,255,255,.2); border-radius: 4px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.emp-id-company { font-size: 9px; font-weight: 800; color: #fff; letter-spacing: .06em; text-transform: uppercase; line-height: 1.2; }
.emp-id-body { display: flex; flex: 1; padding: 8px 10px 4px; gap: 10px; min-height: 0; }
.emp-id-photo-col { flex-shrink: 0; }
.emp-id-photo-frame { width: 60px; height: 72px; border-radius: 4px; border: 2px solid #e2e8f0; overflow: hidden; background: #f1f5f9; display: flex; align-items: center; justify-content: center; }
.emp-id-photo-ph { display: flex; align-items: center; justify-content: center; color: #94a3b8; }
.emp-id-info-col { flex: 1; min-width: 0; padding-top: 2px; }
.emp-id-emp-name { font-size: 10.5px; font-weight: 800; color: #0f172a; letter-spacing: -.01em; margin-bottom: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.emp-id-emp-pos { font-size: 8.5px; font-weight: 600; color: #2563eb; text-transform: uppercase; letter-spacing: .04em; margin-bottom: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.emp-id-emp-dept { font-size: 8px; color: #64748b; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.emp-id-emp-detail { display: flex; align-items: center; gap: 3px; font-size: 7.5px; color: #475569; margin-bottom: 2px; overflow: hidden; }
.emp-id-foot { background: linear-gradient(90deg,#f8fafc,#eff6ff); border-top: 1px solid #e2e8f0; padding: 5px 10px; flex-shrink: 0; }
.emp-id-foot-inner { display: flex; align-items: center; gap: 10px; }
.emp-id-qr-col { flex-shrink: 0; width: 46px; height: 46px; background: #fff; border: 1px solid #e2e8f0; border-radius: 3px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.emp-id-qr-ph { font-size: 8px; font-weight: 700; color: #94a3b8; }
.emp-id-sig-col { flex: 1; }
.emp-id-id-number { font-size: 8px; color: #64748b; margin-bottom: 10px; }
.emp-id-id-number span { font-weight: 700; color: #0f172a; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
.emp-id-sig-line { border-bottom: 1px solid #64748b; margin-bottom: 2px; }
.emp-id-sig-lbl { font-size: 7px; color: #94a3b8; text-align: center; letter-spacing: .03em; }
.emp-id-back-card { background: linear-gradient(160deg,#f8fafc,#eff6ff); display: flex; flex-direction: column; }
.emp-id-back-header { background: linear-gradient(135deg,#1e40af,#2563eb 60%,#0ea5e9 100%); color: #fff; font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; padding: 5px 10px; flex-shrink: 0; }
.emp-id-back-body { flex: 1; padding: 7px 10px 4px; display: flex; flex-direction: column; gap: 4px; overflow: hidden; }
.emp-id-bk-label { font-size: 6.5px; font-weight: 700; color: #2563eb; text-transform: uppercase; letter-spacing: .07em; margin-bottom: 1px; }
.emp-id-bk-val { font-size: 8px; color: #1e293b; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.emp-id-bk-val.fw-bold { font-weight: 700; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
.emp-id-back-footer { border-top: 1px solid #e2e8f0; padding: 4px 10px; flex-shrink: 0; }
.emp-id-bk-note { font-size: 6.5px; color: #94a3b8; text-align: center; }

/* ── Real-world Employee ID Card (Flip-card design) ──────────────────────
 *  Landscape: 340×214px  |  Portrait: 214×340px
 *  .rid-scene(.portrait) → .rid-card(.qr-on-front) → .rid-face(.rid-front/.rid-back)
 * ─────────────────────────────────────────────────────────────────────── */

/* ── Scene / card shell ─────────────────────────────────────────────── */
/* zoom (not transform:scale) so every descendant's dozens of hand-tuned
   hardcoded px values -- fonts, padding, the photo box, the QR frame,
   badge sizes -- all stay in exact proportion with zero risk of manually
   rewriting each one individually, AND the surrounding flex-centered
   layout (templates/portal/id_card.html, templates/idcards/card_detail.html)
   correctly reserves the larger box automatically (transform alone would
   leave the old, smaller layout footprint and either clip or misalign the
   now-bigger visual). Applies to every .rid-scene instance including the
   small branding-preview thumbnail in templates/idcards/card_list.html,
   whose own transform:scale() is compensated below so that one specific
   compact widget keeps its original size instead of growing too. */
.rid-scene { width: 340px; height: 214px; perspective: 1100px; display: inline-block; position: relative; zoom: 1.8; transition: width .45s cubic-bezier(.4,0,.2,1), height .45s cubic-bezier(.4,0,.2,1); }
.rid-scene.portrait { width: 214px; height: 340px; }

.rid-card { width: 100%; height: 100%; position: relative; transform-style: preserve-3d; transition: transform .7s cubic-bezier(.4,0,.2,1); }
.rid-card.is-flipped { transform: rotateY(180deg); }

/* ── Face base ──────────────────────────────────────────────────────────
   Every colour and typeface below is written as var(--rid-…, <today's
   value>). apps/idcards/styling.py emits a variable ONLY for a field the
   tenant has actually customised, so an untouched card resolves every one of
   these to the fallback and renders exactly as it always has -- including in
   dark mode, where the same property is repeated with the dark fallback. A
   customised card overrides both, which is correct: the card is a preview of
   a printed object and should not change colour with the portal theme. */
.rid-face { position: absolute; inset: 0; backface-visibility: hidden; -webkit-backface-visibility: hidden; border-radius: 13px; overflow: hidden; box-shadow: 0 12px 40px rgba(0,0,0,.22), 0 3px 10px rgba(0,0,0,.12), inset 0 1px 0 rgba(255,255,255,.12); font-family: var(--rid-font, Inter, 'Segoe UI', system-ui, sans-serif); user-select: none; display: flex; flex-direction: column; }
.rid-front { background: var(--rid-body-bg, #ffffff); border: 1px solid rgba(0,0,0,.05); }
.rid-back { background: var(--rid-body-bg, var(--rid-back-bg, linear-gradient(155deg,#eaf0fe 0%,#eff6ff 55%,#f0fdf4 100%))); transform: rotateY(180deg); border: 1px solid rgba(30,58,138,.07); }

/* ── Left accent strip (front landscape only) ───────────────────────── */
.rid-front::before { content:''; position:absolute; left:0; top:0; bottom:0; width:5px; background:linear-gradient(180deg, var(--rid-hdr,#1e3a8a) 0%, var(--rid-hdr-mid,#1d4ed8) 50%, var(--rid-hdr-end,#0284c7) 100%); z-index:2; }
.rid-scene.portrait .rid-front::before { display: none; }

/* ── Header ──────────────────────────────────────────────────────────── */
.rid-header { background: linear-gradient(125deg, var(--rid-hdr,#1e3a8a) 0%, var(--rid-hdr-mid,#2563eb) 45%, var(--rid-hdr-end,#0369a1) 100%); padding: 7px 10px 7px 16px; display: flex; align-items: center; gap: 8px; flex-shrink: 0; position: relative; overflow: hidden; }
.rid-scene.portrait .rid-header { padding: 8px 12px; }
.rid-header::before { content:''; position:absolute; inset:0; background:radial-gradient(ellipse at 85% 40%, rgba(255,255,255,.18) 0%, transparent 65%); pointer-events:none; }
.rid-header::after { content:''; position:absolute; inset:0; background:repeating-linear-gradient(-45deg, rgba(255,255,255,.04) 0px, rgba(255,255,255,.04) 1px, transparent 1px, transparent 8px); pointer-events:none; }

.rid-logo { width: 32px; height: 32px; border-radius: 7px; background: rgba(255,255,255,.18); border: 1.5px solid rgba(255,255,255,.32); display: flex; align-items: center; justify-content: center; overflow: hidden; flex-shrink: 0; position: relative; z-index: 1; }
.rid-logo img { width: 100%; height: 100%; object-fit: contain; }
/* Logo border toggle. The frame and its tint are one decision, because a
   border with no fill around a transparent-PNG logo reads as an empty box. */
.rid-scene.rid-logo-plain .rid-logo { background: transparent; border-color: transparent; }
.rid-co-block { flex: 1; min-width: 0; position: relative; z-index: 1; }
.rid-co-name { font-size: 9px; font-weight: 800; color: var(--rid-hdr-text, #fff); letter-spacing: .07em; text-transform: uppercase; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rid-co-sub { font-size: 6px; color: var(--rid-hdr-sub, rgba(255,255,255,.72)); letter-spacing: .05em; text-transform: uppercase; margin-top: 1px; }
.rid-emp-badge { font-size: 5.5px; font-weight: 800; background: rgba(255,255,255,.15); color: var(--rid-hdr-text, rgba(255,255,255,.95)); border: 1px solid rgba(255,255,255,.3); border-radius: 3px; padding: 2px 5px; letter-spacing: .1em; text-transform: uppercase; flex-shrink: 0; position: relative; z-index: 1; }

/* ── Body (landscape) ────────────────────────────────────────────────── */
.rid-body { display: flex; flex: 1; padding: 7px 10px 5px 15px; gap: 9px; overflow: hidden; }

/* Portrait body */
.rid-scene.portrait .rid-body { flex-direction: column; padding: 8px 12px 5px; gap: 6px; }

.rid-photo-col { flex-shrink: 0; display: flex; flex-direction: column; align-items: center; }
/* Portrait photo col */
.rid-scene.portrait .rid-photo-col { flex-direction: row; width: 100%; justify-content: center; align-items: flex-start; gap: 10px; }

.rid-photo { width: 84px; height: 84px; border-radius: 6px; border: 1px solid rgba(191,219,254,.45); overflow: hidden; background: #f1f5f9; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(37,99,235,.12); }
.rid-scene.portrait .rid-photo { width: 100px; height: 100px; }
.rid-photo img { width: 100%; height: 100%; object-fit: cover; }
.rid-photo-ph { color: #94a3b8; }

.rid-sig-area { width: 66px; margin-top: auto; padding-top: 4px; }
.rid-scene.portrait .rid-sig-area { display: none; }
.rid-sig-line { border-bottom: 1.5px solid #cbd5e1; margin-bottom: 2px; }
.rid-sig-lbl { font-size: 5px; color: #94a3b8; text-align: center; letter-spacing: .04em; text-transform: uppercase; }

.rid-info { flex: 1; min-width: 0; padding-top: 1px; display: flex; flex-direction: column; overflow: hidden; }
.rid-scene.portrait .rid-info { align-items: center; text-align: center; }
.rid-scene.portrait .rid-name,
.rid-scene.portrait .rid-pos,
.rid-scene.portrait .rid-dept { text-align: center; }
.rid-scene.portrait .rid-info-divider { margin-left: auto; margin-right: auto; }
.rid-scene.portrait .rid-row { justify-content: center; }
/* Per-field typography. An unset family resolves to --rid-font (the card
   font) rather than to a literal, so changing the card font moves every role
   that has not been given one of its own. */
.rid-name { font-family: var(--rid-name-font, var(--rid-font, inherit)); font-size: 11.5px; font-weight: var(--rid-name-weight, 900); font-style: var(--rid-name-style, normal); text-transform: var(--rid-name-transform, none); color: var(--rid-name-color, #0f172a); letter-spacing: -.02em; margin-bottom: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rid-pos { font-family: var(--rid-pos-font, var(--rid-font, inherit)); font-size: 7.5px; font-weight: var(--rid-pos-weight, 700); font-style: var(--rid-pos-style, normal); color: var(--rid-pos-color, #1d4ed8); text-transform: var(--rid-pos-transform, uppercase); letter-spacing: .06em; margin-bottom: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rid-dept { font-family: var(--rid-detail-font, var(--rid-font, inherit)); font-size: 7px; font-weight: var(--rid-detail-weight, 400); font-style: var(--rid-detail-style, normal); color: var(--rid-detail-color, #64748b); margin-bottom: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rid-info-divider { width: 22px; height: 2px; background: linear-gradient(90deg, var(--rid-hdr-mid,#1d4ed8), var(--rid-hdr-end,#0284c7), transparent); border-radius: 2px; margin-bottom: 4px; flex-shrink: 0; }

/* ════════════════════════════════════════════════════════════════════════
   ID Card Colour Themes
   Apply the class  .rid-theme-<name>  on the .rid-scene (or any ancestor).
   Default (blue corporate) needs no extra class.
   ════════════════════════════════════════════════════════════════════════ */

/* A theme now DECLARES the palette rather than painting it. The rules above
   consume the same variables, so a tenant's own colour -- emitted inline on
   the .rid-scene by apps/idcards/styling.py -- overrides a theme without any
   specificity contest: an inline custom property always beats one declared by
   a class on the same element.
   This also repairs three rules that never matched anything. Each theme
   carried `.rid-theme-x .rid-scene:not(.portrait) .rid-foot::before`, which
   requires .rid-scene to be a DESCENDANT of the theme class -- but the theme
   class is applied TO the scene, so no theme has ever recoloured the footer
   accent tab. As a variable it simply works. */

/* ── Emerald Professional ── */
.rid-theme-emerald {
  --rid-hdr: #064e3b; --rid-hdr-mid: #059669; --rid-hdr-end: #0d9488;
  --rid-accent: #f59e0b; --rid-accent-end: #d97706;
  --rid-pos-color: #065f46; --rid-label-color: #059669;
  --rid-foot-border: #059669;
  --rid-back-bg: linear-gradient(155deg,#ecfdf5 0%,#f0fdfa 55%,#f0fdf4 100%);
}
.rid-theme-emerald .rid-qr-frame      { border-color: #a7f3d0; box-shadow: 0 2px 10px rgba(5,150,105,.18), 0 0 0 3px rgba(167,243,208,.5); }
.rid-theme-emerald .rid-back          { border-color: rgba(5,150,105,.1); }

/* ── Midnight Slate ── */
.rid-theme-midnight {
  --rid-hdr: #0f172a; --rid-hdr-mid: #1e293b; --rid-hdr-end: #334155;
  --rid-accent: #7c3aed; --rid-accent-end: #6366f1;
  --rid-pos-color: #7c3aed; --rid-label-color: #7c3aed;
  --rid-foot-border: #7c3aed;
  --rid-back-bg: linear-gradient(155deg,#f5f3ff 0%,#ede9fe 30%,#f0f4ff 100%);
}
.rid-theme-midnight .rid-qr-frame     { border-color: #ddd6fe; box-shadow: 0 2px 10px rgba(124,58,237,.18), 0 0 0 3px rgba(221,214,254,.5); }
.rid-theme-midnight .rid-photo        { border-color: #ddd6fe; box-shadow: 0 2px 10px rgba(124,58,237,.18), 0 0 0 4px rgba(237,233,254,.45); }
.rid-theme-midnight .rid-back         { border-color: rgba(124,58,237,.1); }

/* ── Ruby Enterprise ── */
.rid-theme-ruby {
  --rid-hdr: #7f1d1d; --rid-hdr-mid: #dc2626; --rid-hdr-end: #e11d48;
  --rid-accent: #f97316; --rid-accent-end: #f59e0b;
  --rid-pos-color: #be123c; --rid-label-color: #dc2626;
  --rid-foot-border: #dc2626;
  --rid-back-bg: linear-gradient(155deg,#fff1f2 0%,#ffe4e6 30%,#fef2f2 100%);
}
.rid-theme-ruby .rid-qr-frame         { border-color: #fecaca; box-shadow: 0 2px 10px rgba(220,38,38,.18), 0 0 0 3px rgba(254,202,202,.5); }
.rid-theme-ruby .rid-photo            { border-color: #fecaca; box-shadow: 0 2px 10px rgba(220,38,38,.18), 0 0 0 4px rgba(254,226,226,.45); }
.rid-theme-ruby .rid-back             { border-color: rgba(220,38,38,.1); }

/* ── Violet Modern ── */
.rid-theme-violet {
  --rid-hdr: #4c1d95; --rid-hdr-mid: #7c3aed; --rid-hdr-end: #6366f1;
  --rid-accent: #06b6d4; --rid-accent-end: #0891b2;
  --rid-pos-color: #6d28d9; --rid-label-color: #7c3aed;
  --rid-foot-border: #7c3aed;
  --rid-back-bg: linear-gradient(155deg,#faf5ff 0%,#ede9fe 30%,#eef2ff 100%);
}
.rid-theme-violet .rid-qr-frame       { border-color: #ddd6fe; box-shadow: 0 2px 10px rgba(124,58,237,.18), 0 0 0 3px rgba(221,214,254,.5); }
.rid-theme-violet .rid-photo          { border-color: #ddd6fe; box-shadow: 0 2px 10px rgba(124,58,237,.18), 0 0 0 4px rgba(237,233,254,.45); }
.rid-theme-violet .rid-back           { border-color: rgba(99,102,241,.1); }
.rid-row { display: flex; align-items: flex-start; gap: 3px; font-family: var(--rid-detail-font, var(--rid-font, inherit)); font-size: 7px; font-weight: var(--rid-detail-weight, 400); font-style: var(--rid-detail-style, normal); color: var(--rid-detail-color, #475569); margin-bottom: 2.5px; overflow: hidden; }
/* Decoration, so it keeps its own variable rather than following the position
   colour -- themes set that to a dark value and the icon would stop reading as
   an icon. */
.rid-row-icon { flex-shrink: 0; color: var(--rid-icon-color, #93c5fd); margin-top: .5px; }
.rid-row-val { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.rid-row-val.addr { white-space: normal; line-height: 1.35; max-height: 2.7em; overflow: hidden; }

/* ── Front QR (shown when qr-on-front) ──────────────────────────────── */
.rid-front-qr { display: none; flex-shrink: 0; flex-direction: column; align-items: center; justify-content: center; gap: 3px; padding-top: 2px; }
.rid-card.qr-on-front .rid-front-qr { display: flex; }
.rid-card.qr-on-front .rid-back .rid-qr-zone { visibility: hidden; }
.rid-front-qr .rid-qr-frame { width: 52px; height: 52px; }
.rid-front-qr .rid-qr-lbl { font-size: 5px; color: #94a3b8; text-align: center; letter-spacing: .04em; text-transform: uppercase; }
/* Portrait qr-on-front */
.rid-scene.portrait .rid-card.qr-on-front .rid-body { flex-direction: row; }
.rid-scene.portrait .rid-card.qr-on-front .rid-photo-col { flex-direction: column; align-items: center; justify-content: flex-start; }
.rid-scene.portrait .rid-card.qr-on-front .rid-front-qr { padding-top: 6px; }
.rid-scene.portrait .rid-card.qr-on-front .rid-front-qr .rid-qr-frame { width: 56px; height: 56px; }

/* ── Footer ─────────────────────────────────────────────────────────── */
.rid-foot { background: var(--rid-foot-bg, linear-gradient(90deg,#f8fafc,#eff6ff 100%)); border-top: 1px solid var(--rid-foot-border, #e2e8f0); padding: 4px 10px 4px 15px; display: flex; align-items: center; gap: 8px; flex-shrink: 0; position: relative; }
.rid-foot::before { content:''; position:absolute; left:0; top:0; bottom:0; width:5px; background:linear-gradient(180deg, var(--rid-accent,#fbbf24) 0%, var(--rid-accent-end,#f59e0b) 100%); }
.rid-scene.portrait .rid-foot { padding: 4px 12px; border-top: 2px solid var(--rid-foot-border, #1d4ed8); justify-content: center; gap: 12px; }
.rid-scene.portrait .rid-foot::before { display: none; }
.rid-foot-idno { font-family: var(--rid-detail-font, var(--rid-font, inherit)); font-size: 7.5px; color: var(--rid-foot-text, #64748b); }
.rid-foot-idno strong { color: var(--rid-foot-strong, #0f172a); font-family: ui-monospace, SFMono-Regular, monospace; font-size: 8px; }
.rid-foot-valid { margin-left: auto; font-family: var(--rid-detail-font, var(--rid-font, inherit)); font-size: 6px; color: var(--rid-foot-text, #94a3b8); text-align: right; line-height: 1.5; }
.rid-scene.portrait .rid-foot-valid { margin-left: 0; text-align: center; }

/* ── Back header ──────────────────────────────────────────────────────── */
.rid-back-header { background: linear-gradient(125deg, var(--rid-hdr,#1e3a8a) 0%, var(--rid-hdr-mid,#2563eb) 45%, var(--rid-hdr-end,#0369a1) 100%); color: var(--rid-hdr-text, #fff); font-size: 8.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .09em; padding: 7px 12px; display: flex; align-items: center; gap: 8px; flex-shrink: 0; position: relative; overflow: hidden; }
.rid-back-header::before { content:''; position:absolute; inset:0; background:radial-gradient(ellipse at 85% 40%, rgba(255,255,255,.18) 0%, transparent 65%); pointer-events:none; }
.rid-back-header::after { content:''; position:absolute; inset:0; background:repeating-linear-gradient(-45deg, rgba(255,255,255,.04) 0px, rgba(255,255,255,.04) 1px, transparent 1px, transparent 8px); pointer-events:none; }
.rid-back-header .rid-logo { width: 26px; height: 26px; z-index: 1; }
.rid-back-header > span { position: relative; z-index: 1; }

/* Magnetic stripe (decorative) */
.rid-back-stripe { height: 22px; background: linear-gradient(90deg,#111827 0%,#1e293b 30%,#111827 100%); flex-shrink: 0; position: relative; overflow: hidden; }
.rid-back-stripe::after { content:''; position:absolute; inset:0; background:linear-gradient(90deg, transparent 0%, rgba(255,255,255,.07) 25%, rgba(255,255,255,.13) 50%, rgba(255,255,255,.07) 75%, transparent 100%); }

/* ── Back body ────────────────────────────────────────────────────────── */
.rid-back-body { flex: 1; padding: 7px 12px 5px; display: flex; align-items: center; gap: 12px; overflow: hidden; }
.rid-scene.portrait .rid-back-body { flex-direction: column; align-items: center; padding: 10px 12px; gap: 8px; }
.rid-qr-zone { flex-shrink: 0; display: flex; flex-direction: column; align-items: center; gap: 3px; }
.rid-qr-frame { width: 76px; height: 76px; background: #fff; border-radius: 8px; display: flex; align-items: center; justify-content: center; overflow: hidden; padding: 4px; box-shadow: 0 2px 10px rgba(37,99,235,.15); }
.rid-scene.portrait .rid-qr-frame { width: 90px; height: 90px; }
.rid-qr-frame img { width: 100%; height: 100%; object-fit: contain; }
.rid-qr-ph { font-size: 8px; font-weight: 700; color: #94a3b8; }
.rid-qr-lbl { font-size: 5px; color: #94a3b8; text-align: center; letter-spacing: .05em; text-transform: uppercase; }
.rid-back-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.rid-scene.portrait .rid-back-info { width: 100%; flex-direction: row; flex-wrap: wrap; gap: 6px 14px; }
.rid-bk-section {}
.rid-scene.portrait .rid-bk-section { flex: 1; min-width: 80px; }
.rid-bk-label { font-family: var(--rid-detail-font, var(--rid-font, inherit)); font-size: 5.5px; font-weight: 700; color: var(--rid-label-color, #1d4ed8); text-transform: uppercase; letter-spacing: .09em; margin-bottom: 2px; }
.rid-bk-val { font-family: var(--rid-detail-font, var(--rid-font, inherit)); font-size: 8px; font-weight: var(--rid-detail-weight, 400); font-style: var(--rid-detail-style, normal); color: var(--rid-detail-color, #1e293b); line-height: 1.4; }
.rid-bk-val.mono { font-family: ui-monospace, SFMono-Regular, monospace; font-weight: 700; color: var(--rid-detail-color, #0f172a); }
.rid-back-foot { border-top: 1px solid #dde6f5; padding: 4px 12px; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
.rid-back-note { font-size: 5.5px; color: #94a3b8; line-height: 1.4; }

/* ── Dark theme ──────────────────────────────────────────────────────────
   Every property a tenant can customise is repeated here as the SAME variable
   with the dark fallback. Written as a flat `color: #f1f5f9` these rules win
   on specificity, so a tenant's colour would apply in light mode and be
   silently discarded in dark -- the branding panel showing one card and the
   employee's screen another. With the variable in place an untouched card
   keeps its dark styling and a customised one renders identically in both,
   which is what a preview of a printed object should do. */
[data-bs-theme="dark"] .rid-front { background: var(--rid-body-bg, #1e293b); border-color: rgba(255,255,255,.05); }
[data-bs-theme="dark"] .rid-back { background: var(--rid-body-bg, var(--rid-back-bg, linear-gradient(155deg,#1a2540 0%,#162032 55%,#14213d 100%))); border-color: rgba(99,155,255,.07); }
[data-bs-theme="dark"] .rid-front::before { background: linear-gradient(180deg, var(--rid-hdr,#1e40af), var(--rid-hdr-mid,#2563eb) 50%, var(--rid-hdr-end,#0284c7) 100%); }
[data-bs-theme="dark"] .rid-name { color: var(--rid-name-color, #f1f5f9); }
[data-bs-theme="dark"] .rid-pos { color: var(--rid-pos-color, #60a5fa); }
[data-bs-theme="dark"] .rid-dept { color: var(--rid-detail-color, #94a3b8); }
[data-bs-theme="dark"] .rid-row { color: var(--rid-detail-color, #94a3b8); }
[data-bs-theme="dark"] .rid-row-icon { color: var(--rid-icon-color, #3b82f6); }
[data-bs-theme="dark"] .rid-info-divider { background: linear-gradient(90deg, var(--rid-hdr-mid,#3b82f6), var(--rid-hdr-end,#0284c7), transparent); }
[data-bs-theme="dark"] .rid-foot { background: var(--rid-foot-bg, linear-gradient(90deg,#1e293b,#172033 70%)); border-color: var(--rid-foot-border, #334155); }
[data-bs-theme="dark"] .rid-foot::before { background: linear-gradient(180deg, var(--rid-accent,#d97706), var(--rid-accent-end,#b45309)); }
[data-bs-theme="dark"] .rid-foot-idno { color: var(--rid-foot-text, #94a3b8); }
[data-bs-theme="dark"] .rid-foot-idno strong { color: var(--rid-foot-strong, #f1f5f9); }
[data-bs-theme="dark"] .rid-foot-valid { color: var(--rid-foot-text, #64748b); }
[data-bs-theme="dark"] .rid-back-note, [data-bs-theme="dark"] .rid-sig-lbl { color: #64748b; }
[data-bs-theme="dark"] .rid-qr-frame { background: #fff; }
[data-bs-theme="dark"] .rid-bk-val { color: var(--rid-detail-color, #cbd5e1); }
[data-bs-theme="dark"] .rid-bk-val.mono { color: var(--rid-detail-color, #f1f5f9); }
[data-bs-theme="dark"] .rid-bk-label { color: var(--rid-label-color, #60a5fa); }
[data-bs-theme="dark"] .rid-back-foot { border-color: #334155; }
[data-bs-theme="dark"] .rid-photo { border-color: #334155; background: #0f172a; }
[data-bs-theme="dark"] .rid-sig-line { border-color: #475569; }
[data-bs-theme="dark"] .rid-back-stripe { background: linear-gradient(90deg,#020617,#0f172a 30%,#020617 100%); }
[data-bs-theme="dark"] .rid-front-qr .rid-qr-lbl { color: #475569; }

/* ════════════════════════════════════════════════════════════════════════
   Header & footer band designs
   ------------------------------------------------------------------------
   Selected per tenant; the catalogue and each band's surface colour live in
   apps/idcards/styling.py, which is also what decides the text colour that
   goes on it. Scoped `.rid-scene.rid-hdr-*` so a band beats both the base
   rule and a theme without needing !important.

   The last two of each set paint a PALE surface, which is where a header
   design turns into a visibility bug: every default on this card assumes a
   dark band, from the white company name down to the white stroke on the
   placeholder logo icon. Each pale band therefore restates all of them, and
   apps/idcards/styling.py flags the same bands as light so the contrast
   arithmetic is done against the surface the text really lands on.
   ════════════════════════════════════════════════════════════════════════ */

/* ── Header: dark bands ── */
.rid-scene.rid-hdr-gradient .rid-header,
.rid-scene.rid-hdr-gradient .rid-back-header {
  background: linear-gradient(125deg, var(--rid-hdr,#1e3a8a) 0%, var(--rid-hdr-mid,#2563eb) 45%, var(--rid-hdr-end,#0369a1) 100%);
}
.rid-scene.rid-hdr-solid .rid-header,
.rid-scene.rid-hdr-solid .rid-back-header { background: var(--rid-hdr,#1e3a8a); }

/* Two tones of the SAME ramp meeting on a hard diagonal. Deliberately not
   the accent colour: an accent is chosen to stand out against the brand, so
   half the header would become a surface nobody checked the text against. */
.rid-scene.rid-hdr-split .rid-header,
.rid-scene.rid-hdr-split .rid-back-header {
  background: linear-gradient(108deg, var(--rid-hdr,#1e3a8a) 0 56%, var(--rid-hdr-end,#0369a1) 56.2% 100%);
}
.rid-scene.rid-hdr-bar .rid-header,
.rid-scene.rid-hdr-bar .rid-back-header {
  background: var(--rid-hdr,#1e3a8a);
  box-shadow: inset 0 -3px 0 var(--rid-accent,#f59e0b);
}
.rid-scene.rid-hdr-wave .rid-header,
.rid-scene.rid-hdr-wave .rid-back-header {
  background: linear-gradient(125deg, var(--rid-hdr,#1e3a8a) 0%, var(--rid-hdr-mid,#2563eb) 45%, var(--rid-hdr-end,#0369a1) 100%);
  border-bottom-right-radius: 26px;
}
.rid-scene.rid-hdr-frame .rid-header,
.rid-scene.rid-hdr-frame .rid-back-header {
  background: var(--rid-hdr,#1e3a8a);
  outline: 1px solid rgba(255,255,255,.42);
  outline-offset: -4px;
}

/* ── Header: pale bands ── */
.rid-scene.rid-hdr-minimal .rid-header,
.rid-scene.rid-hdr-minimal .rid-back-header {
  background: var(--rid-body-bg, #ffffff);
  border-bottom: 2.5px solid var(--rid-hdr,#1e3a8a);
}
.rid-scene.rid-hdr-outline .rid-header,
.rid-scene.rid-hdr-outline .rid-back-header {
  background: var(--rid-body-bg, #ffffff);
  box-shadow: inset 0 0 0 2px var(--rid-hdr,#1e3a8a);
}
/* The two decorative overlays are white-on-white here, and the badge, logo
   frame and placeholder icon are all drawn in white. */
.rid-scene.rid-hdr-minimal .rid-header::before,
.rid-scene.rid-hdr-minimal .rid-header::after,
.rid-scene.rid-hdr-minimal .rid-back-header::before,
.rid-scene.rid-hdr-minimal .rid-back-header::after,
.rid-scene.rid-hdr-outline .rid-header::before,
.rid-scene.rid-hdr-outline .rid-header::after,
.rid-scene.rid-hdr-outline .rid-back-header::before,
.rid-scene.rid-hdr-outline .rid-back-header::after { display: none; }

.rid-scene.rid-hdr-minimal .rid-co-name,
.rid-scene.rid-hdr-outline .rid-co-name,
.rid-scene.rid-hdr-minimal .rid-back-header,
.rid-scene.rid-hdr-outline .rid-back-header { color: var(--rid-hdr-text, #0f172a); }
.rid-scene.rid-hdr-minimal .rid-co-sub,
.rid-scene.rid-hdr-outline .rid-co-sub { color: var(--rid-hdr-sub, #64748b); }
/* Inverted: the chip becomes the coloured element so it still reads as one. */
.rid-scene.rid-hdr-minimal .rid-emp-badge,
.rid-scene.rid-hdr-outline .rid-emp-badge {
  background: var(--rid-hdr,#1e3a8a); color: #ffffff; border-color: transparent;
}
.rid-scene.rid-hdr-minimal .rid-logo,
.rid-scene.rid-hdr-outline .rid-logo {
  background: rgba(15,23,42,.05); border-color: rgba(15,23,42,.14);
}
.rid-scene.rid-hdr-minimal.rid-logo-plain .rid-logo,
.rid-scene.rid-hdr-outline.rid-logo-plain .rid-logo {
  background: transparent; border-color: transparent;
}
/* `stroke` on the markup is a presentation attribute, which any stylesheet
   rule outranks -- so the placeholder icon can be recoloured without touching
   the three templates that carry it. */
.rid-scene.rid-hdr-minimal .rid-logo svg,
.rid-scene.rid-hdr-outline .rid-logo svg { stroke: #94a3b8; }

/* ── Footer bands ──
   "light" is the shipped strip and needs no rule. The filled bands get their
   surface from styling.py, which also emits the text colour that reads on it. */
.rid-scene.rid-foot-solid .rid-foot,
.rid-scene.rid-foot-accent .rid-foot,
.rid-scene.rid-foot-dark .rid-foot {
  background: var(--rid-foot-bg, #1e3a8a);
  border-top-color: transparent;
}
/* The accent tab is invisible against a band already painted in that colour,
   so it becomes a light sliver instead of disappearing. */
.rid-scene.rid-foot-solid .rid-foot::before,
.rid-scene.rid-foot-accent .rid-foot::before,
.rid-scene.rid-foot-dark .rid-foot::before { background: rgba(255,255,255,.34); }

.rid-scene.rid-foot-minimal .rid-foot {
  background: var(--rid-body-bg, transparent);
  border-top: 1px solid rgba(15,23,42,.12);
}
.rid-scene.rid-foot-minimal .rid-foot::before { display: none; }

/* The stripe is a band ABOVE the strip rather than behind the text: a
   repeating gradient under an ID number is unreadable at any colour. */
.rid-scene.rid-foot-stripe .rid-foot {
  background: var(--rid-body-bg, #ffffff);
  border-top: none;
}
.rid-scene.rid-foot-stripe .rid-foot::before {
  left: 0; right: 0; top: 0; bottom: auto; width: auto; height: 3px;
  background: repeating-linear-gradient(115deg,
    var(--rid-accent,#f59e0b) 0 5px, var(--rid-accent-end,#d97706) 5px 10px);
}

/* ════════════════════════════════════════════════════════════════════════
   Card layouts
   ------------------------------------------------------------------------
   Hoisted out of templates/idcards/card_list.html, where they existed twice
   (once per preview scene, both with !important) and applied to the branding
   preview ONLY -- picking a layout changed the thumbnail and never the card
   that got printed. Placed after the portrait rules so the two tie on
   specificity and the layout wins, which is what "compact" has to do to a
   portrait card.
   ════════════════════════════════════════════════════════════════════════ */

/* COMPACT — dense, data-rich, tighter spacing */
.rid-scene.rid-layout-compact .rid-header  { padding: 5px 8px; }
.rid-scene.rid-layout-compact .rid-co-name { font-size: 7.5px; }
.rid-scene.rid-layout-compact .rid-co-sub  { font-size: 5px; }
.rid-scene.rid-layout-compact .rid-photo   { width: 46px; height: 58px; }
.rid-scene.rid-layout-compact .rid-name    { font-size: 9px; }
.rid-scene.rid-layout-compact .rid-pos     { font-size: 7px; }
.rid-scene.rid-layout-compact .rid-dept    { font-size: 6.5px; }
.rid-scene.rid-layout-compact .rid-body    { padding: 5px 7px; gap: 6px; }
.rid-scene.rid-layout-compact .rid-foot    { padding: 4px 8px; }
.rid-scene.rid-layout-compact .rid-logo    { width: 22px; height: 22px; }

/* BOLD — strong name, circular photo, accent ring */
.rid-scene.rid-layout-bold .rid-photo { width: 64px; height: 64px; border-radius: 50%; box-shadow: 0 0 0 3px var(--rid-accent,#f59e0b); }
.rid-scene.rid-layout-bold .rid-name  { font-size: 14px; font-weight: var(--rid-name-weight, 900); letter-spacing: -.02em; }
.rid-scene.rid-layout-bold .rid-pos   { font-size: 8px; font-weight: var(--rid-pos-weight, 700); color: var(--rid-pos-color, var(--rid-hdr, #1e3a8a)); letter-spacing: .08em; }
.rid-scene.rid-layout-bold .rid-front::before { width: 7px; }
.rid-scene.rid-layout-bold .rid-header { padding-top: 10px; padding-bottom: 10px; }
.rid-scene.rid-layout-bold .rid-info-divider { height: 3px; width: 28px; }

/* MODERN — centred info, no left stripe */
.rid-scene.rid-layout-modern .rid-front::before { display: none; }
.rid-scene.rid-layout-modern .rid-info { align-items: center; text-align: center; }
.rid-scene.rid-layout-modern .rid-info-divider { margin-left: auto; margin-right: auto; width: 32px; }
.rid-scene.rid-layout-modern .rid-row  { justify-content: center; }
.rid-scene.rid-layout-modern .rid-name { font-weight: var(--rid-name-weight, 700); letter-spacing: -.01em; }
.rid-scene.rid-layout-modern .rid-photo { border-radius: 10px; }
.rid-scene.rid-layout-modern .rid-foot::before { opacity: .7; }

/* COVER — photo on the right, larger */
.rid-scene.rid-layout-cover .rid-photo-col { order: 2; }
.rid-scene.rid-layout-cover .rid-info { order: 1; }
.rid-scene.rid-layout-cover .rid-photo { width: 72px; height: 88px; border-radius: 8px; }
.rid-scene.rid-layout-cover .rid-name { font-size: 11.5px; font-weight: var(--rid-name-weight, 800); }
.rid-scene.rid-layout-cover .rid-body { padding-left: 6px; }
.rid-scene.rid-layout-cover .rid-front::before { background: linear-gradient(180deg, var(--rid-accent,#f59e0b) 0%, var(--rid-accent-end,#d97706) 100%); }


/* ════════════════════════════════════════════════════════════════════════
   Artwork, and text that stays readable over it
   ------------------------------------------------------------------------
   MOVED HERE from templates/idcards/card_detail.html and card_list.html,
   which each carried their own copy.

   That duplication is the single most expensive thing in this area. The same
   defect has been reported and fixed three times because a fix landed in one
   copy: the artwork stacking rule (the design painted over the photo in the
   preview), the back face's own class names (a scrim added under text that
   stayed dark, making it worse), and the container names (.rid-info-zone
   exists only on the print sheet, so on-screen white text landed on a pale
   design with no backing at all).

   One copy for everything drawn on screen. templates/idcards/card_print.html
   keeps its own because it is a standalone print document that never loads
   this stylesheet; a test asserts the two agree.
   ════════════════════════════════════════════════════════════════════════ */
/* ── Artwork layer ────────────────────────────────────────────────────────
   Mirrors card_print.html: the chosen design is the full-bleed background and
   every data zone sits above it. This page had no artwork support at all, so
   setting a design as the default changed the printable sheet and nothing an
   administrator normally looks at -- which reads as "artwork does nothing".
   The theme background still shows when no design is chosen. */
.rid-artwork {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 0;
}
/* Data zones sit above the artwork. */
.rid-face > *:not(.rid-artwork) { position: relative; z-index: 1; }
/* Over artwork the header band is translucent rather than opaque, and the
   signature rules lighten, so the design stays visible underneath. */
.has-artwork .rid-header { background-color: rgba(0,0,0,.28); }
.has-artwork .rid-sig-line { border-bottom-color: rgba(255,255,255,.5); }
.has-artwork .rid-sig-lbl { color: rgba(255,255,255,.65); }

/* ── Text over artwork: readable on ANY design ────────────────────────────
   The reported defect: on a dark navy artwork the name, position, department,
   contact rows and the whole back face were effectively invisible.

   Cause: every text colour here is a fixed DARK value (--rid-name #0f172a,
   rows #475569, dept #64748b) chosen for the pale theme backgrounds, and the
   only concession to artwork was a WHITE text-shadow -- which assumes the
   artwork is light. Over dark artwork that is dark text wearing a white glow,
   which is what the screenshots show.

   Fix: do not try to guess the artwork's colour. Artwork is an arbitrary
   uploaded image, often a photograph or a gradient, and there is no single
   colour to sample -- a design can be white at the top and black at the
   bottom. Instead put a known surface UNDER the text and colour the text
   against that surface.

   The scrim is 70% #0f172a. Measured at both extremes, since the artwork
   underneath can be anything:

       white artwork -> effective #575d6a -> white text 6.61:1
       black artwork -> effective #0a101d -> white text 19.01:1

   so every value below clears AA whatever the design does. The accent had to
   lighten too: the stock #93c5fd measured 3.66:1 on the worst case. */
.has-artwork .rid-info-zone,
.has-artwork .rid-back-info-zone,
/* The three markups name the SAME container differently -- the print sheet
   uses -zone, the on-screen card and the branding preview do not. Listing only
   the print names is what shipped white text with no backing behind it onto a
   pale design: the colour rules matched (they target .rid-name and friends,
   which exist everywhere) while the scrim matched nothing at all. */
.has-artwork .rid-info,
.has-artwork .rid-back-info {
  background: rgba(15, 23, 42, .70);
  border-radius: 4px;
  padding: 3px 4px;
  /* Nothing is clipped: the scrim sits behind the text it already contains. */
  backdrop-filter: none;
}

/* The -aw variables carry a TENANT'S own colour over artwork, and
   apps/idcards/styling.py emits one only where it measured that colour clear
   of AA against #575d6a, the lightest surface the scrim can produce. A colour
   that fails is simply not emitted, so the audited value below renders and the
   operator is told which of their choices did not travel -- the alternative
   is honouring a picker into an unreadable card, which is the bug this whole
   block exists to fix. */
.has-artwork .rid-name     { color: var(--rid-name-color-aw, #ffffff); }   /* 6.61:1 worst case */
.has-artwork .rid-pos      { color: var(--rid-pos-color-aw, #DBEAFE); }    /* 5.41:1 */
.has-artwork .rid-dept,
.has-artwork .rid-row,
.has-artwork .rid-emp-no,
.has-artwork .rid-row-val  { color: var(--rid-detail-color-aw, #E2E8F0); } /* 5.36:1 */
.has-artwork .rid-row-icon { color: #BFDBFE; }            /* 4.65:1, non-text */

/* The old shadow assumed light artwork and fights light text. Replaced with a
   dark one, which deepens the scrim rather than cancelling it. */
.has-artwork .rid-name,
.has-artwork .rid-emp-no,
.has-artwork .rid-dept,
.has-artwork .rid-row {
  text-shadow: 0 1px 2px rgba(0, 0, 0, .55);
}

/* The divider is decoration, not text, but it disappears against a dark
   scrim in its dark-blue form. */
.has-artwork .rid-info-divider {
  background: linear-gradient(90deg, #BFDBFE, transparent);
}
/* ── Back face text over artwork ──────────────────────────────────────────
   The back uses a DIFFERENT set of class names from the front
   (.rid-bk-label / .rid-bk-val / .rid-qr-lbl rather than .rid-name / .rid-row),
   so the front-face overrides above did not reach it. The scrim landed on
   .rid-back-info-zone while the text stayed at its dark theme values --
   #1e293b for values, #0f172a for the monospaced employee number, and a dark
   theme accent for the labels -- which made the back WORSE than before the
   scrim, not better. Reported, correctly, as still unreadable.

   Same verified palette as the front, against the same 70% scrim.

   The FOOTER is deliberately untouched: it draws its own light gradient band
   (with opacity .9 over artwork), so its dark text sits on a known light
   surface and is already legible. Lightening it would break the one part of
   the back that worked. */
.has-artwork .rid-bk-label  { color: var(--rid-label-color-aw, #DBEAFE); }  /* 5.41:1 worst case */
.has-artwork .rid-bk-val    { color: var(--rid-detail-color-aw, #E2E8F0); } /* 5.36:1 */
.has-artwork .rid-bk-val.mono { color: #ffffff; }         /* 6.61:1 */
.has-artwork .rid-qr-lbl    { color: #E2E8F0; }           /* sits on artwork,
                                                             outside the white
                                                             QR frame */
.has-artwork .rid-bk-label,
.has-artwork .rid-bk-val,
.has-artwork .rid-qr-lbl {
  text-shadow: 0 1px 2px rgba(0, 0, 0, .55);
}

/* Door open toast indicator (tap simulator) */
@keyframes door-slide-in { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes door-slide-out { from { transform: translateY(0); opacity: 1; } to { transform: translateY(100%); opacity: 0; } }
.door-open-toast { position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%); z-index: 2000; min-width: 320px; max-width: 480px; background: linear-gradient(135deg,#14532d,#15803d); color: #fff; border-radius: 1rem; padding: 1rem 1.5rem; box-shadow: 0 8px 32px rgba(21,128,61,.45), 0 2px 8px rgba(0,0,0,.2); display: flex; align-items: center; gap: .85rem; animation: door-slide-in .35s cubic-bezier(.34,1.56,.64,1); }
.door-open-toast.hiding { animation: door-slide-out .35s ease forwards; }
.door-open-toast-icon { font-size: 2rem; flex-shrink: 0; line-height: 1; }
.door-open-toast-body { flex: 1; min-width: 0; }
.door-open-toast-title { font-size: 1rem; font-weight: 800; letter-spacing: -.01em; margin-bottom: .15rem; }
.door-open-toast-sub { font-size: .78rem; opacity: .82; }

/* ── Nav hover dropdowns (desktop only) ──────────────────────────────── */
@media (min-width: 992px) {
  .nav-hover-dd { position: relative; }
  .nav-hover-dd > .nav-hover-menu {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    /* Was 13rem. Lowered with the type size in the compact block below: at
       13px a 13rem floor is no longer a floor, it is padding — the menu was
       held 24px wider than its longest item. This is the ONLY min-width for
       these menus on purpose; the compact block sets no second one, because
       this selector is more specific and would silently win over it. */
    min-width: 11.5rem;
    margin-top: 0 !important;
    /* A floating panel needs an EDGE or it dissolves into the page. This one
       had only the accent top rule and relied on the drop shadow for its other
       three sides — which is fine until a preset whose --shadow is `none`
       (Flat) is selected and the menu loses its outline entirely. The border
       is unconditional rather than flat-scoped: a hairline is an improvement
       on every preset, and a shadow that is load-bearing for legibility is a
       bug wherever it appears. */
    border: var(--border-width) solid var(--border);
    border-top: 2px solid var(--primary, #4f46e5);
    border-radius: 0 0 var(--radius-sm) var(--radius-sm);
    box-shadow: var(--shadow-lg) !important;
    animation: navHoverIn .16s cubic-bezier(.22,1,.36,1);
  }
  /* `.show` is left behind by the mobile accordion. Without this, resizing
     a phone-width window up to desktop leaves that submenu floating open
     over the page with no hover to dismiss it -- `.collapse.show` is
     `display:block`, and up here the menu is `position:absolute`. */
  .nav-hover-dd > .nav-hover-menu.collapse.show { display: none; }
  .nav-hover-dd:hover > .nav-hover-menu { display: block; }
  @keyframes navHoverIn {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .nav-hover-dd .nav-dd-caret { transition: transform .18s ease; }
  .nav-hover-dd:hover .nav-dd-caret { transform: rotate(-180deg); }
}

/* ── Public nav dropdowns: compact ────────────────────────────────────────
   Measured before this block, on the About / News / Events / FAQ / Solutions
   / Demo menus:

       .dropdown-item font-size   16px      <- LARGER than the 15px nav link
       .dropdown-item padding     4px 16px     that opens the menu
       .dropdown-menu padding     8px 0
       row height                 31px

   A submenu set larger than its own parent link is the whole reason these read
   as oversized: the hierarchy is inverted, so the menu shouts and the nav bar
   under it looks like the smaller thing.

   WHY THE SIZES ARE WRITTEN HERE AND NOT AS --bs-dropdown-* TOKENS
   ---------------------------------------------------------------
   They cannot be. Bootstrap declares `--bs-dropdown-item-padding-y/x` (and its
   sixteen siblings) INSIDE the `.dropdown-menu {}` rule, so a value set on
   `:root` or on `body` is shadowed by the element's own declaration and never
   reaches `.dropdown-item`. The `--bs-dropdown-*` block near the top of this
   file is therefore inert for every metric it sets: the measured 4px/16px
   above is Bootstrap's raw `.25rem 1rem` default, not the `.45rem .7rem` that
   block asks for. (Its COLOUR intent does ship, but through the explicit
   `.dropdown-menu` / `.dropdown-item` rules beside it, not through the tokens.)
   Setting them on `.nav-hover-menu` — the element itself — is what makes them
   apply at all.

   Scoped to `.nav-hover-menu`, which only the public header uses. The portal's
   dropdowns are built on the same `.dropdown-item` class and are a different
   product with a different density; compacting those here would be an
   unrequested change to every signed-in screen. Verified unchanged.

   That scoping still stands for the portal at large, but ONE menu was later
   pulled across on purpose: the topbar quick-nav in layouts/_topbar_nav.html
   renders the same six public sections (About / News / Events / FAQ /
   Solutions / Demo) in both signed-in shells, and it looked nothing like the
   header it mirrors. The copy lives in portal-compact.css under
   `.portal-topbar-nav .dropdown-menu` -- deliberately a copy rather than a
   shared selector, because the two open by different mechanisms (bare CSS
   :hover here, an aria-expanded state driven by script there) and only the
   look is meant to be common. If these values change, change those too. */
@media (min-width: 992px) {
  .nav-hover-menu {
    --bs-dropdown-item-padding-y: .375rem;
    --bs-dropdown-item-padding-x: .625rem;
    --bs-dropdown-padding-y: .3rem;
    --bs-dropdown-padding-x: .3rem;
    padding: .3rem;
    font-size: .8125rem;
  }
  .nav-hover-menu .dropdown-item {
    padding: .375rem .625rem;
    font-size: .8125rem;
    line-height: 1.4;
    font-weight: 500;
    /* 13px * 1.4 + 12px of padding = 30px, clear of the 24px floor WCAG 2.2
       AA 2.5.8 sets for a pointer target. Compact is not the same as small. */
  }
  /* The "About overview" / "All news" row is a destination, not a heading:
     it keeps its weight so it reads as the parent page, and gains a hair of
     extra space beneath so the divider separates two groups rather than
     floating between two equal rows. */
  .nav-hover-menu .dropdown-item.fw-semibold { font-weight: 700; }
  .nav-hover-menu .dropdown-divider { margin: .3rem 0; opacity: 1; }
}

/* Below the desktop breakpoint the same <ul> renders inline as an accordion
   under the nav link, where it is a TOUCH target rather than a pointer one.
   Held at .875rem/.5rem — 20px of text in 36px of row — because the density
   that reads as crisp under a mouse is a mis-tap under a thumb. */
@media (max-width: 991.98px) {
  .nav-hover-menu .dropdown-item {
    padding: .5rem .625rem;
    font-size: .875rem;
    line-height: 1.4;
  }
}

/* ── Nav dropdowns on mobile/tablet (<992px) ───────────────────────────────
   Below the desktop breakpoint the block above never applies (it's fully
   gated inside @media (min-width:992px)), and the same <a> has no
   data-bs-toggle="dropdown" either -- so tapping "About" just navigated
   straight through via its href, silently losing access to the child
   pages entirely. Fix: a separate small caret BUTTON (not inside the <a>,
   so the link keeps navigating normally) toggles the *same* <ul> via
   Bootstrap's own collapse JS (already loaded, data-bs-toggle="collapse")
   -- no new JS needed. Bootstrap's base .dropdown-menu class sets
   position:absolute unconditionally (not just in the desktop block above),
   so it must be reset to static here or the revealed menu would try to
   float rather than flow inline like an accordion. */
.nav-dd-mobile-toggle {
  width: 2.25rem;
  height: 2.25rem;
  line-height: 2.25rem;
  text-align: center;
  background: transparent;
  border: none;
  border-radius: .4rem;
  color: var(--muted, #64748b);
  flex-shrink: 0;
}
.nav-dd-mobile-toggle svg { vertical-align: middle; transition: transform .18s ease; }
/* `:hover` on a touch device STICKS: the last-tapped caret keeps the grey
   fill until something else is tapped, so a closed section reads as the
   open one. Gate it on a real pointer. */
@media (hover: hover) {
  .nav-dd-mobile-toggle:hover { background: var(--bg, #f1f5f9); }
}
.nav-dd-mobile-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
@media (max-width: 991.98px) {
  .nav-hover-dd .nav-hover-menu {
    position: static;
    /* `border-top: none` alone was not enough. Bootstrap's `.dropdown-menu`
       sets `border` on all four sides, so resetting only the top left the
       submenu wearing a full rectangle AND the indent rail beside it -- a
       double frame around every expanded section. Reset the shorthand, then
       state the one edge this design wants. */
    border: 0;
    background: transparent;
    box-shadow: none !important;
    border-radius: 0;
    margin: 0 0 .35rem .95rem !important;
    padding: 0 0 0 .9rem;
    border-left: 2px solid var(--border, #cbd5e1);
    animation: none;
  }
  /* The Demo menu carries an inline `max-height:min(70vh,28rem);overflow-y:auto`
     for its 42 module pages. That is right for a floating desktop menu and
     wrong inside a drawer: it nests a scroll region inside a scroll region,
     so a thumb drag either moves the wrong surface or dead-ends at a boundary
     the user cannot see. The drawer is the single scroller here. */
  .hrms-nav-panel .nav-hover-menu {
    max-height: none !important;
    overflow: visible !important;
  }
}

/* == Mobile navigation drawer (.hrms-nav-panel) ===========================
   WHAT WAS WRONG WITH THE COLLAPSE IT REPLACED
   --------------------------------------------
   Measured on /about/ at 390x844 with the menu open, before this block:

       panel height          655px      (of an 844px viewport)
       document height       3849px  -> 4504px on open
       page behind           still scrollable, no backdrop
       close affordance      none except the burger, now 655px above the
                             bottom of the list
       submenu carets        a ragged diagonal -- each sat immediately after
                             its own label ("About v", "Events v"), so the
                             column of five drifted with the word lengths
                             instead of lining up
       "Sign in"             right-aligned under nine left-aligned links
       expanded submenu      wrapped in TWO frames (see the border reset above)

   A menu that is 78% of the viewport is a screen, not a dropdown, and the
   collapse rendered it as neither: in flow, so it displaced the page it was
   navigating away from.

   WHY OFFCANVAS AND NOT A HAND-ROLLED PANEL
   -----------------------------------------
   Focus trapping, Escape-to-close, `aria-modal`, backdrop click-out, body
   scroll lock and inert-ing the background are the parts of a drawer that are
   easy to get wrong and invisible when you do. Bootstrap's plugin already
   ships in the bundle this page loads, so all of it costs zero new bytes.

   `offcanvas-end` (right) rather than `-start`: the trigger is the burger in
   the top-RIGHT corner, and a panel that flies in from the opposite side of
   the screen from the control that summoned it breaks the connection between
   the two. It also puts the list under the thumb that opened it.
   ======================================================================= */
.hrms-nav-panel {
  --bs-offcanvas-width: min(21rem, 86vw);
  --bs-offcanvas-bg: var(--surface, #fff);
  --bs-offcanvas-color: var(--text);
  --bs-offcanvas-border-color: var(--border);
  --bs-offcanvas-padding-x: 1rem;
  --bs-offcanvas-padding-y: .85rem;
}
@media (max-width: 991.98px) {
  .hrms-nav-panel .offcanvas-header {
    border-bottom: 1px solid var(--border);
    min-height: var(--nav-container-h, 60px);
  }
  .hrms-nav-panel .offcanvas-title {
    font-size: .75rem;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
  }
  /* The body scrolls, the header does not: with the Demo accordion open the
     list runs several screens, and a close button that scrolls away is a
     close button that is not there. */
  .hrms-nav-panel .offcanvas-body {
    display: flex;
    flex-direction: column;
    padding: .5rem 0 1rem;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* `mx-auto` centres the primary list horizontally on desktop. Inside a
     full-height column drawer it centres it VERTICALLY instead, floating nine
     links in the middle of the panel with dead space above and below. */
  .hrms-nav-panel .navbar-nav.mx-auto { margin: 0 !important; width: 100%; }
  .hrms-nav-panel .navbar-nav { gap: 0 !important; width: 100%; }

  /* -- One row per destination ------------------------------------------
     The label and its caret live in a `.d-flex` the template already
     renders; `space-between` is what moves the caret from "wherever the
     word ended" to the panel edge, which is the single change that turns
     the ragged column measured above into a straight one. */
  /* The gutter belongs to the LINK, not to the row that wraps it. Put it on
     the wrapper and the active row's `inset` accent bar paints at the wrapper's
     content edge -- i.e. hard against the first letter of the label instead of
     at the panel edge, which reads as an underline fragment rather than a
     marker. It also costs the caret button its flush-right position. */
  .hrms-nav-panel .navbar-nav > .nav-item > .d-flex {
    justify-content: space-between;
  }
  .hrms-nav-panel .navbar-nav .nav-link {
    padding-inline: var(--bs-offcanvas-padding-x);
  }
  .hrms-nav-panel .navbar-nav > .nav-item + .nav-item {
    border-top: 1px solid var(--border);
  }
  .hrms-nav-panel .nav-link {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    /* 44px minimum, per WCAG 2.2 AA 2.5.8 and every platform HIG. The rows
       measured 45px before, so this is not new headroom -- it is the same
       row with the whole width made tappable instead of just the word. */
    min-height: 2.75rem;
    padding: .7rem 0;
    font-size: 1rem;
    font-weight: 500;
    color: var(--text);
    border-radius: 0;
  }
  /* The sliding underline is a pointer affordance: it animates on hover,
     which does not exist here, and on a full-width row it reads as a stray
     rule under the text. The active row states itself with weight, colour
     and a leading accent bar instead. */
  .hrms-nav-panel .nav-link::after { display: none; }
  .hrms-nav-panel .nav-link.active {
    color: var(--primary);
    font-weight: 700;
    box-shadow: inset 3px 0 0 0 var(--primary);
  }
  .hrms-nav-panel .nav-link:active { background: var(--bg, #f1f5f9); }

  /* -- The submenu caret -------------------------------------------------
     Was 36x36 -- under the 44px floor, and sitting mid-row where a stray tap
     lands on the link instead and navigates away from the menu you were
     trying to open. Now a full-height 44px column at the panel edge with a
     hairline separating it from the label, so the two targets are visibly
     two targets. */
  .hrms-nav-panel .nav-dd-mobile-toggle {
    height: 2.75rem;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3rem;
    margin: 0;
    border-left: 1px solid var(--border);
    border-radius: 0;
  }
  .hrms-nav-panel .nav-dd-mobile-toggle[aria-expanded="true"] {
    color: var(--primary);
  }

  /* Submenu rows sit one step in and one step quieter than their parent, so
     depth is legible without a second border or an indent so deep the text
     wraps on a 320px screen. */
  .hrms-nav-panel .nav-hover-menu .dropdown-item {
    min-height: 2.5rem;
    display: flex;
    align-items: center;
    padding: .5rem .75rem;
    font-size: .9375rem;
    color: var(--muted);
    border-radius: var(--radius-sm);
    white-space: normal;
  }
  .hrms-nav-panel .nav-hover-menu .dropdown-item:active { background: var(--bg, #f1f5f9); }
  .hrms-nav-panel .nav-hover-menu .dropdown-divider { display: none; }
  .hrms-nav-panel .nav-hover-menu .dropdown-item.fw-semibold { color: var(--text); }

  /* -- The account / CTA list -------------------------------------------
     Pushed to the bottom of the panel and given the full width. "Sign in"
     was an inline pill that inherited the desktop row's right alignment, so
     it hung under the left-aligned links with no relationship to anything. A
     drawer's primary action belongs at the bottom edge, at full width, where
     a thumb already is. */
  .hrms-nav-panel .navbar-nav:last-child {
    margin-top: auto;
    padding: 1rem var(--bs-offcanvas-padding-x) 0;
    border-top: 1px solid var(--border);
    align-items: stretch !important;
    gap: .5rem !important;
  }
  .hrms-nav-panel .btn-nav-cta {
    display: flex;
    justify-content: center;
    width: 100%;
    min-height: 2.75rem;
    font-size: .9375rem;
  }
  /* Full width, and every child pinned so the row reads left-to-right like
     the nine links above it: avatar, name, then the caret alone on the right.
     `flex: 0 0 auto` on the children rather than a justify-content alone --
     the label span carries an inline `max-width:110px` and the avatar its own
     sizing, and a growing child ignores the container's alignment. */
  .hrms-nav-panel .navbar-user-btn {
    width: 100%;
    justify-content: flex-start;
    min-height: 2.75rem;
    text-align: left;
  }
  .hrms-nav-panel .navbar-user-btn > * { flex: 0 0 auto; }
  .hrms-nav-panel .navbar-user-btn > svg { margin-left: auto; }
  /* The signed-in account menu is `position:absolute` by default and would
     open over the panel's own scroll. Inline, it flows like the rest. */
  .hrms-nav-panel .dropdown-menu:not(.nav-hover-menu) {
    position: static !important;
    transform: none !important;
    width: 100%;
    border: 0;
    box-shadow: none !important;
    background: transparent;
    padding: .25rem 0 0 !important;
  }
}

/* Desktop: Bootstrap flattens `.offcanvas` into an inline row at
   `navbar-expand-lg`, but it leaves the element `flex-direction: column` and
   the body `flex-grow: 0` -- which the old `.navbar-collapse` did not. Without
   these two the primary list loses the `mx-auto` centring it has always had.

   THE BODY RULE MUST OUT-SPECIFY BOOTSTRAP'S OWN, and the first version did
   not. Bootstrap ships

       .navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;...}

   at (0,3,0); this was written as `.hrms-nav .offcanvas-body` at (0,2,0) and
   simply lost, so `flex-grow: 0` stood and the rule had never once taken
   effect. Measured on production at 1920px: the body shrank to its content
   (640px inside an 1127px panel), which packed brand + nav + CTA into the
   left ~850px of a 1320px container and left ~490px of dead space on the
   right -- the header reads as pushed off-centre. It also silently killed the
   `mx-auto` on the primary list: auto margins resolve to ZERO when the
   container has no free space to absorb, so the centring looked like it had
   been overridden when nothing had touched it.

   `.hrms-nav.navbar-expand-lg` (0,4,0) rather than matching Bootstrap at
   (0,3,0) and relying on source order -- the two files' order is a build
   detail, and this rule is invisible when it loses rather than obviously
   broken, which is exactly how it survived unnoticed.

   After: body fills the panel, the list centres, and the CTA sits flush to
   the container's right edge (1608px against a 1620px container). */
@media (min-width: 992px) {
  .hrms-nav.navbar-expand-lg .offcanvas { flex-direction: row; align-items: center; }
  .hrms-nav.navbar-expand-lg .offcanvas .offcanvas-body { flex-grow: 1; align-items: center; }
}

/* ── My Portal — Profile page (promoted from a per-template <style> block so
   it's shared by any self-service page that needs the same hero/section/info
   layout, instead of being duplicated) ─────────────────────────────────── */
.profile-hero {
  background: linear-gradient(135deg, var(--primary) 0%, #7c3aed 100%);
  color: #fff;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 2rem;
  margin-bottom: 1.5rem;
  position: relative;
  overflow: hidden;
}
@media (max-width: 575.98px) {
  .profile-hero { padding: 1.5rem; }
}
.profile-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.04'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.profile-avatar {
  /* Prominent profile-header avatar (hero, next to the h1 name + role badges) --
     sized to match .ep-avatar-ring, the equivalent hero avatar on the
     HR-admin employee detail page (templates/employees/detail.html), so the
     "profile header" reads at the same scale everywhere it appears. Was
     5rem/80px, which read small next to the h1 name heading here; card/list
     avatars elsewhere (.dash-avatar-ring, .avatar, .hrms-avatar) stay as-is,
     that's a different, correctly-compact role. */
  width: 6.5rem; height: 6.5rem;
  border-radius: 50%;
  background: rgba(255,255,255,0.2);
  color: #fff;
  font-size: var(--fs-2xl);
  font-weight: 800;
  display: flex; align-items: center; justify-content: center;
  border: 3px solid rgba(255,255,255,0.4);
  flex-shrink: 0;
}
.profile-section-nav,
.portal-subnav-scroll {
  display: flex;
  gap: .3rem;
  overflow-x: auto;
  padding-bottom: .5rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1.5rem;
  scrollbar-width: none;
}
.profile-section-nav::-webkit-scrollbar,
.portal-subnav-scroll::-webkit-scrollbar { display: none; }
.profile-section-link,
.portal-subnav-link {
  display: inline-flex;
  align-items: center;
  padding: .55rem 1.05rem;
  border-radius: var(--bs-border-radius-pill, 999px);
  font-size: .8125rem;
  font-weight: 600;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
  border: 1px solid transparent;
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease, transform 100ms ease;
}
.profile-section-link:hover,
.portal-subnav-link:hover {
  background: var(--bg);
  color: var(--text);
  transform: translateY(-1px);
}
.profile-section-link.active,
.portal-subnav-link.active {
  background: var(--primary);
  color: #fff;
  border-color: var(--primary);
  box-shadow: var(--shadow-sm);
}

/* ── My Portal — top-level self-service sub-nav shell (layouts/portal.html):
   a slim, sticky tab bar reusing the .profile-section-nav look above so
   every /me/ page shares one consistent, app-like way to move between
   sections instead of only a per-page "Back" button. A filled pill marks
   the active section (rather than a plain color swap) so it reads clearly
   at a glance even on a busy 16-tab strip. ────────────────────────────── */
.portal-subnav {
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border-bottom: var(--border-width, 1px) solid var(--border);
  position: sticky;
  top: 0;
  z-index: 900;
}
.portal-subnav .portal-subnav-scroll {
  margin-bottom: 0;
  padding-top: .6rem;
  padding-bottom: .6rem;
  border-bottom: none;
  /* The scrollbar is hidden (see .portal-subnav-scroll::-webkit-scrollbar
     above) so on a phone-width screen, with most of the 16 tabs off-screen,
     there was otherwise zero visual hint that swiping reveals more —
     fading both edges is the standard cue that a strip scrolls. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 20px, #000 calc(100% - 20px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 20px, #000 calc(100% - 20px), transparent 100%);
}
@media (max-width: 575.98px) {
  .profile-section-link,
  .portal-subnav-link {
    padding: .6rem 1rem;
    font-size: .8rem;
    min-height: 2.5rem;
  }
}
.info-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
  gap: 1rem;
}
.info-item label {
  font-size: .72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
  display: block;
  margin-bottom: .2rem;
}
.info-item .value {
  font-size: .9375rem;
  font-weight: 500;
  color: var(--text);
}
.info-item .value.empty { color: var(--muted); font-style: italic; }
.section-card {
  background: var(--surface);
  border: var(--border-width, 1px) solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 1.5rem;
  margin-bottom: 1.25rem;
}
.section-card h3 {
  font-size: .72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  margin-bottom: 1rem;
}
.tap-row {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .6rem 0;
  border-bottom: 1px solid var(--border);
  font-size: .85rem;
}
.tap-row:last-child { border-bottom: none; }
.tap-badge {
  padding: .2rem .6rem;
  border-radius: .375rem;
  font-size: .72rem;
  font-weight: 700;
}
.tap-in  { background: #d1fae5; color: #065f46; }
.tap-out { background: #ede9fe; color: #5b21b6; }
.tap-tap { background: #dbeafe; color: #1e40af; }

/* ── Extra .badge-dot color variant for in-progress / in-review states,
   used by the workflow request-detail/list pages below. ────────────────── */
.badge-inprogress { background: #dbeafe; color: #1e40af; }

/* ── Workflow Approval Engine — chain visualization (promoted from a
   per-template <style> block in workflows/request_detail.html so any future
   request-detail-style page shares one real implementation). ───────────── */
.chain-node {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  padding: 1rem 1.25rem;
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: .875rem;
  transition: border-color 150ms ease, box-shadow 150ms ease;
}
.chain-node:hover { box-shadow: 0 2px 12px rgba(0,0,0,.08); }
.chain-node.status-in_review { border-color: #3b82f6; background: #eff6ff; }
.chain-node.status-approved  { border-color: #10b981; background: #f0fdf4; }
.chain-node.status-rejected  { border-color: #ef4444; background: #fef2f2; }
.chain-node.status-reassigned{ border-color: #f59e0b; background: #fffbeb; }
.chain-connector {
  width: 2px;
  height: 1.5rem;
  background: var(--border);
  margin: .25rem 0 .25rem 1.8rem;
}
.chain-step-num {
  width: 2rem; height: 2rem;
  border-radius: 50%;
  background: var(--primary-light);
  color: var(--primary);
  font-size: .75rem;
  font-weight: 800;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.chain-node.status-approved  .chain-step-num { background: #d1fae5; color: #065f46; }
.chain-node.status-rejected  .chain-step-num { background: #fee2e2; color: #991b1b; }
.chain-node.status-in_review .chain-step-num { background: #dbeafe; color: #1e40af; }

.audit-entry {
  font-size: .78rem;
  padding: .35rem 0;
  border-bottom: 1px solid var(--border);
  display: flex;
  gap: .5rem;
  align-items: flex-start;
}
.audit-entry:last-child { border-bottom: none; }
.action-chip {
  padding: .15rem .55rem;
  border-radius: .375rem;
  font-size: .68rem;
  font-weight: 700;
  white-space: nowrap;
}
.chip-assign    { background: #dbeafe; color: #1e40af; }
.chip-approve   { background: #d1fae5; color: #065f46; }
.chip-reject    { background: #fee2e2; color: #991b1b; }
.chip-reassign  { background: #fef3c7; color: #92400e; }
.chip-edit      { background: #ede9fe; color: #5b21b6; }
.payload-table td:first-child { color: var(--muted); font-size:.8rem; width: 35%; }

/* ── Tenant Manager: per-tenant tabs ──────────────────────────────────────
   The per-tenant page carried 12 cards in one column, so lifecycle actions,
   API keys, people and billing all competed for the same scroll. Grouping
   them into tabs is the whole fix; these styles just make the tab bar read
   as navigation rather than as another card. Built on the existing token
   vocabulary so every Appearance preset picks it up unchanged. */
.tm-tabs {
  border-bottom: var(--border-width, 1px) solid var(--border);
  gap: .15rem;
  flex-wrap: wrap;
}
.tm-tabs .nav-link {
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  padding: .6rem 1rem;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--muted);
  background: none;
  transition: color .13s ease, border-color .13s ease;
}
.tm-tabs .nav-link:hover { color: var(--text); border-bottom-color: var(--border); }
.tm-tabs .nav-link.active {
  color: var(--primary);
  border-bottom-color: var(--primary);
  background: none;
}
/* Keyboard users must be able to see which tab they are on -- Bootstrap's
   own reset removes the default outline here. */
.tm-tabs .nav-link:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.tm-tab-content > .tab-pane { animation: msgc-fade-in .16s ease; }


/* ── Server-driven data components ────────────────────────────────────────
   Companions to the table furniture above: the tiles, pills, records,
   callouts and meters that render DATA rather than the chrome around it.

   Every value here resolves through an existing token, so all 16 appearance
   presets and both light/dark themes are inherited with zero preset-specific
   rules -- the same property that keeps the table components preset-agnostic.
   `color-mix` against --surface is used for tinted backgrounds instead of
   fixed rgba(): a hardcoded translucent white reads correctly on a light
   preset and muddy on a dark one, which is exactly the drift this replaces.
──────────────────────────────────────────────────────────────────────────── */

/* Tone ramp. One place that maps the semantic vocabulary onto tokens, so a
   component never names a colour itself. */
.hrms-badge--neutral, .hrms-callout--neutral, .hrms-card--neutral,
.hrms-stat--neutral, .hrms-meter--neutral { --tone-color: var(--muted); --tone-text: var(--muted); }
.hrms-badge--primary, .hrms-callout--primary, .hrms-card--primary,
.hrms-stat--primary, .hrms-meter--primary { --tone-color: var(--primary); --tone-text: var(--primary); }
.hrms-badge--success, .hrms-callout--success, .hrms-card--success,
.hrms-stat--success, .hrms-meter--success { --tone-color: var(--success); --tone-text: var(--success-text); }
.hrms-badge--warning, .hrms-callout--warning, .hrms-card--warning,
.hrms-stat--warning, .hrms-meter--warning { --tone-color: var(--warning); --tone-text: var(--warning-text); }
.hrms-badge--danger,  .hrms-callout--danger,  .hrms-card--danger,
.hrms-stat--danger,  .hrms-meter--danger  { --tone-color: var(--danger); --tone-text: var(--danger-text); }
.hrms-badge--info,    .hrms-callout--info,    .hrms-card--info,
.hrms-stat--info,    .hrms-meter--info    { --tone-color: var(--info); --tone-text: var(--info-text); }

/* ── Status badge ─────────────────────────────────────────────────────── */
.hrms-badge {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: .2rem .55rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-2xs);
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
  /* A chip must never be the reason the PAGE scrolls sideways. `nowrap` is
     right for a short status word, but /iot/readiness/ renders a kiosk
     topology name in one: 403px wide, which pushed the whole document 130px
     past a 320px viewport. `max-width` alone cannot fix that while the text
     refuses to wrap, so narrow viewports drop the nowrap — wrapping only
     engages when the label genuinely does not fit, which is the failure case
     and nothing else. Verified against production: 130px of sideways scroll
     at 320px and 60px at 390px, both to zero, with no change at 768+. */
  max-width: 100%;
  /* Text uses --tone-text (AA against the tint below); the tint itself
     uses the vivid --tone-color. See the --*-text tokens' own comment. */
  color: var(--tone-text, var(--tone-color, var(--muted)));
  background: color-mix(in srgb, var(--tone-color, var(--muted)) 12%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--tone-color, var(--muted)) 30%, transparent);
}

@media (max-width: 480px) {
  /* See .hrms-badge's max-width comment. */
  .hrms-badge { white-space: normal; }
}

/* ── Stat tile ────────────────────────────────────────────────────────── */
.hrms-stat {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  text-decoration: none;
  color: inherit;
}
a.hrms-stat:hover { border-color: var(--tone-color, var(--primary)); box-shadow: var(--shadow); }
a.hrms-stat:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.hrms-stat-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; flex: none;
  border-radius: var(--radius-sm);
  color: var(--tone-text, var(--tone-color, var(--primary)));
  background: color-mix(in srgb, var(--tone-color, var(--primary)) 12%, var(--surface));
}
.hrms-stat-body { display: flex; flex-direction: column; min-width: 0; }
.hrms-stat-value {
  font-size: var(--fs-xl, 1.5rem); font-weight: 700; line-height: 1.15;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.hrms-stat-label {
  font-size: var(--fs-2xs); color: var(--muted); font-weight: 600;
  text-transform: uppercase; letter-spacing: .03em;
}
.hrms-stat-hint { font-size: var(--fs-2xs); color: var(--muted); }
.hrms-stat-delta {
  margin-left: auto; flex: none;
  font-size: var(--fs-2xs); font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.hrms-stat-delta--success { color: var(--success); }
.hrms-stat-delta--danger  { color: var(--danger); }
.hrms-stat-delta--warning { color: var(--warning); }

/* ── Card ─────────────────────────────────────────────────────────────── */
.hrms-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
/* Tone shows as an accent edge, not a tinted panel: a fully-tinted card
   competes with the badges and callouts inside it. */
.hrms-card[class*="hrms-card--"]:not(.hrms-card--flush) { border-top: 3px solid var(--tone-text, var(--tone-color, var(--border))); }
.hrms-card-header {
  display: flex; align-items: flex-start; gap: .6rem;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  color: var(--tone-text, var(--tone-color, var(--muted)));
}
.hrms-card-heading { min-width: 0; }
.hrms-card-title {
  margin: 0; font-size: var(--fs-md); font-weight: 700; color: var(--text);
}
.hrms-card-subtitle { margin: .1rem 0 0; font-size: var(--fs-2xs); color: var(--muted); }
.hrms-card-body { padding: var(--space-4); }
.hrms-card--flush > .hrms-card-body { padding: 0; }

/* ── Detail list ──────────────────────────────────────────────────────── */
.hrms-details { margin: 0; display: grid; gap: .1rem; }
.hrms-details--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .1rem 1.75rem; }
.hrms-details-row {
  display: grid;
  grid-template-columns: minmax(8rem, 34%) 1fr;
  gap: .75rem;
  padding: var(--space-2) 0;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}
.hrms-details--dense .hrms-details-row { padding: var(--space-1) 0; }
.hrms-details-row:last-child { border-bottom: none; }
.hrms-details-label {
  margin: 0; font-size: var(--fs-2xs); font-weight: 600; color: var(--muted);
}
.hrms-details-value { margin: 0; font-size: var(--fs-xs); color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.hrms-details-value.is-blank { color: var(--muted); }

@media (max-width: 640px) {
  /* Stacking is required below this width: a 34% label column leaves too
     little room for the value, and a two-column grid of those is unusable. */
  .hrms-details--cols-2 { grid-template-columns: 1fr; }
  .hrms-details-row { grid-template-columns: 1fr; gap: .1rem; }
}

/* ── Callout ──────────────────────────────────────────────────────────── */
.hrms-callout {
  display: flex; align-items: flex-start; gap: var(--space-2);
  padding: var(--space-3);
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--tone-color, var(--info)) 35%, transparent);
  background: color-mix(in srgb, var(--tone-color, var(--info)) 10%, var(--surface));
  color: var(--text);
}
.hrms-callout-icon { color: var(--tone-text, var(--tone-color, var(--info))); flex: none; line-height: 1; margin-top: .1rem; }
.hrms-callout-body { min-width: 0; }
.hrms-callout-title { margin: 0 0 .15rem; font-size: var(--fs-xs); font-weight: 700; }
.hrms-callout-msg { margin: 0; font-size: var(--fs-xs); color: var(--text); }

/* ── Avatar ───────────────────────────────────────────────────────────── */
.hrms-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-pill);
  object-fit: cover;
  flex: none;
  font-weight: 700;
  /* Hue comes from the name (set inline by the tag); saturation and
     lightness stay fixed so every generated colour has comparable contrast
     against the text on top of it, in either theme. */
  background: hsl(var(--avatar-hue, 220) 55% 88%);
  color: hsl(var(--avatar-hue, 220) 60% 28%);
}
:root[data-bs-theme="dark"] .hrms-avatar--initials {
  background: hsl(var(--avatar-hue, 220) 40% 28%);
  color: hsl(var(--avatar-hue, 220) 65% 85%);
}
.hrms-avatar--sm { width: 24px; height: 24px; font-size: .65rem; }
.hrms-avatar--md { width: 32px; height: 32px; font-size: .78rem; }
.hrms-avatar--lg { width: 48px; height: 48px; font-size: 1rem; }
.hrms-avatar--xl { width: 72px; height: 72px; font-size: 1.5rem; }

/* ── Meter ────────────────────────────────────────────────────────────── */
.hrms-meter { display: flex; flex-direction: column; gap: .3rem; }
.hrms-meter-head { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; }
.hrms-meter-label { font-size: var(--fs-2xs); font-weight: 600; color: var(--muted); }
.hrms-meter-value { font-size: var(--fs-2xs); color: var(--muted); font-variant-numeric: tabular-nums; }
.hrms-meter-track {
  height: .5rem;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--border) 60%, transparent);
  overflow: hidden;
}
.hrms-meter-fill {
  height: 100%;
  border-radius: inherit;
  background: var(--tone-color, var(--primary));
  transition: width var(--motion-slow, .3s) ease;
}
.hrms-meter-over { margin: 0; font-size: var(--fs-2xs); color: var(--danger); font-weight: 600; }

/* Motion preset "none" must reach this too -- see the motion block below.
   Declared here rather than there so the component owns its own reduction. */
@media (prefers-reduced-motion: reduce) {
  .hrms-meter-fill { transition: none; }
}


/* ── Toggle switch ────────────────────────────────────────────────────────
   Promoted here from two template <style> blocks (iot/device_list.html and
   iot/camera_dashboard.html) that had each grown their own copy. They had
   genuinely diverged, which is the argument for this living in one place:

     property        device_list.html      camera_dashboard.html
     ─────────────── ───────────────────── ────────────────────────
     off state       #cbd5e1 (hardcoded)   var(--border, #cbd5e1)
     checked         #16a34a (hardcoded)   var(--cam-online)
     dark off state  #475569               rgba(255,255,255,.14)
     :disabled       styled                MISSING

   So the same control was a different green, a different grey in dark mode,
   and gave no disabled feedback at all depending on which page you were on.

   Resolving through --border and --success means the manual
   [data-bs-theme="dark"] override both copies carried is no longer needed:
   those tokens already change per theme and per preset, which is the entire
   reason the token layer exists. The override only existed to compensate for
   the hardcoded hex it sat next to. */
.hrms-toggle {
  position: relative; display: inline-block;
  width: 40px; height: 22px; flex-shrink: 0;
}
.hrms-toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.hrms-toggle .hrms-toggle-slider {
  position: absolute; inset: 0; cursor: pointer;
  background: var(--border);
  transition: background var(--motion-fast, .15s);
  border-radius: var(--radius-pill);
}
.hrms-toggle .hrms-toggle-slider::before {
  content: ''; position: absolute;
  height: 16px; width: 16px; left: 3px; top: 3px;
  /* Deliberately a constant, not a token: the knob must stay legible against
     BOTH the off state (a light border colour) and the on state (a saturated
     success green), in every preset. var(--surface) would make it vanish
     against the slider in dark themes. Same reasoning as the near-black
     video letterbox in camera_dashboard. */
  background: #fff;
  transition: transform .15s;
  border-radius: 50%;
  box-shadow: 0 1px 2px rgb(0 0 0 / .3);
}
.hrms-toggle input:checked + .hrms-toggle-slider { background: var(--success); }
.hrms-toggle input:checked + .hrms-toggle-slider::before { transform: translateX(18px); }
.hrms-toggle input:disabled + .hrms-toggle-slider { opacity: .5; cursor: not-allowed; }
/* Keyboard affordance: the native input is visually hidden, so without this
   the control is completely unfocusable-looking to a keyboard user even
   though it is focusable. Neither template copy had it. */
.hrms-toggle input:focus-visible + .hrms-toggle-slider {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .hrms-toggle .hrms-toggle-slider,
  .hrms-toggle .hrms-toggle-slider::before { transition: none; }
}

/* ===========================================================================
   Marketing-page scroll reveals
   ---------------------------------------------------------------------------
   Expressive motion, confined to the public marketing surface (see
   docs/frontend-design-system.md 3.5 -- the portal stays restrained).

   Only `opacity` and `transform` are animated. Both are compositor-only
   properties, so a reveal cannot trigger layout and cannot jank the scroll
   even with many blocks observed at once. Animating `height`, `top` or
   `filter` here would undo that.

   The initial hidden state is applied by JS (`.js-anim-ready` on <html>), NOT
   by this stylesheet. If the script fails to load, is blocked, or throws, the
   content must remain visible -- a marketing page that renders blank because
   an animation never fired is worse than one that never animates.
   =========================================================================== */

.js-anim-ready [data-anim]:not([data-anim="none"]) {
  opacity: 0;
  transition: opacity var(--anim-duration, .7s) var(--motion-easing),
              transform var(--anim-duration, .7s) var(--motion-easing);
  transition-delay: var(--anim-delay, 0s);
  will-change: opacity, transform;
}

.js-anim-ready [data-anim="rise"]   { transform: translate3d(0, 24px, 0); }
.js-anim-ready [data-anim="scale"]  { transform: scale(.96); }
.js-anim-ready [data-anim="fade"]   { transform: none; }

/* Expressive: a longer travel plus a slight scale, landing on an overshoot
   easing. Intended for a hero or one feature band -- a page where every block
   uses it reads as unstable rather than polished, which is why the admin
   screen names the trade-off in the option label.

   Still transform/opacity only, so it stays compositor-only and cannot jank
   the scroll. The overshoot is in the easing curve rather than in keyframes so
   the reduced-motion and portal-motion resets below neutralise it with no
   extra rules. */
.js-anim-ready [data-anim="expressive"] {
  transform: translate3d(0, 48px, 0) scale(.94);
  --anim-duration: .9s;
}
.js-anim-ready [data-anim="expressive"].is-revealed {
  transform: none;
}
.js-anim-ready [data-anim="expressive"]:not(.is-revealed),
.js-anim-ready [data-anim="expressive"].is-revealed {
  transition-timing-function: cubic-bezier(.22, 1.4, .36, 1);
}

/* Revealed: every preset lands on the same resting state, so a block that is
   already visible on load looks identical to one that scrolled into view. */
.js-anim-ready [data-anim].is-revealed {
  opacity: 1;
  transform: none;
  /* Dropped once the transition has served its purpose: a permanent
     will-change on many elements costs memory for no benefit. */
  will-change: auto;
}

/* Stagger: children animate in sequence. The delay is set per child by JS
   rather than with :nth-child rules, because the child count is content-driven
   and an nth-child ladder would silently stop staggering past its last rule. */
.js-anim-ready [data-anim="stagger"] { opacity: 1; transform: none; }
.js-anim-ready [data-anim="stagger"] > * {
  opacity: 0;
  transform: translate3d(0, 20px, 0);
  transition: opacity var(--anim-duration, .6s) var(--motion-easing),
              transform var(--anim-duration, .6s) var(--motion-easing);
  transition-delay: var(--anim-delay, 0s);
}
.js-anim-ready [data-anim="stagger"].is-revealed > * {
  opacity: 1;
  transform: none;
}

/* --- Opt-outs -------------------------------------------------------------
   Three independent kill switches, all of which must win over the rules above.

   1. prefers-reduced-motion -- an OS-level accessibility preference. This is
      deliberately NOT configurable by an administrator: WCAG 2.3.3 treats it
      as a user right, not a site preference.
   2. data-portal-motion="disabled" -- the Off level of the 3-level motion
      setting. This read "none" for a long time, which is not a value the
      setting can hold, so Off never stopped scroll-reveal on public pages.
   3. data-anim="none" -- this block switched off in the CMS.

   Each resets to the resting state rather than merely shortening the
   transition, so content is visible immediately and unconditionally. */
@media (prefers-reduced-motion: reduce) {
  .js-anim-ready [data-anim],
  .js-anim-ready [data-anim="stagger"] > * {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

[data-portal-motion="disabled"] [data-anim],
[data-portal-motion="disabled"] [data-anim="stagger"] > * {
  opacity: 1 !important;
  transform: none !important;
  transition: none !important;
}

/* Reduced motion preset: keep a short fade so the page still feels alive,
   but drop all translation -- vestibular triggers come from movement. */
[data-portal-motion="reduced"] [data-anim] {
  transform: none !important;
  --anim-duration: .25s;
  --anim-delay: 0s !important;
}
[data-portal-motion="reduced"] [data-anim="stagger"] > * {
  transform: none !important;
  --anim-duration: .25s;
}

/* ── Employee photo viewer ──────────────────────────────────────────────
   Full-screen preview with zoom/pan, opened from any [data-photo-zoom]
   image on the employee detail and edit pages.

   Uses a fixed overlay rather than a Bootstrap modal: the stage needs the
   full viewport for zoom to be worth anything, and the modal's own padding
   and max-width fight that at every breakpoint.                          */
.photo-viewer {
  position: fixed;
  inset: 0;
  /* Above every layer this app stacks: Bootstrap modal (1055), its backdrop
     (1050), the portal's own sticky topbar and sidebar, and the toast stack at
     1100. Reported as the preview opening *behind* page chrome, which is what
     1080 gets you once the toast container is involved. */
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
}
.photo-viewer[hidden] { display: none; }

.photo-viewer-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(2, 6, 23, .92);
  backdrop-filter: blur(2px);
}

/* Clips the zoomed image so panning reveals it rather than overflowing the
   page and creating a scrollbar behind the overlay. */
.photo-viewer-stage {
  position: relative;
  max-width: 92vw;
  max-height: 82vh;
  overflow: hidden;
  border-radius: .75rem;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
}
.photo-viewer-stage img {
  display: block;
  max-width: 92vw;
  max-height: 82vh;
  transform-origin: center center;
  transition: transform .12s ease-out;
  user-select: none;
  -webkit-user-drag: none;
}

.photo-viewer-toolbar {
  position: absolute;
  bottom: 1.25rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: .25rem;
  padding: .35rem .5rem;
  border-radius: 999px;
  background: rgba(15, 23, 42, .96);
  border: 1px solid rgba(255, 255, 255, .18);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .55);
  /* Above the stage so a zoomed image cannot cover its own controls -- the
     reported "toolbar disappears when you zoom in". */
  z-index: 2;
  flex-wrap: wrap;
  max-width: calc(100vw - 2rem);
  justify-content: center;
}
.photo-viewer .pv-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .35rem;
  min-width: 34px;
  height: 34px;
  padding: 0 .6rem;
  border: 0;
  border-radius: 999px;
  font-size: .8rem;
  white-space: nowrap;
  background: transparent;
  color: #e2e8f0;
  text-decoration: none;
  cursor: pointer;
}
.photo-viewer .pv-btn:hover,
.photo-viewer .pv-btn:focus-visible { background: rgba(255, 255, 255, .14); }
/* Visible focus ring: the toolbar sits on a dark backdrop where the default
   outline is nearly invisible. */
.photo-viewer .pv-btn:focus-visible { outline: 2px solid #818cf8; outline-offset: 2px; }

.photo-viewer .pv-level {
  min-width: 3.5rem;
  text-align: center;
  font-size: .8rem;
  color: #cbd5e1;
  font-variant-numeric: tabular-nums;   /* stops the toolbar twitching as digits change */
}

.photo-viewer-caption {
  position: absolute;
  top: 1.1rem;
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  max-width: 80vw;
  text-align: center;
  color: #e2e8f0;
  font-size: .9rem;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
}

/* Stops the page behind the overlay scrolling under a wheel-zoom. */
body.photo-viewer-open { overflow: hidden; }

/* The affordance that tells a user the photo is clickable at all. */
img[data-photo-zoom] { cursor: zoom-in; }

@media (prefers-reduced-motion: reduce) {
  .photo-viewer-stage img { transition: none; }
}

/* Button labels. Icon-only controls are guessable at best; the reported
   confusion was not knowing which magnifier zoomed in. Hidden on very narrow
   screens where the toolbar would otherwise wrap past the viewport. */
.photo-viewer .pv-btn-label { display: inline; }
@media (max-width: 480px) {
  .photo-viewer .pv-btn-label { display: none; }
  .photo-viewer .pv-btn { padding: 0; min-width: 34px; }
}

/* The stage must not sit above the toolbar once the image is scaled up. */
.photo-viewer-stage { z-index: 1; }

/* ── Shared confirmation dialog (hrmsConfirm) ───────────────────────────
   Replaces window.confirm(). Styled here rather than via Bootstrap's modal
   so it can be shown and dismissed synchronously from a click handler that
   needs to cancel navigation.                                            */
.hrms-confirm {
  position: fixed;
  inset: 0;
  /* Above the photo viewer (2000): "discard changes?" can legitimately be
     asked while a preview is open. */
  z-index: 2100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}
.hrms-confirm[hidden] { display: none; }

.hrms-confirm-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(2, 6, 23, .6);
  backdrop-filter: blur(2px);
}

.hrms-confirm-dialog {
  position: relative;
  width: 100%;
  max-width: 26rem;
  padding: 1.5rem;
  border-radius: var(--radius-lg);
  background: var(--bs-body-bg, #fff);
  color: var(--bs-body-color, #0f172a);
  border: 1px solid var(--bs-border-color, rgba(0, 0, 0, .12));
  box-shadow: var(--shadow-lg);
  text-align: center;
}

.hrms-confirm-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  margin-bottom: .85rem;
  border-radius: 50%;
}
.hrms-confirm-icon-danger  { background: rgba(239, 68, 68, .14); color: #ef4444; }
.hrms-confirm-icon-warning { background: rgba(245, 158, 11, .16); color: #b45309; }
.hrms-confirm-icon-primary { background: rgba(99, 102, 241, .14); color: #6366f1; }

.hrms-confirm-title {
  font-size: 1.05rem;
  font-weight: 700;
  margin: 0 0 .4rem;
}
.hrms-confirm-body {
  font-size: .875rem;
  color: var(--bs-secondary-color, #64748b);
  margin: 0 0 1.25rem;
}
.hrms-confirm-actions {
  display: flex;
  gap: .5rem;
  justify-content: center;
  flex-wrap: wrap;
}
.hrms-confirm-actions .btn { min-width: 8rem; }

/* Stops the page behind scrolling while the dialog is open. */
body.hrms-confirm-open { overflow: hidden; }

/* ── Packages & subscriptions page ──────────────────────────────────────
   The page ran as one continuous column of prose, cards and tables, so
   nothing signalled where one idea ended and the next began. These bands
   give the intro and the closing CTA their own ground, and let the pricing
   tables read as documents rather than loose rows.

   Kept to background + spacing on purpose: the tier cards and tables carry
   their own colour, and adding more here would compete with them. ── */
.pricing-band {
  position: relative;
  padding: 3.25rem 0;
  margin: 0 0 2.5rem;
  border-radius: 1.25rem;
  overflow: hidden;
}
.pricing-band > * { position: relative; z-index: 1; }
.pricing-band::before {
  content: '';
  position: absolute;
  inset: 0;
  /* Very low alpha: this sits behind body text on both themes, and anything
     stronger turned the paragraphs into low-contrast grey on grey. */
  background:
    radial-gradient(1200px 320px at 15% 0%, rgba(99, 102, 241, .13), transparent 70%),
    radial-gradient(900px 300px at 85% 100%, rgba(14, 116, 144, .11), transparent 70%);
  z-index: 0;
}
.pricing-band--outro::before {
  background:
    radial-gradient(1000px 320px at 80% 0%, rgba(245, 158, 11, .10), transparent 70%),
    radial-gradient(900px 300px at 10% 100%, rgba(99, 102, 241, .12), transparent 70%);
}
.pricing-band .pricing-band-eyebrow {
  text-transform: uppercase;
  letter-spacing: .08em;
  font-size: .72rem;
  font-weight: 700;
  opacity: .75;
}

/* ── Two pricing tables, presented as two ──────────────────────────────
   They used to sit flush against each other in identical frames with no
   heading between them, so a reader scanning down went straight from the
   last tier row into the first add-on row without any signal that the
   columns had changed meaning. Each table now gets its own titled section
   and real separation. */
.pricing-table-section { margin-bottom: 3rem; }
.pricing-table-section:last-child { margin-bottom: 0; }
.pricing-table-title {
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: -.01em;
  margin-bottom: .35rem;
}
.pricing-table-lede {
  font-size: .9rem;
  opacity: .78;
  margin-bottom: 1rem;
  max-width: 60ch;
}
/* A rule above the add-ons block, so the break between the two is visible
   even when the heading scrolls out of view on a narrow screen. */
.pricing-table-section--addons {
  border-top: 1px solid var(--bs-border-color, rgba(148, 163, 184, .25));
  padding-top: 2.5rem;
}

/* Tables: give them a frame so they read as a spec sheet, and let them
   scroll horizontally on a phone rather than forcing the page to.

   Every colour here was a hard-coded `rgba(148,163,184,…)` slate wash, which
   is the one thing a themed surface must not be: no preset can reach it, so
   the frame, the header band and the hover tint stayed the same cool grey on
   all twenty of them — visibly foreign on `retro` (warm paper), `solarized`
   (olive) and `dark` (where a light slate wash on a dark ground is a smear
   rather than a band). Rebuilt on tokens, which is also what makes the header
   band track the preset's own ink instead of being a fixed grey. */
.pricing-table-wrap {
  border: var(--border-width, 1px) solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 2rem;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  /* Both axes are named explicitly. `overflow-x: auto` alone computes the
     other axis to `auto` as well, and an unclipped y-axis would let the
     header band's fill square off the rounded top corners. */
  overflow-x: auto;
  overflow-y: hidden;
}
.pricing-table-wrap > table {
  margin-bottom: 0;
  --bs-table-bg: transparent;   /* the frame paints the fill, once */
  --bs-table-border-color: var(--border);
}
.pricing-table-wrap > table > thead th {
  white-space: nowrap;
  font-size: .74rem;
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text);          /* see the note on .cms-body table thead th */
  /* Mixed from the preset's own ink so the band is the same *relationship* to
     the surface on a light and a dark theme, rather than a fixed grey that
     only works on one of them. */
  background: color-mix(in srgb, var(--text) 7%, var(--surface));
  /* A header is the strongest horizontal rule in a data table; at 1px it was
     the same weight as every row divider and the head stopped reading as a
     head. This is the single change that turns the block from a data dump
     into a spec sheet. */
  border-bottom: 2px solid var(--border);
  padding: .8rem 1rem;
  vertical-align: bottom;
}
.pricing-table-wrap > table > tbody td {
  vertical-align: middle;
  /* Was Bootstrap's .5rem/8px, which is a dense admin-grid density on a page
     whose job is to be read once, carefully, by somebody deciding what to
     buy. */
  padding: .9rem 1rem;
  border-color: var(--border);
}
.pricing-table-wrap > table > tbody tr:last-child > * { border-bottom-width: 0; }
.pricing-table-wrap > table > tbody tr:hover > * {
  background: color-mix(in srgb, var(--primary) 6%, transparent);
}
/* The tier name is the row's subject: it anchors the scan and everything else
   in the row is read relative to it. */
.pricing-table-wrap > table > tbody td:first-child { color: var(--text); }

/* Sub-labels ("+ $50 base fee / month", "Assessed on-site", "additional fee").
   Authored as `<br><span class="text-muted small">`, which put them on the
   same baseline rhythm as the value above and at a size close enough to
   compete with it. A value and its qualifier are two levels, so they are set
   as two. */
.pricing-table-wrap .text-muted.small {
  display: block;
  margin-top: .15rem;
  font-size: .78rem;
  line-height: 1.3;
}

/* "Not included" was a bare &mdash; sitting alone in a cell, which reads as a
   rendering failure rather than an answer. Same glyph, given a size and a
   weight that say "deliberately empty", and an accessible name so a screen
   reader says something other than "em dash". */
.pricing-table-wrap td:has(> .addon-none) { text-align: center; }
.addon-none::before {
  content: "\2014";            /* em dash, drawn rather than authored */
  display: inline-block;
  color: var(--muted);
  opacity: .7;
  font-size: 1.05rem;
  line-height: 1;
}

/* ── Add-ons table ──────────────────────────────────────────────────────
   Grouped rows need a visual break, or the group headings read as ordinary
   rows and the grouping does nothing. Kept quiet: a tinted band and small
   caps, not a second header. ── */
.addons-table .addons-group th {
  /* Tokenised for the same reason as the header band above: a fixed slate
     wash is foreign on two thirds of the presets. Mixed a little stronger
     than the header so the two are distinguishable when both are on screen —
     a group rule sits INSIDE the body and has to separate rows that already
     have their own dividers. */
  background: color-mix(in srgb, var(--text) 10%, var(--surface));
  color: var(--text);
  font-size: .7rem;
  font-weight: 800;
  letter-spacing: .10em;
  text-transform: uppercase;
  padding: .5rem 1rem;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
/* First group opens the body — no rule above it, the header already ended. */
.addons-table tbody tr:first-child.addons-group th { border-top: 0; }
.addons-table .addons-what {
  max-width: 34rem;
  color: var(--muted);
  font-size: .875rem;
  line-height: 1.5;
}
/* The tier list is a qualifier, not a value -- de-emphasised so the eye goes
   to the add-on name and the price column.
   The fill was a fixed indigo (`rgba(99,102,241,.13)`) while the text was the
   preset's ink: two colours chosen independently, which is the pairing the
   `.text-bg-*` block further down this file exists to stop. Both sides now
   come from --primary, so the chip is legible by construction on every
   preset instead of by luck on the few whose accent happens to be indigo. */
.addons-table .addons-tiers {
  display: inline-block;
  padding: .15rem .55rem;
  border-radius: var(--radius-pill, 999px);
  background: color-mix(in srgb, var(--primary) 12%, var(--surface));
  color: color-mix(in srgb, var(--primary) 72%, var(--text));
  border: 1px solid color-mix(in srgb, var(--primary) 24%, transparent);
  font-size: .75rem;
  font-weight: 600;
  white-space: nowrap;
}

/* ── Tenant status badge (Trial / Demo) ─────────────────────────────────
   Sits beside the brand in the portal topbar. Small and quiet on purpose:
   it is a persistent statement of fact about the account, not an alert, and
   anything louder would nag every user on every page all day. ── */
.tenant-badge {
  display: inline-flex;
  align-items: center;
  margin-left: .5rem;
  padding: .12rem .5rem;
  border-radius: 999px;
  font-size: .68rem;
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: default;
}
.tenant-badge--warning {
  background: rgba(245, 158, 11, .16);
  color: #b45309;
  border: 1px solid rgba(245, 158, 11, .35);
}
.tenant-badge--info {
  background: rgba(99, 102, 241, .16);
  color: #4338ca;
  border: 1px solid rgba(99, 102, 241, .35);
}
/* Dark portal chrome needs lighter text than the light-theme values above,
   which sit at roughly 2:1 against a dark surface. */
/* The theme signal is data-bs-theme. Selectors here were once written
   against data-portal-theme, which carried the sidebar accent and never held
   "dark" -- so they matched nothing. That attribute no longer exists. */
[data-bs-theme="dark"] .tenant-badge--warning { color: #fbbf24; }
[data-bs-theme="dark"] .tenant-badge--info { color: #a5b4fc; }

/* ══════════════════════════════════════════════════════════════════════════
   DESIGN-LANGUAGE PRESETS
   ─────────────────────────────────────────────────────────────────────────
   Unlike the colour-named presets above, each of these commits to a specific
   treatment of DEPTH, EDGE and MOTION. That is the point: swapping a palette
   makes two themes look related, whereas changing how a surface sits above
   the page makes them feel like different products.

   Each sets the same custom-property vocabulary the existing presets use
   (--radius / --border-width / --shadow* / fonts / --motion-easing), so no
   component needs to know these exist.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Material Design ──────────────────────────────────────────────────────
   Elevation is the only depth cue: hard-edged neutral-grey shadows that grow
   with height, no borders, modest radius. */
html[data-portal-preset="material"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="material"] body.auth-page {
  --bg:            #FAFAFA;
  --surface:       #FFFFFF;
  --text:          #212121;
  --muted:         #5F6368;
  --border:        #E0E0E0;
  --primary:       #1976D2;
  --primary-dark:  #0D47A1;
  --primary-light: #E3F2FD;

  --radius:       .25rem;
  --border-width: 0;
  /* Material's elevation ramp: a tight key shadow plus a wider ambient one.
     Neutral, never colour-tinted. */
  --shadow-sm: 0 1px 2px rgba(0,0,0,.30), 0 1px 3px 1px rgba(0,0,0,.15);
  --shadow:    0 1px 2px rgba(0,0,0,.30), 0 2px 6px 2px rgba(0,0,0,.15);
  --shadow-lg: 0 4px 4px rgba(0,0,0,.30), 0 8px 12px 6px rgba(0,0,0,.15);

  --portal-font-family:     Roboto, system-ui, -apple-system, sans-serif;
  --heading-font-family:    Roboto, system-ui, -apple-system, sans-serif;
  --letter-spacing-heading: 0;
  --motion-easing: cubic-bezier(.2, 0, 0, 1);

  --bs-body-bg: #FAFAFA;
}
/* The ink ripple is Material's signature feedback. Clipped to the control so
   it cannot bleed over neighbours.

   It RESTS AT scale(0) and grows on press. The first version had that
   backwards -- resting at scale(8) and snapping to 0 on press -- which
   animated on release rather than on contact and, more seriously, left an
   8x-sized pseudo-element inside every button at rest. `overflow: hidden`
   clips the PAINT but not the scrollable overflow area, so every button
   reported a scrollWidth roughly 4.5x its own ("Employees" measured 323px in
   a 72px box). Caught by the layout probe in
   apps/system_settings/tests/test_preset_browser.py; `contain: paint` was
   tried first and does not help. currentColor keeps the ink readable on both
   filled and outlined buttons without a second rule. */
html[data-portal-preset="material"] .btn { position: relative; overflow: hidden; }
html[data-portal-preset="material"] .btn::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(circle, currentColor 12%, transparent 12.5%);
  opacity: 0; transform: scale(0);
  transition: transform .5s var(--motion-easing), opacity .4s;
}
html[data-portal-preset="material"] .btn:active::after {
  opacity: .22; transform: scale(8);
  transition: transform .5s var(--motion-easing), opacity 0s;
}

/* ── Neumorphism ──────────────────────────────────────────────────────────
   Surfaces are EXTRUDED from the background rather than floating above it, so
   background and surface share one colour and depth comes from paired
   light/dark shadows. The style is low-contrast by nature; text and border
   values here are pushed darker than a typical neumorphic palette so it stays
   readable rather than merely fashionable. */
html[data-portal-preset="neumorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="neumorphism"] body.auth-page {
  --bg:            #E0E5EC;
  --surface:       #E0E5EC;
  --text:          #2D3748;
  --muted:         #4A5568;
  --border:        #C8D0DA;
  --primary:       #5A67D8;
  --primary-dark:  #434190;
  --primary-light: #E6E9FB;

  --radius:       1rem;
  --border-width: 0;
  /* Two shadows on opposite corners: one highlight, one shade. */
  --shadow-sm: 3px 3px 6px #B8BEC7, -3px -3px 6px #FFFFFF;
  --shadow:    6px 6px 12px #B8BEC7, -6px -6px 12px #FFFFFF;
  --shadow-lg: 12px 12px 24px #B0B7C0, -12px -12px 24px #FFFFFF;

  --portal-font-family:     Nunito, system-ui, -apple-system, sans-serif;
  --heading-font-family:    Nunito, system-ui, -apple-system, sans-serif;
  --letter-spacing-heading: -.01em;
  --motion-easing: cubic-bezier(.4, 0, .2, 1);

  --bs-body-bg: #E0E5EC;
}
/* Pressing INVERTS the extrusion so the control sinks into the page, which is
   the entire idiom. */
html[data-portal-preset="neumorphism"] .btn:active,
html[data-portal-preset="neumorphism"] .sidebar-link.active {
  box-shadow: inset 4px 4px 8px #B8BEC7, inset -4px -4px 8px #FFFFFF;
}

/* ── Skeuomorphism ────────────────────────────────────────────────────────
   Surfaces imitate physical material: vertical gradient, inset top highlight,
   visible bevel, cast shadow beneath. */
html[data-portal-preset="skeuomorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="skeuomorphism"] body.auth-page {
  --bg:            #D9D2C5;
  --surface:       #F4EFE6;
  --text:          #3B302A;
  --muted:         #6B5D52;
  --border:        #A89880;
  --primary:       #8B5E34;
  --primary-dark:  #5C3D21;
  --primary-light: #EFE2D0;

  --radius:       .5rem;
  --border-width: 1px;
  --shadow-sm: inset 0 1px 0 rgba(255,255,255,.7), 0 1px 2px rgba(59,48,42,.25);
  --shadow:    inset 0 1px 0 rgba(255,255,255,.75), 0 3px 6px rgba(59,48,42,.30);
  --shadow-lg: inset 0 1px 0 rgba(255,255,255,.8), 0 10px 20px rgba(59,48,42,.35);

  --portal-font-family:     Georgia, "Times New Roman", serif;
  --heading-font-family:    Georgia, "Times New Roman", serif;
  --letter-spacing-heading: 0;
  --motion-easing: cubic-bezier(.25, .1, .25, 1);

  --bs-body-bg: #D9D2C5;
}
html[data-portal-preset="skeuomorphism"] .card,
html[data-portal-preset="skeuomorphism"] .btn {
  background-image: linear-gradient(180deg, rgba(255,255,255,.55), rgba(0,0,0,.05));
  border: 1px solid #A89880;
}

/* ── Glassmorphism ────────────────────────────────────────────────────────
   Translucent panels over a coloured field, blurred behind, with a bright 1px
   edge. The body gradient is load-bearing: frosted glass over a flat colour
   looks like grey plastic -- it needs something behind it to refract. */
html[data-portal-preset="glassmorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="glassmorphism"] body.auth-page {
  --bg:            #0F172A;
  --surface:       rgba(255,255,255,.09);
  --text:          #F1F5F9;
  --muted:         #CBD5E1;
  --border:        rgba(255,255,255,.22);
  --primary:       #38BDF8;
  --primary-dark:  #0284C7;
  --primary-light: rgba(56,189,248,.18);

  /* Semantic TEXT tones, restated because this is a dark preset that was
     inheriting :root's light-optimised ones. The composited surface here is
     #252c3d (a 9% white veil over #0F172A), against which #c43838 measured
     2.64:1, #0b7e58 2.75:1, #9a6407 2.79:1 and #0a75a5 2.72:1 — all four
     failing, all four invisible to the old audit because a translucent
     --surface was skipped rather than composited.

     Same resolution the `dark` preset reached: on a dark ground the vivid
     values are already the accessible ones (5.03–6.49:1), so success/warning/
     info simply point at them, and only danger needs lightening. */
  --danger-text:   #F36D6D;   /* 5.04:1 — matches the `dark` preset's value */
  --success-text:  var(--success);
  --warning-text:  var(--warning);
  --info-text:     var(--info);

  --radius:       1.25rem;
  --border-width: 1px;
  --shadow-sm: 0 4px 16px rgba(2,6,23,.30);
  --shadow:    0 8px 32px rgba(2,6,23,.40);
  --shadow-lg: 0 20px 60px rgba(2,6,23,.55);

  --portal-font-family:     Inter, system-ui, -apple-system, sans-serif;
  --heading-font-family:    Inter, system-ui, -apple-system, sans-serif;
  --letter-spacing-heading: -.02em;
  --motion-easing: cubic-bezier(.4, 0, .2, 1);

  --bs-body-bg: #0F172A;
  background-image:
    radial-gradient(900px 500px at 12% 8%,  rgba(56,189,248,.30), transparent 60%),
    radial-gradient(800px 480px at 88% 92%, rgba(168,85,247,.28), transparent 60%);
  background-attachment: fixed;
}
html[data-portal-preset="glassmorphism"] .card,
html[data-portal-preset="glassmorphism"] .hrms-sidebar,
html[data-portal-preset="glassmorphism"] .portal-topbar {
  backdrop-filter: blur(16px) saturate(160%);
  -webkit-backdrop-filter: blur(16px) saturate(160%);
  border: 1px solid rgba(255,255,255,.22);
}

/* ── Polymorphism ─────────────────────────────────────────────────────────
   Many shapes, not one: radii differ per component so the interface reads as
   assembled from distinct parts. Flat saturated fills with a hard offset
   shadow (no blur) give it a graphic, printed feel. */
html[data-portal-preset="polymorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="polymorphism"] body.auth-page {
  --bg:            #FFF8F0;
  --surface:       #FFFFFF;
  --text:          #1A1A2E;
  --muted:         #55556B;
  --border:        #1A1A2E;
  --primary:       #FF5B5B;
  --primary-dark:  #D93A3A;
  --primary-light: #FFE8E8;

  --radius:       .75rem;
  --border-width: 2px;
  /* Hard offset, zero blur -- the shape reads as a cut-out on paper. */
  --shadow-sm: 2px 2px 0 #1A1A2E;
  --shadow:    4px 4px 0 #1A1A2E;
  --shadow-lg: 8px 8px 0 #1A1A2E;

  --portal-font-family:     "Space Grotesk", system-ui, sans-serif;
  --heading-font-family:    "Space Grotesk", system-ui, sans-serif;
  --letter-spacing-heading: -.02em;
  --motion-easing: cubic-bezier(.68, -.55, .27, 1.55);

  --bs-body-bg: #FFF8F0;
}
/* Different components, different silhouettes -- the "poly" in the name. */
html[data-portal-preset="polymorphism"] .card  { border-radius: 1.25rem .35rem 1.25rem .35rem; }
html[data-portal-preset="polymorphism"] .btn   { border-radius: 999px; border: 2px solid #1A1A2E; }
html[data-portal-preset="polymorphism"] .badge { border-radius: .2rem; }
html[data-portal-preset="polymorphism"] .btn:active {
  transform: translate(2px, 2px); box-shadow: 2px 2px 0 #1A1A2E;
}

/* ── Flat Design ──────────────────────────────────────────────────────────
   No depth at all: no shadows, no gradients, no radius. Hierarchy comes from
   colour and spacing alone, which is the discipline of the style. */
html[data-portal-preset="flat"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="flat"] body.auth-page {
  --bg:            #ECF0F1;
  --surface:       #FFFFFF;
  --text:          #2C3E50;
  --muted:         #7F8C8D;
  --border:        #BDC3C7;
  --primary:       #16A085;
  --primary-dark:  #0E6E5B;
  --primary-light: #E8F6F3;

  /* 0px, NOT a bare 0. The derived scale multiplies this token, and
     `calc(0 * .7)` types as a <number>, not a <length> -- an invalid
     border-radius, so the whole declaration is dropped and the element keeps
     whatever radius it would otherwise have had. Flat would silently stay
     rounded. Every other preset writes a unit already. */
  --radius:       0px;
  --border-width: 1px;
  --shadow-sm: none;
  --shadow:    none;
  --shadow-lg: none;

  --portal-font-family:     "Open Sans", system-ui, -apple-system, sans-serif;
  --heading-font-family:    "Open Sans", system-ui, -apple-system, sans-serif;
  --letter-spacing-heading: 0;
  /* Linear, not eased: flat design does not imitate physical momentum. */
  --motion-easing: linear;

  /* No depth, no gradients — the two things this style is defined by. A flat
     fill, not the two-stop brand gradient every other preset gets. */
  --brand-gradient: var(--primary);

  --bs-body-bg: #ECF0F1;
}
html[data-portal-preset="flat"] .card,
html[data-portal-preset="flat"] .btn,
html[data-portal-preset="flat"] .badge { box-shadow: none !important; }

/* The sidebar's box-shadow is doing two different jobs in one declaration: a
   1px rail that stands in for a border, and 24px of ambient depth. An EDGE is
   not elevation, so Flat keeps the rail and drops the blur — the alternative,
   letting the token zero the whole thing, leaves a 240px dark column butting
   straight into the page with no separation at all. This cannot be expressed
   by pointing the rule at a token, hence the one preset-scoped rule. */
html[data-portal-preset="flat"] .hrms-sidebar {
  box-shadow: 1px 0 0 var(--sidebar-border) !important;
}

/* Anyone who has asked for reduced motion gets none of the idioms above that
   move. Covers all six. */
@media (prefers-reduced-motion: reduce) {
  html[data-portal-preset="material"] .btn::after,
  html[data-portal-preset="polymorphism"] .btn:active {
    transition: none; transform: none;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   PRESET DIFFERENTIATION
   ─────────────────────────────────────────────────────────────────────────
   Five presets shared a structural signature with another preset and so read
   as the same theme in a different hue:

     ocean   was modern  re-tinted   (identical radius/border/shadow/easing)
     red     was blue    re-tinted   (likewise)
     nord    ) all three carried the generic soft-blur ambient shadow and the
     emerald ) stock cubic-bezier(.4,0,.2,1), differing only in accent colour

   Each now commits to its own treatment of depth, edge, typography and
   motion. "default" is deliberately left alone: it is the neutral baseline
   the others are read against.

   These come LAST so they win over the original blocks above; only the
   properties that carry the identity are restated.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Ocean — light through water ──────────────────────────────────────────
   Was a re-tinted Modern. Its own idiom: nothing sits on a flat plane, it is
   suspended in a graded medium. Wide, very soft, deeply offset shadows (light
   diffusing through water) instead of Modern's tight floaty ones, a fixed
   vertical gradient behind everything, and airy positive heading tracking —
   the exact opposite of Modern's tight -.03em. */
html[data-portal-preset="ocean"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="ocean"] body.auth-page {
  --radius:       1rem;
  --border-width: 1px;
  --shadow-sm: inset 0 1px 0 rgba(255,255,255,.55), 0 2px 10px -4px rgba(12,74,90,.18);
  --shadow:    inset 0 1px 0 rgba(255,255,255,.60), 0 18px 40px -14px rgba(12,74,90,.28);
  --shadow-lg: inset 0 1px 0 rgba(255,255,255,.65), 0 36px 80px -24px rgba(12,74,90,.38);
  --portal-font-family:     system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --heading-font-family:    system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --letter-spacing-heading: .015em;
  --portal-line-height:     1.65;
  /* Slow settle, as though moving through something denser than air. */
  --motion-easing: cubic-bezier(.22, .61, .36, 1);

  background-image: linear-gradient(180deg, #F2FAFC 0%, #DCEEF3 55%, #C7E2EA 100%);
  background-attachment: fixed;
}
/* The surface catches the light at its top edge; without this the cards read
   as flat rectangles floating on the gradient. */
html[data-portal-preset="ocean"] .card,
html[data-portal-preset="ocean"] .kpi-card {
  background-image: linear-gradient(180deg, rgba(255,255,255,.75), rgba(255,255,255,0) 42%);
}

/* ── Red — editorial masthead ─────────────────────────────────────────────
   Was structurally identical to Blue. Its own idiom is assertion: a heavy
   frame, a tight radius, a crimson ring rather than a soft drop, and a rule
   down the left of every card — a print/alert motif no other preset uses.
   Uppercase tracked headings finish the masthead read. */
html[data-portal-preset="red"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="red"] body.auth-page {
  --radius:       .25rem;
  --border-width: 2px;
  --shadow-sm: 0 0 0 1px rgba(220,38,38,.14);
  --shadow:    0 0 0 1px rgba(220,38,38,.22), 0 2px 6px rgba(58,20,20,.14);
  --shadow-lg: 0 0 0 1px rgba(220,38,38,.28), 0 8px 20px rgba(58,20,20,.22);
  --letter-spacing-heading: .06em;
  --motion-easing: cubic-bezier(.4, 0, .6, 1);
}
html[data-portal-preset="red"] .card,
html[data-portal-preset="red"] .kpi-card { border-left: 4px solid var(--primary); }
html[data-portal-preset="red"] h1,
html[data-portal-preset="red"] h2,
html[data-portal-preset="red"] h3,
html[data-portal-preset="red"] .h1,
html[data-portal-preset="red"] .h2,
html[data-portal-preset="red"] .h3 { text-transform: uppercase; }

/* ── Blue — instrument panel ──────────────────────────────────────────────
   Kept the corporate register but made it dense and engineered rather than
   generically soft: smaller type, tighter leading, a crisp stepped shadow
   with no colour cast, and a rule across the TOP of each card (distinct from
   Red's left rule) so a screen of cards reads as a report. */
html[data-portal-preset="blue"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="blue"] body.auth-page {
  --radius:       .375rem;
  --border-width: 1px;
  --shadow-sm: 0 1px 0 rgba(15,30,51,.07);
  --shadow:    0 1px 0 rgba(15,30,51,.09), 0 2px 5px rgba(15,30,51,.10);
  --shadow-lg: 0 1px 0 rgba(15,30,51,.10), 0 10px 24px -6px rgba(15,30,51,.18);
  --portal-font-size:       .9375rem;
  --portal-line-height:     1.5;
  --letter-spacing-heading: -.005em;
  /* Even, mechanical -- no overshoot, no lingering tail. */
  --motion-easing: cubic-bezier(.3, 0, .2, 1);
}
html[data-portal-preset="blue"] .card,
html[data-portal-preset="blue"] .kpi-card { border-top: 3px solid var(--primary); }

/* ── Nord — arctic, definition without depth ──────────────────────────────
   Nord is a code-editor palette and its character is low contrast and calm;
   drop shadows are foreign to it. Structure comes from hairline borders and
   square corners instead, and headings take a monospaced face -- the editor
   origin made literal, and a register nothing else here uses. */
html[data-portal-preset="nord"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="nord"] body.auth-page {
  --radius:       .25rem;
  --border-width: 1px;
  --shadow-sm: none;
  --shadow:    0 1px 0 rgba(46,52,64,.10);
  --shadow-lg: 0 2px 0 rgba(46,52,64,.10), 0 6px 16px -8px rgba(46,52,64,.20);
  --heading-font-family: ui-monospace, "Cascadia Code", "JetBrains Mono",
                         "SF Mono", Menlo, Consolas, monospace;
  --letter-spacing-heading: 0;
  --motion-easing: cubic-bezier(.45, .05, .55, .95);
}
/* With shadows gone the border is the only separator, so it has to be real. */
html[data-portal-preset="nord"] .card,
html[data-portal-preset="nord"] .kpi-card { border-color: #C2CAD6; }

/* ── Emerald — luxury print ───────────────────────────────────────────────
   The gold accent existed only in the sidebar. Here it becomes the edge of
   every surface: a gold hairline ring over a deep, soft green shadow. The
   heading serif is an old-style face, deliberately a different serif register
   from Classic's slabby Georgia. */
html[data-portal-preset="emerald"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="emerald"] body.auth-page {
  --radius:       .875rem;
  --border-width: 1px;
  --shadow-sm: 0 0 0 1px rgba(212,175,55,.20), 0 1px 3px rgba(4,55,39,.08);
  --shadow:    0 0 0 1px rgba(212,175,55,.30), 0 10px 28px -8px rgba(4,55,39,.20);
  --shadow-lg: 0 0 0 1px rgba(212,175,55,.38), 0 26px 60px -16px rgba(4,55,39,.30);
  --heading-font-family: "Iowan Old Style", "Palatino Linotype", Palatino,
                         "Book Antiqua", Georgia, serif;
  --letter-spacing-heading: .01em;
  --portal-line-height:     1.65;
  --motion-easing: cubic-bezier(.2, .8, .2, 1);
}

/* ── Inputs on the borderless presets ─────────────────────────────────────
   --border-width feeds --bs-border-width, so a preset that sets it to 0 for
   its cards also strips the border off every text input. That is correct for
   a card and wrong for a field somebody has to click into, so each of those
   presets restores an edge in its own idiom. */
html[data-portal-preset="material"] .form-control,
html[data-portal-preset="material"] .form-select {
  border: 0;
  border-bottom: 1px solid var(--border);
  border-radius: .25rem .25rem 0 0;
  background-color: #F1F3F4;
}
html[data-portal-preset="material"] .form-control:focus,
html[data-portal-preset="material"] .form-select:focus {
  border-bottom: 2px solid var(--primary);
  box-shadow: none;
}
html[data-portal-preset="neumorphism"] .form-control,
html[data-portal-preset="neumorphism"] .form-select {
  border: 0;
  background-color: var(--surface);
  box-shadow: inset 3px 3px 6px #B8BEC7, inset -3px -3px 6px #FFFFFF;
}
html[data-portal-preset="neumorphism"] .form-control:focus,
html[data-portal-preset="neumorphism"] .form-select:focus {
  box-shadow: inset 3px 3px 6px #B8BEC7, inset -3px -3px 6px #FFFFFF,
              0 0 0 2px rgba(76,86,192,.40);
}

/* ── Contrast corrections for the design-language presets ─────────────────
   The WCAG audit (apps/system_settings/tests/test_preset_contrast.py) picks up
   new presets automatically, and it caught 15 failures across these four. Two
   causes, both structural rather than careless colour picking:

     * --border-strong and the four --*-text tokens are inherited from :root,
       where they were solved against a WHITE surface. A preset whose surface
       is #E0E5EC or #F4EFE6 has to solve them again against its own.
     * Neumorphism and Flat are low-contrast styles BY DESIGN, and Polymorphism
       is built on a hot coral. Left alone, the fashionable value wins over the
       readable one -- which is exactly the criticism these styles attract.

   Values below come from the audit's own solver (`suggest_accessible`), so
   each is the smallest change that clears the threshold. KNOWN_GAPS stays
   empty. */

html[data-portal-preset="neumorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="neumorphism"] body.auth-page {
  /* Everything sits on #E0E5EC, ~12% darker than the white these were solved
     against, so all six needed re-solving. */
  --border-strong: #7D838B;
  --primary:       #4C56C0;
  /* Re-solved 2026-08-18 against the status-CHIP ground (a ~14% tint of the
     vivid counterpart composited on --bg) rather than bare --bg. This preset
     has no white anywhere -- --surface IS --bg -- so the tint has nothing
     light to sit on and every one of these fell to 3.84-4.23:1. */
  --danger-text:   #A73030;
  --success-text:  #096B4B;
  --warning-text:  #865706;
  --info-text:     #096892;
}

html[data-portal-preset="skeuomorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="skeuomorphism"] body.auth-page {
  --muted:         #66584E;
  /* Warm to match the palette; the inherited value is a cool grey that reads
     as a foreign object against parchment. */
  --border-strong: #8A7B66;
  /* Re-solved 2026-08-18 against the status-CHIP ground, as for neumorphism
     above. The parchment canvas (#D9D2C5) is the dimmest of the 21 light
     presets, so these were the worst in the product at 3.11-3.29:1.
     --danger-text is absent deliberately: the darkened :root value already
     clears 4.50:1 here, and a redundant override is one more thing to drift. */
  --success-text:  #095F43;
  --warning-text:  #794E06;
  --info-text:     #085E84;
}

html[data-portal-preset="polymorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="polymorphism"] body.auth-page {
  /* #FF5B5B measured 3.04:1 as link text. The coral survives everywhere it is
     decorative -- tints, borders, the hard offset shadow -- but the token that
     also paints link text has to be readable. */
  --primary:      #D93A3A;
  --primary-dark: #B02525;
}

html[data-portal-preset="flat"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="flat"] body.auth-page {
  /* With no shadows, no gradients and no radius, colour is the ONLY carrier of
     hierarchy in this preset -- so it is the one where weak contrast does the
     most damage. --muted is solved against the page bg, the harder of its two
     backgrounds. */
  --muted:   #646F6F;
  --primary: #12856E;
}

/* ══════════════════════════════════════════════════════════════════════════
   ANIMATION: ON  (data-portal-motion="full")
   ─────────────────────────────────────────────────────────────────────────
   The top level used to be "smooth" and added nothing — it was the absence of
   the Reduced and Off overrides, so choosing it looked exactly like choosing
   nothing. This is the level that actually animates.

   Everything below is opacity/transform only, so it runs on the compositor
   and never triggers layout. Each preset's own --motion-easing drives it, so
   On feels different under Material than under Neumorphism rather than
   flattening them all to one curve.

   The whole section sits inside `prefers-reduced-motion: no-preference`.
   WCAG 2.3.3 makes that an entitlement rather than a preference, so an
   administrator can turn motion DOWN for everyone but can never turn it back
   on for somebody whose system asked for less.
   ══════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: no-preference) {

  /* ── Entrance ───────────────────────────────────────────────────────────
     Cards rise in as the page paints. Runs on `animation`, not on a JS class,
     so there is no flash of hidden content if a script fails: the keyframes
     start at opacity 0 but `animation-fill-mode: backwards` only applies that
     during the delay, and a browser that ignores the animation entirely still
     paints the resting state. */
  @keyframes hrms-motion-rise {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes hrms-motion-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  html[data-portal-motion="full"] body.portal-page .card,
  html[data-portal-motion="full"] body.portal-page .kpi-card,
  html[data-portal-motion="full"] body.portal-page .stg-card,
  html[data-portal-motion="full"] body.portal-page .feature-card {
    animation: hrms-motion-rise .42s var(--motion-easing, cubic-bezier(.4,0,.2,1)) backwards;
  }
  /* A short cascade down the page. Capped at six: beyond that the last card
     is waiting long enough to read as lag rather than as choreography. */
  html[data-portal-motion="full"] body.portal-page .card:nth-child(1)  { animation-delay: 0s; }
  html[data-portal-motion="full"] body.portal-page .card:nth-child(2)  { animation-delay: .05s; }
  html[data-portal-motion="full"] body.portal-page .card:nth-child(3)  { animation-delay: .10s; }
  html[data-portal-motion="full"] body.portal-page .card:nth-child(4)  { animation-delay: .15s; }
  html[data-portal-motion="full"] body.portal-page .card:nth-child(5)  { animation-delay: .20s; }
  html[data-portal-motion="full"] body.portal-page .card:nth-child(n+6) { animation-delay: .25s; }

  /* A modal or offcanvas has its own entrance and animates every time it
     opens; layering a second rise on the card inside it reads as a stutter. */
  html[data-portal-motion="full"] .modal .card,
  html[data-portal-motion="full"] .offcanvas .card,
  html[data-portal-motion="full"] .dropdown-menu .card { animation: none; }

  html[data-portal-motion="full"] body.portal-page .portal-topbar {
    animation: hrms-motion-fade .3s var(--motion-easing, ease) backwards;
  }

  /* ── Hover: surfaces lift ───────────────────────────────────────────────
     .card already declares a transition for box-shadow AND transform; until
     now nothing ever set the transform, so half of that declaration was
     inert. */
  /* .card gets shadow only. A transform would make it a containing block for
     fixed-position descendants and trap any dropdown opened inside it; since
     opening one requires hovering the card, the lift and the menu always
     collided. .kpi-card keeps the movement -- it never contains a menu. */
  html[data-portal-motion="full"] body.portal-page .card:hover {
    box-shadow: var(--shadow-lg);
  }
  html[data-portal-motion="full"] body.portal-page .kpi-card:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow-lg);
  }

  /* ── Press: controls give way ───────────────────────────────────────────
     Scale rather than translate, so a button does not shift under the cursor
     mid-click. Presets that own their press idiom (Neumorphism inverts its
     extrusion, Polymorphism drops into its shadow) are left to it. */
  html[data-portal-motion="full"] .btn {
    transition: transform .12s var(--motion-easing, ease),
                box-shadow .2s var(--motion-easing, ease),
                background-color .2s var(--motion-easing, ease);
  }
  html[data-portal-motion="full"]:not([data-portal-preset="neumorphism"]):not([data-portal-preset="polymorphism"]) .btn:active {
    transform: scale(.97);
  }

  /* ── Navigation ─────────────────────────────────────────────────────────
     A short slide toward the content on hover; the active item stays put so
     the current location never appears to drift. */
  html[data-portal-motion="full"] .sidebar-link {
    transition: transform .18s var(--motion-easing, ease),
                background-color .18s var(--motion-easing, ease),
                color .18s var(--motion-easing, ease);
  }
  html[data-portal-motion="full"] .sidebar-link:hover:not(.active) { transform: translateX(3px); }

  /* ── Tables ─────────────────────────────────────────────────────────────
     Rows get a tint transition only. A row is a hit target in a dense list,
     and moving one under the pointer costs accuracy for no information. */
  html[data-portal-motion="full"] body.portal-page .table tbody tr {
    transition: background-color .15s var(--motion-easing, ease);
  }
}

/* Reduced and Off already neutralise all of the above through the global
   duration/none overrides earlier in this file: those use !important and
   match every element, including the animations declared here. */

/* ══════════════════════════════════════════════════════════════════════════
   DESIGN-LANGUAGE PRESETS — the tokens the first pass left out
   ─────────────────────────────────────────────────────────────────────────
   The six presets above set the HRMS tokens (--bg / --surface / --text /
   --primary / depth / edge) but not the two OTHER families every one of the
   sixteen colour presets also sets: the Bootstrap mirrors (--bs-*) and the
   thirteen sidebar tokens.

   Anything reading those fell through to :root, whose values are solved
   against a white page. The result was invisible UI rather than merely ugly
   UI, and none of it is visible in the stylesheet -- a static audit compares
   the pairs a preset DECLARES, and these were exactly the pairs it did not.
   The browser sweep (apps/system_settings/tests/test_preset_browser.py) found
   them by reading composed pages:

     glassmorphism  .stg-sidebar-title   #f1f5f9 on #f8f9fa   1.03:1
     glassmorphism  dashboard captions   #1d2229 on #0f172a   1.11:1
     neumorphism    topbar nav links     #617187 on #e0e5ec   3.93:1
     skeuomorphism  topbar nav links     #617187 on #f4efe6   4.34:1

   White on white, and near-black on near-black.

   The mirrors are declared as var() references rather than repeated literals,
   so they cannot drift from the values the contrast audit already checks.
   ══════════════════════════════════════════════════════════════════════════ */

html[data-portal-preset="material"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="material"] body.auth-page,
html[data-portal-preset="neumorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="neumorphism"] body.auth-page,
html[data-portal-preset="skeuomorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="skeuomorphism"] body.auth-page,
html[data-portal-preset="glassmorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="glassmorphism"] body.auth-page,
html[data-portal-preset="polymorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="polymorphism"] body.auth-page,
html[data-portal-preset="flat"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="flat"] body.auth-page {
  --bs-body-color:       var(--text);
  --bs-secondary-color:  var(--muted);
  --bs-border-color:     var(--border);
  --bs-tertiary-bg:      var(--surface);
  /* `--primary-text`, NOT `--primary`. This is the same three-role split the
     token block at the top of the file documents: `--primary` is a FILL,
     `--primary-text` is the value of that hue that is safe as INK. The bare
     `a {}` rule was moved onto `--primary-text` when that split landed; this
     Bootstrap bridge was not, so everything Bootstrap colours through
     `--bs-link-color` -- `.nav-link`, `.page-link`, `.dropdown-item:active`,
     `.link-primary` -- kept the fill value.

     Measured on Flat: #12856E on the page canvas #ECF0F1 is 3.97:1, and it
     was the single largest contrast family left in the product (16 of 41
     violations, all `.nav-link` filter tabs on /me/ screens). `--primary-text`
     is #0E6E5B there and clears AA on the same ground -- which is exactly what
     `Pair("link text on page bg", "primary-text", "bg")` in
     apps/system_settings/preset_audit.py already asserts. The audit was right
     and the product was not reading the token it audits. */
  --bs-link-color:       var(--primary-text, var(--primary));
  --bs-link-hover-color: var(--primary-dark);
  /* Bootstrap's stock #d63384 measures 3.74:1 on Neumorphism's surface. One
     value that clears AA on all six. */
  --bs-code-color:       #B02765;
}

/* ── Sidebar palettes ─────────────────────────────────────────────────────
   Stated per preset rather than inherited: the :root sidebar is a dark slate
   panel, which is right for Simple and wrong for a white Material drawer or
   a parchment Skeuomorphic one. Every value below clears AA against its own
   sidebar background. */

/* Material: the nav drawer is a white surface with dark text, not a dark
   rail. Elevation separates it from the content, not colour. */
html[data-portal-preset="material"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="material"] body.auth-page {
  --sidebar-bg:                  #FFFFFF;
  --sidebar-text:                #5F6368;   /* 6.05:1 */
  --sidebar-text-hover:          #202124;
  --sidebar-text-active:         #0D47A1;
  --sidebar-icon-active:         #1565C0;
  --sidebar-hover-bg:            rgba(60,64,67,.08);
  --sidebar-active-bg:           rgba(21,101,192,.12);
  --sidebar-active-border:       #1565C0;
  --sidebar-section-label:       #5F6368;
  --sidebar-section-label-hover: #202124;
  --sidebar-toggle-hover-bg:     rgba(60,64,67,.06);
  --sidebar-group-accent:        rgba(21,101,192,.16);
  --sidebar-border:              #DADCE0;
}

/* Neumorphism: one colour everywhere. A contrasting sidebar would break the
   single-surface illusion the whole style rests on. */
html[data-portal-preset="neumorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="neumorphism"] body.auth-page {
  --sidebar-bg:                  #E0E5EC;
  --sidebar-text:                #4A5568;   /* 5.94:1 */
  --sidebar-text-hover:          #2D3748;
  --sidebar-text-active:         #3B4499;
  --sidebar-icon-active:         #4C56C0;
  --sidebar-hover-bg:            rgba(184,190,199,.35);
  --sidebar-active-bg:           rgba(76,86,192,.12);
  --sidebar-active-border:       #4C56C0;
  --sidebar-section-label:       #4A5568;
  --sidebar-section-label-hover: #2D3748;
  --sidebar-toggle-hover-bg:     rgba(184,190,199,.25);
  --sidebar-group-accent:        rgba(76,86,192,.16);
  --sidebar-border:              #C2C9D4;
}

/* Skeuomorphism: dark stained wood against the parchment content area. */
html[data-portal-preset="skeuomorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="skeuomorphism"] body.auth-page {
  --sidebar-bg:                  #4A4038;
  --sidebar-text:                #D6C9B4;   /* 6.18:1 */
  --sidebar-text-hover:          #F4EFE6;
  --sidebar-text-active:         #F0D9B0;
  --sidebar-icon-active:         #E0B070;
  --sidebar-hover-bg:            rgba(255,255,255,.07);
  --sidebar-active-bg:           rgba(224,176,112,.18);
  --sidebar-active-border:       #E0B070;
  --sidebar-section-label:       #c5baac;
  --sidebar-section-label-hover: #D6C9B4;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.05);
  --sidebar-group-accent:        rgba(224,176,112,.20);
  --sidebar-border:              rgba(255,255,255,.12);
}

/* Glassmorphism: a solid, slightly-lifted panel rather than a transparent
   one. The sidebar is the only always-visible chrome, and text over a moving
   gradient is unreadable at any opacity -- the frosting belongs on cards. */
html[data-portal-preset="glassmorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="glassmorphism"] body.auth-page {
  --sidebar-bg:                  #16203A;
  --sidebar-text:                #CBD5E1;   /* 10.87:1 */
  --sidebar-text-hover:          #F1F5F9;
  --sidebar-text-active:         #E0F2FE;
  --sidebar-icon-active:         #38BDF8;
  --sidebar-hover-bg:            rgba(255,255,255,.07);
  --sidebar-active-bg:           rgba(56,189,248,.18);
  --sidebar-active-border:       #38BDF8;
  --sidebar-section-label:       #94A3B8;   /* 6.29:1 */
  --sidebar-section-label-hover: #CBD5E1;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.05);
  --sidebar-group-accent:        rgba(56,189,248,.20);
  --sidebar-border:              rgba(255,255,255,.14);
}

/* Polymorphism: the same ink the borders and hard shadows are drawn in, so
   the sidebar reads as one more cut-out shape. */
html[data-portal-preset="polymorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="polymorphism"] body.auth-page {
  --sidebar-bg:                  #1A1A2E;
  --sidebar-text:                #C9C9D4;   /* 10.39:1 */
  --sidebar-text-hover:          #FFFFFF;
  --sidebar-text-active:         #FFD5D5;
  /* The coral is lightened for the dark rail; #C22F2F would be 2:1 here. */
  --sidebar-icon-active:         #FF8A8A;
  --sidebar-hover-bg:            rgba(255,255,255,.08);
  --sidebar-active-bg:           rgba(255,138,138,.18);
  --sidebar-active-border:       #FF8A8A;
  --sidebar-section-label:       #A0A0B0;   /* 6.62:1 */
  --sidebar-section-label-hover: #C9C9D4;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.06);
  --sidebar-group-accent:        rgba(255,138,138,.20);
  --sidebar-border:              rgba(255,255,255,.16);
}

/* Flat: the canonical midnight-blue rail, no depth of any kind. */
html[data-portal-preset="flat"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="flat"] body.auth-page {
  --sidebar-bg:                  #2C3E50;
  --sidebar-text:                #BDC3C7;   /* 6.17:1 */
  --sidebar-text-hover:          #ECF0F1;
  --sidebar-text-active:         #A3E4D7;
  --sidebar-icon-active:         #1ABC9C;
  --sidebar-hover-bg:            rgba(255,255,255,.07);
  --sidebar-active-bg:           rgba(26,188,156,.20);
  --sidebar-active-border:       #1ABC9C;
  /* #95A5A6 is the Flat UI stock grey and measures 4.29:1 here. */
  --sidebar-section-label:       #BDC3C7;
  --sidebar-section-label-hover: #ECF0F1;
  --sidebar-toggle-hover-bg:     rgba(255,255,255,.05);
  --sidebar-group-accent:        rgba(26,188,156,.20);
  --sidebar-border:              rgba(255,255,255,.12);
}

/* ── Primary as small text ────────────────────────────────────────────────
   Both of these cleared AA against their surface as body text and failed as
   12px links on the dashboard, where the same token is used at a smaller
   size. Darkened until they clear on the page background too. */
html[data-portal-preset="material"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="material"] body.auth-page {
  --primary:      #1565C0;   /* was #1976D2 -- 4.40:1 at 12px */
  --primary-dark: #0D47A1;
}
html[data-portal-preset="polymorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="polymorphism"] body.auth-page {
  --primary:      #C22F2F;   /* was #D93A3A -- 4.31:1 at 12px */
  --primary-dark: #9E2020;
}

/* ── Two colours a single global value cannot serve ───────────────────────
   Both found by the browser sweep on composed pages. */

/* Inline <code>. #B02765 clears AA on five of the six design languages and
   measures 2.83:1 on Glassmorphism's near-black page and 4.20:1 on
   Skeuomorphism's darker parchment BACKGROUND (it passes on that preset's
   lighter surface, so it depends on which of the two the snippet lands on). */
html[data-portal-preset="glassmorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="glassmorphism"] body.auth-page { --bs-code-color: #F0A0C0; }
html[data-portal-preset="skeuomorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="skeuomorphism"] body.auth-page { --bs-code-color: #A72560; }

/* Glassmorphism's accent is a bright sky cyan. It is excellent as link text
   on the dark page (7.5:1) and impossible under white button text (2.14:1) --
   the same token doing both jobs. Keeping the accent and darkening the text
   on it, rather than dulling the accent everywhere for the sake of buttons.

   That reasoning was right and is now the general mechanism: this used to be
   a per-preset `.btn-primary { color: #0F172A }` override, which fixed the
   button here and left the identical failure unfixed on the `dark` preset,
   because nothing connected the two. It is a token now (--on-primary), so a
   new preset either inherits a safe default or is caught by
   apps/system_settings/preset_audit.py. --primary-hover lightens for the same
   reason as on `dark`. */
html[data-portal-preset="glassmorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="glassmorphism"] body.auth-page {
  --on-primary:    #0F172A;   /* 8.81:1 on --primary */
  --primary-hover: #7DD3FC;   /* 11.32:1 under --on-primary */
}

/* ── --primary-text per design language ───────────────────────────────────
   "The accent, at a value that is safe as TEXT." Distinct from --primary
   (fills, borders) and from --primary-dark (a hover state). The five light
   presets darken; Glassmorphism LIGHTENS, because on a dark panel that is the
   direction that gains contrast. Consumed by the active settings-nav item. */
html[data-portal-preset="material"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="material"] body.auth-page      { --primary-text: #0D47A1; }
html[data-portal-preset="neumorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="neumorphism"] body.auth-page   { --primary-text: #434190; }
html[data-portal-preset="skeuomorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="skeuomorphism"] body.auth-page { --primary-text: #5C3D21; }
html[data-portal-preset="glassmorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="glassmorphism"] body.auth-page { --primary-text: #7DD3FC; }
html[data-portal-preset="polymorphism"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="polymorphism"] body.auth-page  { --primary-text: #9E2020; }
html[data-portal-preset="flat"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="flat"] body.auth-page          { --primary-text: #0E6E5B; }

/* ── Inline <code> ────────────────────────────────────────────────────────
   Bootstrap ships #d63384, which is tuned for white and measures 3.93:1 on
   Ocean's surface and worse on the softer ones. One darker value serves every
   light preset; the dark ones need to go the other way. */
:root { --bs-code-color: #B02765; }
[data-bs-theme="dark"] { --bs-code-color: #F0A0C0; }
html[data-portal-preset="dark"] body:is(.portal-page,.public-cms-page,.themed-page),
html[data-portal-preset="dark"] body.auth-page { --bs-code-color: #F0A0C0; }

/* The active settings-nav label takes --primary-dark, which gains contrast on
   a light tint and LOSES it on a dark one. Glassmorphism is the only preset
   with a dark panel there, so it lightens instead: #0284C7 measured 2.40:1 on
   its own tint, #7DD3FC measures 5.91:1. */
html[data-portal-preset="glassmorphism"] .stg-nav-item.active { color: #7DD3FC; }


/* ── Carousel indicators: 24px touch target ───────────────────────────────
   Bootstrap draws these as a 3px bar with 10px transparent borders above and
   below, totalling 23px -- one pixel under WCAG 2.2 SC 2.5.8 (AA), which asks
   for 24x24. Measured on /demo/ at both 390px and 768px.

   The transparent border IS the hit area in Bootstrap's own design, so it is
   the thing to grow; the visible bar stays 3px and nothing moves. The
   spacing exception does not rescue these -- the dots sit right beside each
   other, so their 24px circles would overlap. */
.carousel-indicators [data-bs-slide-to] {
  border-top-width: 11px;
  border-bottom-width: 11px;
}

/* The height fix above was only half of it. Bootstrap lays the dots out in a
   flex row inside `padding: 0 15%`, and they are shrinkable: on the /demo/
   carousel, which has one dot per module, eight 30px dots plus their margins
   need 282px and only get ~207px at 320px. Flex takes the difference out of
   every dot equally, so they arrive at 20x25 -- back under the floor, in the
   other axis, on the narrowest screen where precision is worst.

   Two changes: stop them shrinking, and give them room to wrap onto a second
   row instead. A second row of dots is legible; eight 20px dots are not. */
@media (max-width: 575.98px) {
  .carousel-indicators {
    flex-wrap: wrap;
    row-gap: .35rem;
    padding-inline: 5%;
  }
  .carousel-indicators [data-bs-slide-to] { flex: 0 0 auto; min-width: 24px; }
}

/* == Touch ergonomics ======================================================
   Everything below is gated on `pointer: coarse`, not on a width. A target
   size is a property of the INPUT DEVICE: a 1024px tablet needs 44px and a
   768px-wide desktop window does not. Gating these on a breakpoint would both
   miss the tablet and shrink the desktop.

   Sizes found by ui_audit/mobile_sweep.py at 320/360/390 and listed with what
   they measured, so a regression is recognisable as one.
   ======================================================================= */

/* -- Password reveal button (login, register) -----------------------------
   Measured 16x16 -- the icon and nothing else, because the three buttons
   carried `padding:0;line-height:0` inline. It sits on the sign-in screen,
   which is the single most-used form in the product, and it is the control
   somebody reaches for precisely when they have already mistyped once.

   The class is shared by login.html and register.html (x2) and the inline
   box styling was removed from all three, so this is now the only definition
   of where that button is and how big it is. */
.pw-toggle-btn {
  position: absolute;
  right: .25rem;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--muted);
  cursor: pointer;
}
.pw-toggle-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
/* An input with a visibility toggle already has something at its right edge.
   Bootstrap draws its `.is-invalid` warning glyph as a BACKGROUND IMAGE at
   that same spot, so on a failed sign-up the eye and the red marker land on
   top of each other and read as one unrecognisable smudge. The red border
   and the message underneath already say "invalid"; the glyph is the
   redundant one, so it goes. `:has()` because the toggle is a sibling of the
   input, not an ancestor -- nothing on the input itself says it has one. */
.form-control.is-invalid:has(~ .pw-toggle-btn) { background-image: none; }
@media (hover: hover) {
  .pw-toggle-btn:hover { color: var(--text); background: var(--bg, #f1f5f9); }
}

@media (pointer: coarse) {
  /* -- Checkboxes and radios ---------------------------------------------
     Measured 15x15 (`privacy_consent` on /accounts/register/, and every
     checkbox in the product shares the rule). 1.5em lands exactly on the
     24x24 WCAG 2.2 AA floor at the default root size.

     `:not(.form-switch)` is load-bearing: a switch is the SAME
     `.form-check-input` class with `width: 2em` and a background-image track,
     so a blanket width would squash every toggle in Settings into a square.
     The `>` is deliberate too -- it limits this to well-formed Bootstrap
     check groups, where the compensating `padding-left` below is correct. */
  .form-check:not(.form-switch) { padding-left: 1.9em; }
  .form-check:not(.form-switch) > .form-check-input {
    /* `em`, so the box tracks whatever type size the form group is set in --
       and `min-*` in px, because it must not. 1.5em landed at 23x23 inside a
       .fs-sm group on /accounts/register/: correct arithmetic, one pixel short
       of the floor, which is exactly the failure a relative unit invites. */
    width: 1.5em;
    height: 1.5em;
    min-width: 24px;
    min-height: 24px;
    margin-top: .0625em;
    margin-left: -1.9em;
  }

  /* -- "See more" links in card headers -----------------------------------
     Measured 109x17, 110x17, 71x17 on the HR dashboard ("View full analytics
     ->", "Full calendar ->", "Monitor ->"). Each is the only way into the
     section it heads, each sits alone at the end of its header row, and none
     of them is a link inside a sentence -- so the inline-link exemption from
     SC 2.5.8 does not apply and 17px is simply too thin.

     `inline-flex` is required, not decoration: `min-height` does nothing at
     all on a plain inline box, so a rule that sets only the height silently
     changes nothing and reads as fixed. */
  .card-header a.fs-xs,
  .card-header a.fs-2xs,
  a.fs-xs.text-decoration-none {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
  }

  /* -- Role chips that are links ------------------------------------------
     `.rbac-tag` on /portal/org-units/ measured 43x20: a 12px chip with
     .08rem of vertical padding. It is a LINK -- it navigates to the role --
     so it is a target, not a label, and the eleven of them sit in a wrapped
     flex row where a mis-tap lands on a different role. Padding rather than
     min-height so the chips keep their shape and the row simply gets taller. */
  a.rbac-tag {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
    padding-block: .5rem;
  }

  /* -- Small "-> more" links, wherever they sit ---------------------------
     Scoped to `.card-header` in the first pass, which missed the two that
     matter most: the HR dashboard's "View full analytics ->" and "Browse media
     library ->" sit in plain divs. The class carries the meaning here -- a
     `.fs-xs` anchor is a deliberate small link, never body copy -- so the
     class is the right hook and the container is not. */
  a.fs-xs,
  a.fs-2xs,
  /* `.form-text` is Bootstrap's field-hint line. A link inside one is never
     prose -- it is the "Browse media library ->" style escape hatch, alone at
     the end of the hint, measured 125x16 on the CMS editors. */
  .form-text a {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
  }

  /* -- Icon-only buttons that escaped the .btn-sm floor -------------------
     `.pin-toggle-btn` (14x19, the kiosk PIN reveal -- same control as
     `.pw-toggle-btn`, built separately), the ID-card preview's zoom pair
     (26x21), and `.btn-link` used as a disclosure ("SEO & Social", 104x22).
     None carries `.btn-sm`, so portal-compact's floor never saw them. */
  .pin-toggle-btn,
  .btn.btn-link,
  button[id^="bZoom"] {
    min-width: 2.75rem;
    min-height: 2.75rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* -- CKEditor's own toolbar --------------------------------------------
     17x30. Third-party markup, but it ships inside our editor and a phone
     user has to hit it, so "not our HTML" is not a reason to leave it. The
     vendor exposes no size variable; the button is padded rather than resized
     so the icon glyph inside keeps its own metrics. */
  .ck.ck-toolbar .ck-button {
    /* `flex: 0 0 auto` is the load-bearing part. The first version set only
       min-width/min-height; the height took (30 -> 36px) and the width did
       not, because the toolbar is a flex row and a shrinkable item ignores
       `min-width` when the row is over-full. */
    flex: 0 0 auto;
    min-width: 2.25rem;
    min-height: 2.25rem;
    justify-content: center;
  }
  .ck.ck-toolbar > .ck-toolbar__items { gap: .25rem; }

  /* -- Django's built-in admin -------------------------------------------
     Not our templates either, and reachable in production by staff. One
     blanket row height on its list/action links is enough to clear the floor
     without restyling a surface we do not own. */
  #container a, #container button {
    /* `min-height` alone does nothing on an inline box -- the same trap this
       file warns about two rules up, walked into anyway. `inline-flex` is what
       makes the height real. */
    display: inline-flex;
    align-items: center;
    min-height: 1.5rem;
  }
  #container input[type="checkbox"] { min-width: 1.5rem; min-height: 1.5rem; }
  #container .theme-toggle { min-width: 1.5rem; min-height: 1.5rem; }

  /* -- Spacing between adjacent chips ------------------------------------
     SC 2.5.8 counts the GAP toward effective target size, which is why
     portal-compact.css already spaces `.btn-sm` pairs. These four chip strips
     were built with their own containers and measured 5-6px apart -- correctly
     sized individually, close enough together that a thumb cannot reliably
     pick one. Raised at the CONTAINER's `gap`, so nothing about the chips
     changes and the strip simply breathes.

     `a.nav-link` pairs at gap=0 are deliberately NOT here: adjacent tabs
     sharing an edge is what a tab strip is, and prising them apart would
     break the control to satisfy a number. */
  .profile-section-link,
  .msgc-folder-link,
  .ac-stat-pill,
  .ts-chip,
  .msgc-compose-btn { margin: .15rem; }

  /* The same argument for row-action toolbars, which measured 3-5px apart.
     portal-compact.css already spaces `.btn-sm + .btn-sm`; these families were
     built with their own classes and never inherited it, and one of the pairs
     is Edit next to Delete on /portal/org-units/. `+` so only genuinely
     adjacent siblings pay, and `.btn-group` stays exempt because buttons butted
     together is what a segmented control IS. */
  .btn + .btn,
  .cms-btn + .cms-btn,
  .dept-action-btn + .dept-action-btn,
  .cms-mode-btn + .cms-mode-btn,
  .btn + .cms-btn,
  .cms-btn + .btn { margin-left: .375rem; }
  .btn-group .btn + .btn,
  .btn-group-sm > .btn + .btn,
  .btn-group-vertical .btn + .btn { margin-left: 0; }

  /* -- Column sort controls in table headers ------------------------------
     20 findings, all the same shape: `<th><a>Reason</a></th>` at 63x17. The
     probe exempts an inline link inside `p`, `li` or `td` because that is a
     link in running text; a link in a `th` is not that -- it IS the column's
     sort control, it is the only one, and nothing else in the header cell
     competes for the tap. Scoped to `th` for exactly that reason. */
  th a {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
  }

  /* -- "<- Back" links above a form ---------------------------------------
     `<- Enrollments`, `<- Benefit plans`, `<- Buckets`, `Clear filters`,
     `Manage ads` -- 58-85px wide and 16-17px tall, each alone on its line as
     the only way back out of the page it heads. */
  a.small,
  a.text-muted.small,
  a.text-decoration-none.text-muted,
  a.text-decoration-none.text-dark {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
  }

  /* -- Colour swatches and their reset buttons ----------------------------
     The ID-card branding panel: seven `<input type="color">` at 22x22 (two
     pixels under the floor, which is the most annoying kind of near-miss) and
     five "Auto" reset buttons at 35x23. A colour input's box IS its swatch,
     so growing it is an improvement rather than a compromise -- the same
     argument portal-compact.css makes for checkboxes. */
  input[type="color"] {
    min-width: 2.75rem;
    min-height: 2rem;
  }
  button[id$="Reset"] {
    min-height: 2rem;
    padding-inline: .6rem;
  }

  /* -- Toggle switches ----------------------------------------------------
     Measured 25x14 on /portal/attendance/corrections/new/ and every other
     switch in the product: 14px of height is under half the 24px floor, and
     it is thin in exactly the axis a thumb is least precise in.

     `.form-check-input` is sized `1em` square by Bootstrap and `.form-switch
     .form-check-input` overrides only the WIDTH to 2em -- so a rule that sets
     height on `.form-check-input` alone is out-specified for switches and
     silently does nothing for them. portal-compact.css has carried exactly
     that rule since 2026-08-15 and switches stayed 14px. The selector has to
     name `.form-switch` to reach them.

     Height and width move together: a switch is a track, and a 24px-tall
     track on a 25px-wide one is a square with a circle in it, not a switch. */
  /* `[role="switch"]` as well as `.form-switch`: /kiosk/manage/new/ builds its
     three toggles as `<input class="form-check-input" role="switch">` inside a
     custom `.kf-status-toggle` wrapper, with no `.form-switch` class at all --
     so the selector below reached none of them and they measured 35x18. The
     ARIA role is the more reliable signal of what the control IS. */
  .form-switch .form-check-input,
  .form-check-input[role="switch"] {
    height: 1.5rem;
    width: 2.75rem;
    background-position: left center;
    /* margin-top is NOT set here any more. Bootstrap's default
       `.form-check-input { margin-top: .25em }` exists to centre a 1em control
       against a 1.5-line label; zeroing it while making the control TALLER
       than the line box top-aligns the switch instead, leaving it 3.5px below
       the label's centre -- ((24 - 17) / 2), measured on every switch in the
       product. Alignment is handled once, below, for both pointer types. */
  }
  .form-switch .form-check-input:checked,
  .form-check-input[role="switch"]:checked { background-position: right center; }

  /* -- Inline links that are really actions -------------------------------
     A link inside a sentence is exempt from the target-size rule and should
     stay exempt -- growing every one of them would wreck body copy. These are
     not that: a phone number, an email address and "Forgot password?" are
     each the primary action of the block they sit in, they sit alone on their
     line, and they measured 17px tall. `inline-block` plus vertical padding
     brings the row to 44px without moving anything around it. */
  a[href^="tel:"],
  a[href^="mailto:"],
  .auth-links a {
    display: inline-block;
    padding-block: .55rem;
    line-height: 1.35;
  }
}

/* -- The smallest type, on a phone -----------------------------------------
   Bootstrap sizes `.badge` at `.75em` -- RELATIVE to its parent, so a badge in
   a 15px block lands at 11.25px. Measured on /packages-and-subscriptions/,
   where the section kicker "WHAT THE MONTHLY SUBSCRIPTION COVERS" is 36
   all-caps characters with .04em of extra tracking at that size. SC 1.4.4 is
   about not having to pinch to read, and 11px all-caps is where people start.

   `max()` rather than a flat size: a badge sitting inside larger type should
   still scale up with it. This only stops the relative unit going BELOW 12px,
   and only on phone widths -- a dense desktop table full of status chips is
   read at half the distance and is left alone. */
@media (max-width: 575.98px) {
  .badge { font-size: max(.75em, .75rem); }

  /* -- The bottom of the type scale, on a phone ---------------------------
     Measured at 390px across 276 routes:

         --fs-4xs   .6rem   =  9.6px   .tag-chip, .std-badge
         --fs-3xs   .65rem  = 10.4px   micro labels
         <code>     .875em  = 10.5px   inherits, so it lands wherever it sits

     Those are defensible on a 27" monitor at 60cm and not on a phone at 30cm.
     The flagged strings are not one-word chips either -- "Assets & Inventory,
     Clinic Management,RFID,barcode,QR code" at 9.6px, a settings hint at
     9.6px, a payroll formula at 10.5px.

     All three collapse to the same 12px, and that is the point rather than a
     compromise: below 12px on a phone the steps stop being a hierarchy and
     become degrees of unreadable. --fs-xs is left alone deliberately -- it is
     one of the two ANCHORED values (see the scale's own comment) and is
     already 12px, so the bottom of the scale simply meets it.

     `:root`, NOT `body`. portal-compact.css already raises --fs-2xs to .75rem
     at <=480px, on `:root`. A `body` declaration here BEATS that (element
     selector over pseudo-class on the same element), so an earlier version of
     this block silently undercut it to 11.8px and put 178 `.fs-2xs` elements
     back under the floor -- measured, not theoretical: tinyText went 16 -> 219
     across the sweep. Matching that file's selector keeps the later, more
     specific file in charge, which is the whole point of loading it after. */
  :root {
    --fs-4xs: .75rem;
    --fs-3xs: .75rem;
    --fs-2xs: .75rem;
  }
  /* `code` sizes itself relative to its parent, so no token can floor it. */
  code, kbd, samp { font-size: max(.875em, .75rem); }
}

/* ── CMS-authored content: let an author's text colour survive .badge ──────
   Found 2026-08-15 by a contrast sweep of the live public pages.

   The category chips on the home page render as

       <span style="background-color:#fda4af;color:#04252b">
         <span class="badge …"><strong>CLINIC MANAGEMENT</strong></span>
       </span>

   The author did the right thing: a dark ink on a pale rose, about 9:1. What
   ships is WHITE on that rose at 1.89:1, because Bootstrap's `.badge` sets its
   own `color` and the inner element wins over the inherited one. The author
   cannot see this in the editor, cannot fix it from the editor, and every chip
   they create has the same defect — six of them measured between 1.44:1 and
   1.89:1.

   Scoped deliberately narrowly: only a badge INSIDE an element whose style
   attribute sets a colour, and only within CMS blocks. That is precisely the
   case where an author expressed an intent that Bootstrap is overriding.
   A blanket `.badge { color: inherit }` would change every status chip in the
   product, including the ones the design system already guarantees.

   This does not make CMS content theme-aware — an author-set colour is a fixed
   value and will not follow a dark/light switch. That is a content-model
   problem (the editor offers a free colour picker with no contrast check), and
   the fix for it is to constrain the palette, not to override authors here. */
.cms-block [style*="color"] .badge { color: inherit; }

/* ── {% form_field %} ─────────────────────────────────────────────────────
   Everything resolves through existing tokens, so this inherits all 23
   presets with no preset-specific rules -- the property that lets the other
   twelve components stay preset-agnostic. */
.hrms-field { margin-bottom: var(--space-4); }
.hrms-field--group { border: 0; padding: 0; margin-bottom: var(--space-4); }
.hrms-field-label { font-weight: 600; margin-bottom: var(--space-1); }
/* The required marker is --danger as a FILL-weight glyph, not as body text,
   so it takes the text-safe variant. Paired with a visually-hidden
   "(required)" in the template, because a bare asterisk is a convention a
   screen reader cannot explain. */
.hrms-field-req { color: var(--danger-text); font-weight: 700; }
.hrms-field-error {
  display: flex; align-items: center; gap: var(--space-1);
  margin: var(--space-1) 0 0;
  font-size: var(--fs-xs);
  color: var(--danger-text);
}
/* A red edge is the visual half of the same signal aria-invalid carries. */
.hrms-field--invalid .form-control,
.hrms-field--invalid .form-select,
.hrms-field--invalid textarea { border-color: var(--danger-text); }

/* ── .table-responsive must not inflate the PAGE's scrollable area ────────
   /iot/observability/ scrolled sideways 529px at 320px wide while containing
   nothing out there — measured: the viewport screenshot changes when you
   scroll right, and ZERO text elements start beyond the right edge. The user
   can drag the whole page into half a screen of blank space.

   It is not a stray wide element. Isolated empirically by hiding candidates
   one at a time: no single element stopped it, hiding every .table-responsive
   stopped it completely, and the page has four wide tables all correctly
   wrapped. Chromium folds a descendant scroll container's scrollable overflow
   into its ancestors' scrollWidth, so four 670–890px tables inside 286px
   wrappers make the ROOT scrollable even though each is clipped and scrolls
   properly on its own.

   `contain: paint` scopes that overflow to the wrapper, which is the cause
   rather than the symptom. Measured on the same page: 529 -> 0.

   It adds NO new clipping. `.table-responsive` already sets
   `overflow-x: auto`, and CSS computes the other axis to `auto` too when one
   axis is not `visible` — so this element has always clipped its descendants
   in both directions, which is exactly why a dropdown inside a responsive
   table is already cut off in Bootstrap. Verified against the alternatives:
   `overflow-x: clip` on the wrapper does nothing (529 -> 529) and would break
   the horizontal scrolling this element exists for; `overflow-x: clip` on the
   page container also works but masks any FUTURE real overflow, which is the
   opposite of what this audit is for. */
.table-responsive { contain: paint; }

/* ── CMS-authored page bodies must not be able to break the layout ────────
   Everything inside `.cms-body` is HTML a person typed into the page editor.
   The editor cannot make them wrap a table in `.table-responsive`, and a
   sweep of all 89 public pages found the two ways that goes wrong at 320px:
   `/cookie-policy/` pastes a bare <table> (326px in a 320px viewport) and
   `/packages-and-subscriptions/` uses a `.badge` as a banner (306px of
   nowrap text). Both made the WHOLE page scroll sideways.

   Rewriting their content would fix today's copy and nothing else -- the next
   edit reintroduces it. Containing authored content at the boundary is what
   every prose stylesheet does for the same reason, and it costs the author
   nothing: the table still scrolls, it just scrolls inside its own box.

   Deliberately NOT applied to the whole site: a table outside the CMS is
   written by us and belongs in `.table-responsive`, where the wrapper is the
   documented pattern and `display: block` on the table itself would fight it.
   Verified against production: 18px and 34px of sideways scroll, both to
   zero, nothing else on either page changed. */
.cms-body table {
  display: block;
  max-width: 100%;
  overflow-x: auto;
}

/* ── ...but a table that ALREADY has a scroll wrapper must stay a table ────
   `display: block` above makes the <table> a block box, so its rows are laid
   out in an ANONYMOUS table inside it — and an anonymous table is shrink-to-
   fit. The block fills its parent; the rows do not. Wherever the table sits in
   a frame, that gap is visible as dead space inside the frame, with the header
   tint and every row rule stopping short of the border.

   Measured on /packages-and-subscriptions/ at 1440px:

       .pricing-table-wrap right edge   1356px
       tier-table rows end at            931px   ->  425px of empty frame
       add-ons-table rows end at        1127px   ->  229px of empty frame

   The 320px protection that `display: block` exists for is provided here by
   the wrapper's own `overflow-x: auto`, so restoring `display: table` inside a
   wrapper costs nothing and is verified below: page-level sideways scroll on
   this page stays 0 at every breakpoint.

   Deliberately scoped to the two wrappers rather than to `.cms-body table`
   generally — a BARE table pasted into the editor (/cookie-policy/) has no
   wrapper and still needs the block treatment, which is the case the rule
   above was written for. */
.cms-body :is(.table-responsive, .pricing-table-wrap) table {
  display: table;
  width: 100%;
}

/* ── One table treatment for the whole public site ────────────────────────
   Four distinct table shapes ship across the 97 public pages and each looked
   like a different product:

     6x  .table.table-hover   in a wrapper    8px cells
     1x  the tier table       in a wrapper    8px cells
     1x  #addons-table        in a wrapper    8px cells
     2x  a bare <table>       NO wrapper      1px cells, header fill identical
                                              to the body fill

   The last one is the one worth stating plainly: on /cookie-policy/ an author
   pasted a plain table, so it inherited nothing at all — one-pixel cells and a
   header row indistinguishable from the data under it. That is not an
   authoring mistake to correct page by page, it is the default a CMS has to
   supply, or the next pasted table looks the same again.

   So the treatment is defined once here for anything inside `.cms-body`, and
   the pricing block further down adds only what is genuinely specific to it.
   8px -> .8rem cells, a header band mixed from the preset's own ink, and a 2px
   rule under the head so it reads as a head rather than as the first row. */
.cms-body table {
  --bs-table-bg: transparent;
  --bs-table-border-color: var(--border);
  /* Bootstrap resolves --bs-table-color from --bs-emphasis-color, which is
     BLACK unless the document carries data-bs-theme="dark". Only the preset
     literally named `dark` sets that attribute, so on `glassmorphism` -- a
     dark canvas by every other measure -- every table cell rendered pure black
     on a near-black surface: 36 nodes at 1.18:1. Taking the colour from the
     token instead makes it follow the preset rather than the attribute. */
  --bs-table-color: var(--text);
  color: var(--text);
  border-color: var(--border);
  font-size: .925rem;
  /* NB: no `overflow` here, and none in the frame rule below. The block at the
     top of this section sets `overflow-x: auto` on exactly this selector, and
     any later `overflow: hidden` would silently reset that axis to hidden —
     clipping a wide authored table instead of letting it scroll, which is the
     defect that whole block exists to prevent. `overflow-x: auto` already
     establishes a clipping box, so the radius below is honoured without it. */
}
.cms-body table :is(th, td) {
  padding: .8rem 1rem;
  border-color: var(--border);
}
/* `.table-light` is Bootstrap's own #f8f9fa literal, applied to <thead> by six
   of the seeded tables. No preset can reach it, so on a dark-canvas preset it
   painted a near-white band and the themed header ink landed light-on-light:
   24 nodes at 1.04:1. Neutralising the fill lets the tokenised band below
   apply; the class is left on the markup, since it is authored content. */
.cms-body table thead.table-light,
.cms-body table thead.table-light > tr > th { --bs-table-bg: transparent; }

.cms-body table thead th {
  background: color-mix(in srgb, var(--text) 7%, var(--surface));
  /* --text, NOT --muted. A header set in --muted measured under AA on the
     header band for the `dark` and `glassmorphism` presets (24 nodes): --muted
     is solved against --surface, and the band is a mix that moves away from it.
     The uppercase/bold/small treatment is already doing the de-emphasis, so the
     colour does not have to -- and --text on a 93%-surface band is legible by
     construction on all twenty presets. */
  color: var(--text);
  font-size: .74rem;
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  text-align: left;
  vertical-align: bottom;
  border-bottom: 2px solid var(--border);
}
/* A table with no wrapper carries its own frame; one inside a wrapper must
   not, or the two edges sit a pixel apart and read as a double border. */
.cms-body table {
  background: var(--surface);
  border: var(--border-width, 1px) solid var(--border);
  border-radius: var(--radius);
  border-collapse: collapse;
}
/* Descendant, NOT child. Six of the ten public tables sit as
   `.table-responsive > div.table > table` -- the CMS wraps them in a div that
   carries the `table` CLASS -- so a `>` combinator matched four of them and
   silently left the other six laid out as blocks. Any table inside a scroll
   wrapper wants this treatment regardless of how deeply it is nested. */
.cms-body :is(.table-responsive, .pricing-table-wrap) table {
  background: transparent;
  border: 0;
  border-radius: 0;
}
.cms-body .badge {
  max-width: 100%;
  white-space: normal;
}
.cms-body img,
.cms-body video,
.cms-body iframe { max-width: 100%; }

/* A module sub-page's own per-step "how to use it" screenshots -- seeded by
   seed_demo_pages._body() as a plain <img> inside each <li>, sanitizer-safe
   per apps.public.html_sanitize. Block-level with its own top margin so it
   reads as "the picture for this step" rather than running into the list
   text above it. */
.cms-body .demo-step-image {
  display: block;
  margin-top: .5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* ── CMS video block ──────────────────────────────────────────────────────
   The home page's "Watch a two-minute walkthrough" shipped as a bare <video>
   with an inline background, pointed at /media/cms_media/videos/…. Two
   separate faults, and the visible result of both was the same empty
   rectangle with a play glyph in it:

     * the file lived only in the local Docker media volume, so production
       returned 404 for it (verified against hr-oms.com);
     * the element reserved no space and had no poster or `preload`, so even
       with a working source the block was blank until the user clicked it,
       and the page reflowed when the metadata finally arrived.

   The frame fixes the second: a 16:9 box that holds its size from first paint,
   so there is no layout shift, and a real surface behind the player instead of
   a transparent gap. `preload="metadata"` on the element (now allowlisted by
   the sanitizer, which had been stripping it) is what paints the first frame.

   The dark fill is deliberate and not a preset violation: it is the letterbox
   behind a video, which is black-ish in every player ever made, and it is
   mixed from --text rather than hardcoded so a light-on-dark preset still
   lands somewhere sensible. */
.cms-video-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  border: var(--border-width, 1px) solid var(--border);
  background: color-mix(in srgb, var(--text) 92%, black);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.cms-video-frame > .cms-video,
.cms-video-frame > video {
  display: block;
  width: 100%;
  height: 100%;
  /* `contain`, not `cover`: a walkthrough recording is information, and
     cropping the edges off a screen recording removes the part being
     explained. Letterboxing against the fill above is the correct trade. */
  object-fit: contain;
  background: transparent;
}
/* No aspect-ratio support (or a stubbornly tall source): never let the frame
   grow past the fold. */
@supports not (aspect-ratio: 16 / 9) {
  .cms-video-frame { min-height: 15rem; }
}
.cms-body pre { max-width: 100%; overflow-x: auto; }

/* ── Bootstrap's semantic FILL utilities, bridged to the token layer ──────
   Measured 2026-08-15. Three separate failures, one cause: `.text-bg-*` and
   `.bg-*` paint Bootstrap's own compiled literals, which no preset has ever
   touched, and then the text on them is decided independently.

     .text-bg-primary          4.19:1  our --on-primary on Bootstrap's #0d6efd
     .text-bg-info .text-dark  1.52:1  themed ink on Bootstrap's #0dcaf0
     .bg-warning               1.26:1  themed ink on Bootstrap's #ffc107

   The first is self-inflicted: --on-primary is solved against OUR --primary,
   so applying it to a fill that is still Bootstrap blue pairs two colours
   that were never solved together. The other two are the reverse -- a light
   fill that genuinely needs dark text, which `.text-dark { color: var(--text) }`
   above turns light on a dark theme.

   Resolved by making each fill and its text one decision again:

   * primary/success/danger get OUR token as the fill, so the text colour that
     was solved against it is the right one. success/danger use the same
     70%-toward-black mix as .btn-success/.btn-danger, for the same reason
     recorded there: white text needs the fill dark enough to clear AA.
   * warning/info stay LIGHT fills with dark text, which is what they are for.
     The dark value is a literal on purpose: these two fills are light in
     every theme, so their text must be dark in every theme, and a token
     would reintroduce exactly the flip being fixed. */
.text-bg-primary {
  background-color: var(--primary) !important;
  color: var(--on-primary) !important;
}
.text-bg-success {
  background-color: color-mix(in srgb, var(--success) 70%, black) !important;
  color: #fff !important;
}
.text-bg-danger {
  background-color: color-mix(in srgb, var(--danger) 70%, black) !important;
  color: #fff !important;
}
/* Light fills. The text stays dark regardless of theme -- see above. */
.text-bg-warning, .text-bg-info,
.text-bg-warning .text-dark, .text-bg-info .text-dark,
.bg-warning, .bg-info,
.bg-warning .text-dark, .bg-info .text-dark {
  color: #212529 !important;
}

/* An opacity utility on already-muted text pushes it under AA: --muted is
   solved to 4.5:1, and half of that is 4.25:1. Measured on 20 nodes. The
   utility means "de-emphasise", which is what --muted already does, so the
   fix is to stop stacking the two rather than to invent a third grey. */
.opacity-50.fs-xs, .opacity-50.fs-2xs, .opacity-50.fs-3xs { opacity: 1 !important; color: var(--muted); }

/* ── Glass chips authored for a dark band, rendered on a light card ───────
   A BARE `.badge` -- one with no `.text-bg-*` / `.bg-*` -- takes Bootstrap's
   `--bs-badge-color: #fff`. The seeded marketing content pairs that with an
   inline `background:rgba(255,255,255,.18)`, which is a glass chip: correct on
   the dark hero band it was authored against, and white-on-white anywhere
   else. Measured on /news/, where the category chips sit on light cards:

       winxp          1.00:1     retro        1.03:1
       solarized      1.06:1     skeuomorph   1.12:1

   1.00:1 is not "low contrast", it is invisible text, and it is live on the
   production preset too. The same defect the `ground_inverted_sections` /
   `repair_inverted_text` commands exist for, in the one form they cannot
   reach: the colour is on the CHIP, not on the block around it.

   There is deliberately NO CSS rule here, and that is the finding worth
   recording. The obvious one -- restyle every bare `.badge` on the public site
   -- was written, measured, and thrown away: the seeded content carries 150
   bare badges and only 18 use the white-glass pattern. The other 132 have
   deliberate inline tints (41 cyan, 10 green, 8 hero-vivid, plus amber and
   violet), every one of which an `!important` fill would have flattened to the
   same grey. That trades 4 contrast failures for ~66 destroyed design
   decisions, which is a worse page rather than a better one.

   CSS cannot tell the two apart: the discriminator is the inline fill VALUE,
   which no selector can read. The defect is a property of the content -- a
   light-on-dark chip placed on a light ground -- so it is repaired in the
   content, per instance, where the ground is known. See
   `apps/public/management/commands/repair_glass_badges.py`. */

/* ── A card with no border and no shadow is not a card ────────────────────
   `.card` draws its edge with `border: … var(--border)` and its lift with
   `--shadow-sm`. `.border-0` removes the first (as `!important`, so nothing
   short of the same can restore it) and the Flat and Nord presets set
   `--shadow-sm: none`, which removes the second. On a page where the card's
   own `--surface` equals the fill behind it, that leaves a container with
   nothing whatsoever to mark where it starts: measured across 12 pages on
   both presets at a colour difference of exactly 0.

   Drawn with an INSET box-shadow rather than by fighting `.border-0`: the
   utility says "no border" and it is not this rule's place to contradict it,
   whereas a shadow is the property the card already uses for its own edge
   treatment and `.border-0` does not touch it. Inset so it lands on the card's
   own edge instead of adding an outer ring that would read as elevation.

   `outline` was the other candidate and is deliberately not used: the pricing
   cards reserve outline for focus and box-shadow for selection, and borrowing
   the focus property for decoration is what that split exists to prevent. */
body.public-cms-page .card.border-0 {
  box-shadow: inset 0 0 0 var(--border-width, 1px) var(--border);
}

/* ── Micro-interactions on the public site ────────────────────────────────
   The scroll-reveal system above handles a block ARRIVING. Nothing handled a
   block RESPONDING: cards, buttons and nav links had no pointer or press
   feedback beyond `.card:hover`'s shadow swap, which is invisible on the two
   presets whose shadow scale is `none`.

   Deliberately small. An enterprise marketing page is read, not played with,
   and the failure mode of "more animation" is a page that feels unstable while
   you are trying to read a price off it. Every value here is a 1-2px move or a
   150ms fade.

   Compositor-only (`transform` / `opacity` / `box-shadow`) for the same reason
   the reveal system is: nothing here can trigger layout, so nothing here can
   jank a scroll.

   All of it is inside the motion opt-outs at the end of this block. Those are
   the same three switches scroll-reveal honours -- the OS preference, the
   administrator's 3-level setting, and reduced-motion -- restated rather than
   inherited because these rules are not `[data-anim]` and the existing resets
   would not have caught them. */
@media (prefers-reduced-motion: no-preference) {
  body.public-cms-page .card {
    transition: transform .18s var(--motion-easing, ease),
                box-shadow .18s var(--motion-easing, ease);
  }
  /* Not applied to the pricing cards: those own their box-shadow outright to
     draw a selection ring, and a hover lift competing with "this is the tier
     you picked" muddies the one signal that page needs to be unambiguous. */
  body.public-cms-page .card:not(.hrms-selectable-card):hover {
    transform: translateY(-2px);
  }

  /* Press feedback. `:active` only -- a button that moves on hover reads as
     unstable, one that moves on press reads as a button. */
  body.public-cms-page .btn { transition: transform .12s ease, filter .12s ease; }
  body.public-cms-page .btn:active { transform: translateY(1px); }

  /* Nav: the underline grows from the link's own centre rather than fading in,
     so the eye is led along the word it belongs to. Drawn on a pseudo-element
     so it cannot shift the text baseline the way a real border would. */
  .hrms-nav .nav-link { position: relative; }
  .hrms-nav .nav-link::after {
    content: "";
    position: absolute;
    left: 50%;
    right: 50%;
    bottom: .28rem;
    height: 2px;
    border-radius: 2px;
    background: var(--primary);
    transition: left .18s var(--motion-easing, ease),
                right .18s var(--motion-easing, ease);
  }
  .hrms-nav .nav-link:hover::after,
  .hrms-nav .nav-link:focus-visible::after { left: .75rem; right: .75rem; }
  /* A dropdown parent keeps its underline while the menu is open, or the
     indicator contradicts the panel hanging off it. Descendant rather than
     child: the <a> is wrapped in a plain `.d-flex` div (it shares the row with
     the mobile caret button), so `>` would match nothing. */
  .nav-hover-dd:hover .nav-link::after { left: .75rem; right: .75rem; }
}

/* The three opt-outs, matching the scroll-reveal block above. */
@media (prefers-reduced-motion: reduce) {
  body.public-cms-page .card,
  body.public-cms-page .btn,
  .hrms-nav .nav-link::after { transition: none !important; }
  body.public-cms-page .card:hover,
  body.public-cms-page .btn:active { transform: none !important; }
}
[data-portal-motion="disabled"] body.public-cms-page .card,
[data-portal-motion="disabled"] body.public-cms-page .btn {
  transition: none !important;
}
[data-portal-motion="disabled"] body.public-cms-page .card:hover,
[data-portal-motion="disabled"] body.public-cms-page .btn:active {
  transform: none !important;
}
[data-portal-motion="disabled"] .hrms-nav .nav-link::after { display: none; }

/* ── Selectable pricing cards ─────────────────────────────────────────────
   Driven by static/js/pricing-card-select.js on /packages-and-subscriptions/:
   the "how to buy" band and the tier grid. The indigo ring means SELECTED and
   nothing else, so it is defined only here, on the [aria-checked] state.

   `box-shadow: none !important` is doing real work, not tidying. The featured
   tier card carries an always-on ring as an INLINE `box-shadow:0 0 0 2px
   <accent>` written into stored CMS HTML by a seeder. Inline styles beat
   stylesheet rules, so nothing short of !important can retire it, and leaving
   it would mean two cards wearing "selected" before the reader has picked
   anything. The badge still marks which tier is recommended.

   One indigo for every card, deliberately, rather than each card's own accent:
   "selected" has to read as one consistent signal across both groups, and an
   amber ring on the Custom tier would not. */
.hrms-selectable-card {
  cursor: pointer;
  box-shadow: none !important;
  transition: outline-color .18s var(--motion-easing, ease),
              box-shadow .18s var(--motion-easing, ease);
}

/* Selection is drawn with box-shadow, focus with outline, and that split is
   deliberate. Both started out as `outline`, which meant a keyboard user
   arrowing through the group saw the FOCUS colour on the card they had just
   selected -- one property, two meanings, last-one-wins. Different properties
   let a focused selected card show both rings at once, which is what it is. */
.hrms-selectable-card:focus-visible {
  outline: 2px solid var(--primary, #6366f1);
  outline-offset: 4px;
}

.hrms-selectable-card[aria-checked="true"],
.hrms-selectable-card[aria-checked="true"]:hover {
  box-shadow: 0 0 0 2px #4f46e5, 0 0 0 6px rgba(79, 70, 229, .18) !important;
}

@media (prefers-reduced-motion: reduce) {
  .hrms-selectable-card { transition: none; }
}


