/* ============================================================================
   BRAINWEBER THEME — CORE. Two complete design generations, one switch.

   ⚠ THIS FILE IS THE PRODUCT. It styles only markup that is NOT owned by the host
   application: Serenity's own widgets (grids, dialogs, forms, select2, flatpickr,
   pager, toolbar, toasts), the shell and auth views the theme package ships, and
   the UX modules it ships (list-page chrome, filter pills, validation banner,
   density switch, grid states, stacked dialogs, export/print sheet). It carries no
   knowledge of any particular application's screens, so it drops into any Serene
   app unmodified.

   Styles for markup the HOST owns live in theme-app.css, which loads after this.
   ⚠ Keep that order. This file must also load AFTER Serenity.Extensions'
   common-theme.css and Bootstrap — it wins by source order, never by !important.

   ⚠ Token prefix is --bw-*. It was --m-* until 2026-08-19; renamed because a theme
   shipped to another company must not squat on a two-letter namespace in their app.

   HOW THIS FILE IS ORGANISED
     1. Font
     2. Token block for v1   (:root[data-theme-version="v1"])
     3. Token block for v2   (:root[data-theme-version="v2"])
    3b. Dark overrides       ([data-theme-version="vN"][data-theme="dark"])
     4. Framework bridge     (--bs-* and --s-* remapped onto --bw-*)
     5. Component layer      (reads --bw-* only, never a literal colour)
     6. Structural shell     (rail vs sidebar, dialog vs drawer)
     7. Auth screens
     8. Dashboard
     9. Print

   The active version is stamped on <html> by the layouts from
   Common/Theming/ThemeVersion.cs, driven by AppSettings:ThemeVersion.

   TWO INDEPENDENT AXES
   data-theme-version picks the design generation (v1 or v2); data-theme picks
   light or dark within it, from Common/Theming/ThemeMode.cs. All four
   combinations are real, so section 3b overrides only the COLOUR tokens and never
   geometry — a dark v1 is still 32px rows and 4px corners.

   WHY THIS FILE IS LAST IN THE "Base" CSS BUNDLE
   theme.css restyles components that common-theme.css and site.css have already
   styled, so it is listed last in appsettings.bundles.json and wins on source
   order rather than by escalating to !important. Keep it there. (That note lives
   here and not in the bundles file because @serenity-is/tsbuild JSON.parses
   appsettings.bundles.json strictly — a // comment in it fails the whole TS build.)

   WHY BOTH VERSIONS LIVE IN ONE FILE, ALWAYS LOADED
   The obvious alternative — emit a <link> to tokens-v1.css or tokens-v2.css
   depending on the setting — means the browser cache, the asset-version hash and
   the bundler all have to agree about which file is current, and a stale hash
   silently serves the wrong theme. Attribute-scoped blocks in one file cannot
   desync: the served CSS is identical for both versions and only the attribute
   differs. The cost is a few KB of unused custom properties, which compress to
   almost nothing. It also means a future per-user or preview switch is a
   one-attribute DOM change with no network round trip.

   THE ONE RULE FOR THE COMPONENT LAYER
   No literal colours, radii or fonts below section 4 — every value comes from a
   --bw-* token. If a component needs something the tokens do not express, add the
   token to BOTH blocks rather than hardcoding it in the component. That is what
   keeps the two versions genuinely switchable instead of v1-with-patches.

   WHY SECTION 4 EXISTS
   Serenity and Bootstrap both style hundreds of elements this file never names,
   through their own custom properties (--bs-primary, --s-card-bg, --s-tabs-text
   and so on). Restyling those elements one selector at a time would be endless
   and would still leave stock colours showing in components nobody thought to
   list. Re-pointing the framework variables at the BWDesignSystem tokens fixes the
   whole surface at once, and the component layer below then only has to correct
   SHAPE and DENSITY — the things a variable cannot express.
   ============================================================================ */

/* ---- 1. FONT ----
   Both designs specify IBM Plex Sans (with Plex Mono for figures and counts).
   The faces are SELF-HOSTED and no longer @imported from fonts.googleapis.com:
   see ~/Content/fonts/ibm-plex/ibm-plex.css, which is listed immediately before
   this file in the Base bundle. That file's header explains the subsetting and
   how to regenerate it.

   Nothing is declared here on purpose. An @import is only valid at the TOP of a
   stylesheet, so a font pulled in from this file would have to sit above the
   token blocks and would block their parse on a network round trip; a separate
   bundle entry loads in parallel instead. The stacks in the token blocks below
   still end in system-ui, so the app stays legible if the woff2 files are
   missing or blocked. */

/* ============================================================================
   2. VERSION 1 — navy chrome, orange accent, dense
   Accent discipline: #F7941D is an ACCENT ONLY. It marks state and focus; it is
   never a surface and never a primary button. Primary actions are navy. Using the
   orange as a fill is the single fastest way to make this design look wrong.
   ============================================================================ */
:root[data-theme-version="v1"] {
  --bw-primary:            #0F2A5F;
  --bw-primary-rgb:        15, 42, 95;
  --bw-primary-hover:      #16386F;
  --bw-primary-active:     #0A1F49;
  --bw-primary-soft:       #EEF2F8;
  --bw-primary-softer:     #F3F7FE;
  /* The rule that pairs with --bw-primary-soft. The soft fill had no matching line, and
     a neutral --bw-border beside a tinted fill reads as a mistake. */
  --bw-primary-soft-line:  #D5DCE7;
  --bw-on-primary:         #FFFFFF;
  /* ⚠ THE PRIMARY COLOUR WHEN IT IS TEXT OR AN ICON, not a fill. In light mode it is the
     primary itself, so this token changes nothing here — it exists for dark, where the
     same navy that works as a button fill is unreadable AS TEXT on a dark surface (2.0:1
     for the grid's own edit links, measured). Every foreground use of the primary in the
     component layer reads this instead. Fills keep --bw-primary. */
  --bw-link:               #0F2A5F;
  /* A chart bar is the primary used as a MARK: nothing sits on top of it, so it has to
     stand off the surface the way ink does rather than the way a button fill does.
     ⚠ In light mode that is the primary itself; in dark it is NOT — v1 dark
     --bw-primary #2E4C93 measures 2.08:1 against the dark surface, below the 3:1 a data
     mark needs, so the dark blocks take the --bw-link value instead. Measured, not
     guessed: 13.85 / 6.23 / 8.36 / 9.25:1 across v1-light, v2-light, v1-dark, v2-dark. */
  --bw-chart-mark:         #0F2A5F;
  --bw-chart-track:        #EEF1F6;
  /* Categorical slots — segment identity for the donut and stacked-bar forms ONLY. Bar and
     Column stay one hue (--bw-chart-mark): bar length already carries magnitude, and colouring
     nominal bars by value would re-encode it and spend the identity channel for nothing.
     ⚠ These are the dataviz reference palette's validated slots, adopted wholesale because
     BWDesignSystem's own tokens have no categorical ramp — five hues plus a green, in a FIXED order
     that is itself the colourblind-safety mechanism. Do not reorder them and do not add a
     seventh: a generated hue is indistinguishable from an existing one under CVD, so a longer
     tail folds into "Other" instead (DashboardChartResolver.CategoricalSlotCount).
     Validated with the skill's own script — light on #FFFFFF: worst adjacent CVD ΔE 9.1,
     normal-vision ΔE 19.6, all six inside the lightness band and over the chroma floor.
     ⚠ Three of them (aqua 2.82, yellow 2.17, magenta 2.69) sit BELOW 3:1 on white. That is a
     WARN, not a pass, and it is discharged by the always-visible labelled legend on those two
     forms — never by dismissing it. If the legend ever goes, these have to be re-stepped. */
  --bw-cat-1:              #2a78d6;
  --bw-cat-2:              #eb6834;
  --bw-cat-3:              #1baf7a;
  --bw-cat-4:              #eda100;
  --bw-cat-5:              #e87ba4;
  --bw-cat-6:              #008300;
  --bw-link-rgb:           15, 42, 95;
  --bw-link-hover:         #16386F;

  --bw-chrome:             #0A1B3D;   /* sidebar / rail */
  --bw-chrome-text:        #FFFFFF;
  --bw-chrome-muted:       #8494B4;
  --bw-chrome-hover:       #16386F;
  --bw-chrome-rule:        rgba(255, 255, 255, .08);
  --bw-nav-active-bg:      #16386F;
  --bw-nav-active-fg:      #FFFFFF;
  /* Lifted from the handoff's #63769B, which is 3.73:1 on the navy rail — it carries the
     brand tagline and the nav group captions, both small text. #7C8FB5 is 5.22:1. */
  --bw-nav-group:          #7C8FB5;
  --bw-nav-count:          #8494B4;

  --bw-accent:             #F7941D;
  --bw-accent-soft:        #FFF1DC;
  --bw-accent-ink:         #8A4A00;
  --bw-accent-line:        #F7CE9B;

  --bw-canvas:             #F4F6F9;   /* page background */
  --bw-surface:            #FFFFFF;   /* cards, grids, dialogs */
  --bw-field:              #F7F9FC;   /* toolbars, table heads, footers */
  --bw-input-bg:           #FFFFFF;   /* v1 inputs are white, not tinted */
  --bw-readonly-bg:        #F0F3F8;

  --bw-text:               #2B3A52;
  --bw-text-strong:        #0F1B2D;
  --bw-muted:              #5B6880;
  /* ⚠ Darkened from the handoff's #7A8699, which measures 3.69:1 on white — under the
     4.5:1 WCAG AA needs for text below 18.66px. This token carries every KPI note, module
     subtitle, queue meta line and hint chip in the app, so it was the single largest
     legibility problem in the theme. #6B7789 keeps the same hue and reads as the same
     grey, at 4.54:1. Deliberate deviation from the doc; revert both --bw-faint values
     together if exact fidelity is ever needed over legibility. */
  --bw-faint:              #6B7789;
  /* ⚠ Was #8A94A6 (3.06:1). Placeholder text is still text, and on a select2 the
     placeholder IS the control's only visible content — unreadable placeholder means the
     user cannot tell what the control is for. Clearing 4.5:1 on white takes roughly this
     value, so placeholder lands on the same grey as --bw-faint; there is no lighter shade
     that passes. */
  --bw-placeholder:        #6B7789;
  --bw-disabled:           #A3ADBD;

  --bw-border:             #DDE3EC;
  --bw-border-strong:      #C6CFDD;
  --bw-border-field:       #C6CFDD;
  --bw-rule:               #EEF1F6;   /* hairlines inside a surface */
  --bw-row-line:           #F1F4F9;

  --bw-success:            #0B6B45;
  --bw-success-bg:         #E7F3EC;
  --bw-success-line:       #C7E2D2;
  --bw-warning:            #8A4A00;
  --bw-warning-bg:         #FFF1DC;
  --bw-warning-line:       #F7CE9B;
  --bw-danger:             #C0392B;
  --bw-danger-ink:         #8E241A;
  --bw-danger-bg:          #FDECEA;
  --bw-danger-line:        #F2BDB6;
  /* Destructive text ON the dark chrome — v1's bulk bar is a navy strip, and --bw-danger
     at #C0392B is far too dark to read against it. */
  --bw-danger-on-chrome:   #F2BDB6;
  --bw-info:               #1B3A75;
  --bw-info-bg:            #EEF2F8;
  --bw-info-line:          #D5DEEF;

  --bw-hover-bg:           #F7F9FC;
  --bw-selected-bg:        #F3F7FE;
  --bw-alt-row:            #FCFDFE;
  --bw-focus-ring:         0 0 0 3px rgba(15, 42, 95, .12);
  --bw-focus-ring-error:   0 0 0 3px rgba(192, 57, 43, .10);
  --bw-overlay:            rgba(10, 27, 61, .42);
  /* The scrim a SECOND dialog level lays over the first. Lighter than --bw-overlay on
     purpose: the parent is already dimmed once, and dimming it again at full strength
     turns it into a dark slab instead of a dialog you can still read. */
  --bw-overlay-2:          rgba(10, 27, 61, .30);
  /* Loading-skeleton bars: -a is the resting bar, -b the sweep highlight. Both sit
     between the canvas and the border so the bars read as absent content, not as data. */
  --bw-skeleton-a:         #E4EAF3;
  --bw-skeleton-b:         #F2F5FA;

  --bw-radius:             4px;
  --bw-radius-sm:          3px;
  --bw-radius-lg:          6px;
  --bw-radius-xl:          6px;
  --bw-pill:               3px;       /* v1 status chips are square-ish, not pills */
  --bw-chip:               13px;      /* filter chips are the one rounded thing */

  --bw-font:               'IBM Plex Sans', system-ui, -apple-system, sans-serif;
  --bw-font-mono:          'IBM Plex Mono', ui-monospace, monospace;
  --bw-text-base:          13px;
  --bw-text-sm:            12px;
  --bw-text-xs:            11px;
  --bw-h1-size:            20px;
  --bw-h2-size:            15px;
  --bw-h3-size:            13px;

  /* Form labels are SENTENCE case in both designs — the uppercase belongs to
     column heads and section eyebrows, which have their own tokens below. */
  --bw-label-size:         12px;
  --bw-label-weight:       600;
  --bw-label-color:        #2B3A52;
  --bw-label-spacing:      .02em;

  --bw-eyebrow-size:       11px;
  --bw-eyebrow-spacing:    .09em;
  --bw-eyebrow-transform:  uppercase;
  --bw-eyebrow-color:      #54637C;
  --bw-eyebrow-weight:     600;

  --bw-colhead-size:       11px;
  --bw-colhead-spacing:    .06em;
  --bw-colhead-color:      #54637C;

  --bw-form-cols:          3;         /* create dialog is 3 fields across */
  --bw-control-h:          32px;      /* dense: 20-row grids are the point */
  --bw-control-h-sm:       26px;
  /* ⚠ The handoff draws a form control TALLER than a list control: every field and every
     footer button in section 04's create dialog is 34px, while the tools row, top-bar
     search and toolbar buttons in section 03 are 32px. One --bw-control-h for both left
     dialog fields 2px short of spec. Applied by a contextual override inside the dialog
     body (section 5, "dialog form density"), not by a second set of selectors. */
  --bw-form-h:             34px;
  /* v1 squares its avatar off like the rest of its chrome; v2 makes it a disc. */
  --bw-avatar-radius:      4px;
  --bw-checkbox:           14px;      /* the handoff's grid checkbox */
  /* Its own radius: --bw-radius-sm is tuned for controls and on a 15px box reads as a
     CIRCLE, which is a radio button, not a checkbox. */
  --bw-checkbox-radius:    3px;
  --bw-btn-h:              32px;
  --bw-row-h:              32px;
  --bw-head-h:             32px;
  --bw-gap:                12px;
  --bw-pad:                16px;
  --bw-pad-lg:             20px;

  --bw-nav-w:              56px;      /* icon rail, collapsed */
  --bw-nav-w-open:         236px;     /* rail expanded on hover */
  --bw-nav-item-h:         38px;
  --bw-topbar-h:           52px;
  --bw-topbar-bg:          #FFFFFF;
  --bw-topbar-border:      #DDE3EC;

  --bw-shadow:             0 1px 2px rgba(10, 27, 61, .06);
  --bw-shadow-lg:          0 8px 24px rgba(10, 27, 61, .12);
  --bw-shadow-pop:         0 24px 60px rgba(10, 27, 61, .35);

  --bw-toast-w:            380px;     /* the prototype's notification width */

  /* dialogs: v1 is a centred modal with navy chrome */
  --bw-dialog-w:           1000px;
  /* Section 06 draws a stacked pair, and the two levels are NOT the same sheet: the parent
     recedes to 960px at 74px from the top, and the child sits at 820px / 190px over a
     second scrim. Level 1 alone keeps --bw-dialog-w / 60px. */
  --bw-dialog-w-back:      960px;
  --bw-dialog-top-back:    74px;
  --bw-dialog-w-2:         820px;
  --bw-dialog-top-2:       190px;
  --bw-dialog-head-bg:     #0A1B3D;
  --bw-dialog-head-fg:     #FFFFFF;
  --bw-dialog-head-h:      52px;
  --bw-dialog-foot-bg:     #F7F9FC;

  /* auth split screen — the doc fixes the brand column at 588px of 1440 */
  --bw-auth-cols:          588fr 852fr;
  --bw-auth-brand-pad:     56px 52px;
  --bw-auth-brand-bg:      #0A1B3D;
  --bw-auth-brand-fg:      #FFFFFF;
  --bw-auth-brand-lede:    #8494B4;
  --bw-auth-brand-item:    #C7D3E6;
  --bw-auth-brand-foot:    #63769B;
  --bw-auth-brand-rule:    rgba(255, 255, 255, .12);
  --bw-auth-num:           #F7941D;   /* the accent's one job on this screen */
  --bw-auth-mark-bg:       #F7941D;
  --bw-auth-mark-fg:       #0A1B3D;
  --bw-auth-mark-size:     30px;
  --bw-auth-mark-radius:   5px;
  --bw-auth-title-size:    40px;
  --bw-auth-title-spacing: -.02em;
  --bw-auth-form-w:        396px;
  --bw-auth-form-gap:      26px;
  --bw-auth-control-h:     40px;
  --bw-auth-btn-h:         44px;
}

/* ============================================================================
   3. VERSION 2 — green, warm-grey canvas, softer and roomier
   Not a hue swap of v1: corners roughly double, controls and rows grow, chips
   become true pills, and the accent role disappears entirely because the green
   carries emphasis on its own.
   ============================================================================ */
:root[data-theme-version="v2"] {
  --bw-primary:            #0F6E52;
  --bw-primary-rgb:        15, 110, 82;
  --bw-primary-hover:      #0C5B44;
  --bw-primary-active:     #073E2F;
  --bw-primary-soft:       #F2F8F5;
  --bw-primary-softer:     #F7FBF9;
  /* see the v1 block; this is the handoff's own #CFE3DA against the #F2F8F5 fill */
  --bw-primary-soft-line:  #CFE3DA;
  --bw-on-primary:         #FFFFFF;
  --bw-link:               #0F6E52;   /* see the v1 block */
  --bw-chart-mark:         #0F6E52;
  --bw-chart-track:        #EDF2EF;
  /* Categorical slots — segment identity for the donut and stacked-bar forms ONLY. Bar and
     Column stay one hue (--bw-chart-mark): bar length already carries magnitude, and colouring
     nominal bars by value would re-encode it and spend the identity channel for nothing.
     ⚠ These are the dataviz reference palette's validated slots, adopted wholesale because
     BWDesignSystem's own tokens have no categorical ramp — five hues plus a green, in a FIXED order
     that is itself the colourblind-safety mechanism. Do not reorder them and do not add a
     seventh: a generated hue is indistinguishable from an existing one under CVD, so a longer
     tail folds into "Other" instead (DashboardChartResolver.CategoricalSlotCount).
     Validated with the skill's own script — light on #FFFFFF: worst adjacent CVD ΔE 9.1,
     normal-vision ΔE 19.6, all six inside the lightness band and over the chroma floor.
     ⚠ Three of them (aqua 2.82, yellow 2.17, magenta 2.69) sit BELOW 3:1 on white. That is a
     WARN, not a pass, and it is discharged by the always-visible labelled legend on those two
     forms — never by dismissing it. If the legend ever goes, these have to be re-stepped. */
  --bw-cat-1:              #2a78d6;
  --bw-cat-2:              #eb6834;
  --bw-cat-3:              #1baf7a;
  --bw-cat-4:              #eda100;
  --bw-cat-5:              #e87ba4;
  --bw-cat-6:              #008300;
  --bw-link-rgb:           15, 110, 82;
  --bw-link-hover:         #0B5741;

  --bw-chrome:             #0C2A22;
  --bw-chrome-text:        #EAF2EE;
  --bw-chrome-muted:       #A8C4B9;
  --bw-chrome-hover:       rgba(255, 255, 255, .07);
  --bw-chrome-rule:        rgba(255, 255, 255, .10);
  --bw-nav-active-bg:      rgba(255, 255, 255, .11);
  --bw-nav-active-fg:      #FFFFFF;
  /* Same correction as v1: the handoff's #5E8578 is 3.72:1 on the dark sidebar. */
  --bw-nav-group:          #6F998B;
  --bw-nav-count:          #6F998B;

  --bw-accent:             #0F6E52;   /* same as primary: v2 has no separate accent */
  --bw-accent-soft:        #FEF6E7;
  --bw-accent-ink:         #94620A;
  --bw-accent-line:        #F7E3B5;

  --bw-canvas:             #F5F7F6;
  --bw-surface:            #FFFFFF;
  --bw-field:              #FAFBFA;
  --bw-input-bg:           #FFFFFF;
  --bw-readonly-bg:        #F5F7F6;

  --bw-text:               #16211D;
  --bw-text-strong:        #0C3B2E;
  --bw-muted:              #3C4A45;
  /* Same correction as v1: the handoff's #6E7A75 measures 4.47:1 on white, a hair under
     AA for small text. #697570 reads identically at 4.79:1. */
  --bw-faint:              #697570;
  /* Same correction as v1: the handoff's #98A29E is well under AA for placeholder text. */
  --bw-placeholder:        #697570;
  --bw-disabled:           #A9B2AE;

  --bw-border:             #E4E7E5;
  --bw-border-strong:      #D9DEDB;
  --bw-border-field:       #D9DEDB;
  --bw-rule:               #EDF0EE;
  --bw-row-line:           #F1F4F2;

  --bw-success:            #0A6C4E;
  --bw-success-bg:         #EAF6F0;
  --bw-success-line:       #CFE3DA;
  --bw-warning:            #94620A;
  --bw-warning-bg:         #FEF6E7;
  --bw-warning-line:       #F7E3B5;
  --bw-danger:             #B42318;
  --bw-danger-ink:         #B42318;
  --bw-danger-bg:          #FEF3F2;
  --bw-danger-line:        #F3C9C4;
  /* v2's bulk bar is light, so this is unused there — defined to keep both blocks in
     step, as the token rule requires. */
  --bw-danger-on-chrome:   #F3C9C4;
  --bw-info:               #0C3B2E;
  --bw-info-bg:            #F2F8F5;
  --bw-info-line:          #CFE3DA;

  --bw-hover-bg:           #FAFBFA;
  --bw-selected-bg:        #F2F8F5;
  --bw-alt-row:            #FFFFFF;   /* v2 has no zebra striping */
  --bw-focus-ring:         0 0 0 3px rgba(15, 110, 82, .13);
  --bw-focus-ring-error:   0 0 0 3px rgba(180, 35, 24, .12);
  --bw-overlay:            rgba(12, 42, 34, .32);
  --bw-overlay-2:          rgba(12, 42, 34, .28);   /* see the v1 block */
  /* see the v1 block for what these two are */
  --bw-skeleton-a:         #E4EAE7;
  --bw-skeleton-b:         #F3F7F5;

  --bw-radius:             8px;
  --bw-radius-sm:          7px;
  --bw-radius-lg:          10px;
  --bw-radius-xl:          11px;
  --bw-pill:               999px;     /* v2 uses true pills for badges/chips */
  --bw-chip:               999px;

  --bw-font:               'IBM Plex Sans', system-ui, -apple-system, sans-serif;
  --bw-font-mono:          'IBM Plex Mono', ui-monospace, monospace;
  --bw-text-base:          13.5px;
  --bw-text-sm:            12.5px;
  --bw-text-xs:            11.5px;
  --bw-h1-size:            24px;
  --bw-h2-size:            14.5px;
  --bw-h3-size:            13px;

  --bw-label-size:         12.5px;
  --bw-label-weight:       500;
  --bw-label-color:        #3C4A45;
  --bw-label-spacing:      0;

  /* v2's section headings are sentence case at body weight, not shouted caps —
     one of the clearest tells between the two generations in a form. */
  --bw-eyebrow-size:       13px;
  --bw-eyebrow-spacing:    .02em;
  --bw-eyebrow-transform:  none;
  --bw-eyebrow-color:      #16211D;
  --bw-eyebrow-weight:     600;

  --bw-colhead-size:       11px;
  --bw-colhead-spacing:    .05em;
  /* Darkened from the handoff's #6E7A75 (4.30:1 on the header fill) to clear AA. */
  --bw-colhead-color:      #64706B;

  --bw-form-cols:          2;         /* the drawer is narrower — 2 across */
  /* v2 splits the same way v1 does, in the same direction: the doc's list controls — the
     tools-row search, the Filters toggle, the filter-panel selects — are 34px, and the
     drawer's own fields and footer buttons are 36px. This token is the LIST height; the
     36px belongs to --bw-form-h. (Was 36px here, which made every tools-row control 2px
     taller than the design.) */
  --bw-control-h:          34px;
  --bw-control-h-sm:       30px;
  --bw-form-h:             36px;
  --bw-avatar-radius:      50%;       /* see the v1 block */
  --bw-checkbox:           15px;      /* v2 draws it a touch larger than v1 */
  --bw-checkbox-radius:    4px;       /* see the v1 block */
  --bw-btn-h:              36px;
  --bw-row-h:              44px;
  --bw-head-h:             38px;
  --bw-gap:                14px;
  --bw-pad:                17px;
  --bw-pad-lg:             22px;

  --bw-nav-w:              248px;     /* always-labelled sidebar */
  --bw-nav-w-open:         248px;     /* v2 does not collapse — no hover state */
  --bw-nav-item-h:         33px;
  --bw-topbar-h:           56px;
  --bw-topbar-bg:          #FFFFFF;
  --bw-topbar-border:      #E4E7E5;

  --bw-shadow:             0 1px 2px rgba(12, 59, 46, .05);
  --bw-shadow-lg:          0 16px 40px rgba(12, 42, 34, .14);
  --bw-shadow-pop:         -16px 0 48px rgba(12, 42, 34, .18);

  --bw-toast-w:            380px;     /* see the v1 block */

  /* dialogs: v2 is a right-hand drawer with a light header */
  --bw-dialog-w:           min(760px, 90vw);
  /* The child drawer is narrower — the doc says min(560px, 80vw) — which is what lets the
     level-1 drawer stay visible behind it. v2 keeps the parent where it is; the depth is
     conveyed by the child being inset, not by moving the parent. */
  --bw-dialog-w-back:      min(760px, 90vw);
  --bw-dialog-w-2:         min(560px, 80vw);
  --bw-dialog-head-bg:     #FFFFFF;
  --bw-dialog-head-fg:     #16211D;
  --bw-dialog-head-h:      auto;
  --bw-dialog-foot-bg:     #FAFBFA;

  /* auth split screen — v2 sizes the brand column in fractions, not pixels */
  --bw-auth-cols:          1.05fr 1fr;
  --bw-auth-brand-pad:     56px 64px;
  --bw-auth-brand-bg:      #0C2A22;
  --bw-auth-brand-fg:      #EAF2EE;
  --bw-auth-brand-lede:    #A8C4B9;
  --bw-auth-brand-item:    #C7DCD4;
  --bw-auth-brand-foot:    #6F9488;
  --bw-auth-brand-rule:    rgba(255, 255, 255, .12);
  --bw-auth-num:           #35C79A;
  --bw-auth-mark-bg:       #12855F;
  --bw-auth-mark-fg:       #FFFFFF;
  --bw-auth-mark-size:     38px;
  --bw-auth-mark-radius:   9px;
  --bw-auth-title-size:    40px;
  --bw-auth-title-spacing: -.025em;
  --bw-auth-form-w:        388px;
  --bw-auth-form-gap:      28px;
  --bw-auth-control-h:     42px;
  --bw-auth-btn-h:         44px;
}

/* ============================================================================
   3b. DARK — one override block per generation.

   Added 2026-08-17 from the interactive prototype's Light / Dark / Auto control.
   These blocks re-point COLOUR tokens only: radius, control heights, row heights,
   spacing and type all stay exactly as the generation defines them, because dark
   is not a third design — it is the same design on a dark ground.

   ⚠ The chrome deliberately does NOT change. v1's navy rail (#0A1B3D) and v2's
   deep green sidebar are already dark surfaces, and the prototype hardcodes that
   navy in both its light and dark states rather than tokenising it. Inventing a
   "darker dark" for them would only wash out the one part of the app that already
   worked, so --bw-chrome, the nav tokens, the auth brand panel and v1's dialog
   header keep their light-mode values.

   ⚠ `color-scheme: dark` is not decoration. It is what makes the browser render
   native furniture — scrollbars, the date input's own controls, autofill, form
   control defaults — dark. Without it a perfectly dark page grows white
   scrollbars the CSS cannot reach.

   Only v1's palette is specified: it comes from the prototype's own
   html[data-theme="dark"] block (--bg #0C1322, --surface #141C2E, --primary
   #2E4C93 …). ⚠ THE v2 DARK PALETTE IS DERIVED — no handoff or prototype
   specifies a dark green generation, so those values are this project's
   invention, built to the same relationships as v1's: canvas below surface,
   surface below field, borders two steps up from the canvas, and a primary
   lightened until it carries text on a dark ground. Treat them as a proposal to
   review, not as spec.
   ============================================================================ */
:root[data-theme-version="v1"][data-theme="dark"] {
  color-scheme: dark;

  /* Primary lifts from #0F2A5F to the prototype's #2E4C93: the light navy is
     nearly invisible against a dark canvas, and a fill has to read as a fill. */
  --bw-primary:            #2E4C93;
  --bw-primary-rgb:        46, 76, 147;
  --bw-primary-hover:      #3B5DB0;
  --bw-primary-active:     #24407F;
  --bw-primary-soft:       #1B2A4A;
  --bw-primary-softer:     #16233D;
  --bw-primary-soft-line:  #384663;
  --bw-on-primary:         #FFFFFF;
  /* The prototype's own --link. This is the token that fixes the grid's edit links, which
     measured 2.01:1 rendered in --bw-primary on the dark row. #9FB6E8 is 7.4:1 there. */
  --bw-link:               #9FB6E8;
  --bw-chart-mark:         #9FB6E8;   /* the primary is only 2.08:1 here — see the v1 light block */
  --bw-chart-track:        #1D2740;
  /* The same six hues stepped for a dark surface — not a different palette. Validated against
     BOTH BWDesignSystem dark surfaces (#141C2E and #13201C): all six clear 3:1 there, so unlike light
     mode there is no contrast WARN to discharge. Worst adjacent CVD ΔE 8.4, normal-vision 19.3. */
  --bw-cat-1:              #3987e5;
  --bw-cat-2:              #d95926;
  --bw-cat-3:              #199e70;
  --bw-cat-4:              #c98500;
  --bw-cat-5:              #d55181;
  --bw-cat-6:              #008300;
  --bw-link-rgb:           159, 182, 232;
  --bw-link-hover:         #C0D0F2;

  --bw-canvas:             #0C1322;
  --bw-surface:            #141C2E;
  --bw-field:              #111827;
  --bw-input-bg:           #111827;   /* light v1 inputs are white; dark ones are the field */
  --bw-readonly-bg:        #0E1523;

  --bw-text:               #E6ECF7;
  --bw-text-strong:        #EAF0FA;
  --bw-muted:              #A6B3C9;   /* lifted from the prototype's #93A0B8 — 4.5:1 on --bw-surface */
  --bw-faint:              #93A0B8;
  --bw-placeholder:        #93A0B8;
  --bw-disabled:           #64708A;

  --bw-border:             #273350;
  --bw-border-strong:      #384663;
  --bw-border-field:       #384663;
  --bw-rule:               #1E2A44;
  --bw-row-line:           #1A2438;

  --bw-hover-bg:           #1A2338;
  --bw-selected-bg:        #1B2A4A;
  --bw-alt-row:            #161F33;

  /* Semantic tones are re-picked, not darkened: #0B6B45 on #141C2E is unreadable.
     Each is a light tint for the text/icon with a deep tint behind it. */
  --bw-success:            #4FC08D;
  --bw-success-bg:         #10281F;
  --bw-success-line:       #235141;
  --bw-warning:            #E3A93F;
  --bw-warning-bg:         #2A2110;
  --bw-warning-line:       #4E3C1B;
  --bw-danger:             #F0857B;
  --bw-danger-ink:         #F5A79F;
  --bw-danger-bg:          #2A1614;
  --bw-danger-line:        #522722;
  --bw-info:               #9FB6E8;
  --bw-info-bg:            #16233D;
  --bw-info-line:          #2C3C63;

  --bw-focus-ring:         0 0 0 3px rgba(159, 182, 232, .22);
  --bw-focus-ring-error:   0 0 0 3px rgba(240, 133, 123, .20);
  --bw-overlay:            rgba(3, 7, 16, .64);
  --bw-overlay-2:          rgba(3, 7, 16, .46);
  --bw-skeleton-a:         #1B2338;
  --bw-skeleton-b:         #26314A;

  /* Elevation cannot be a darker shadow on a dark ground — there is nothing darker
     to cast onto. It is carried by a deeper, wider shadow plus the border. */
  --bw-shadow:             0 1px 2px rgba(0, 0, 0, .40);
  --bw-shadow-lg:          0 10px 28px rgba(0, 0, 0, .52);
  --bw-shadow-pop:         0 24px 60px rgba(0, 0, 0, .62);

  --bw-label-color:        #C7D1E3;
  --bw-eyebrow-color:      #A6B3C9;
  --bw-colhead-color:      #A6B3C9;

  --bw-topbar-bg:          #141C2E;
  --bw-topbar-border:      #273350;

  /* v1's dialog header is the navy chrome in both modes; only the footer moves. */
  --bw-dialog-foot-bg:     #111827;
}

:root[data-theme-version="v2"][data-theme="dark"] {
  color-scheme: dark;

  /* ⚠ Derived, not specified. v2's #0F6E52 is too dark to carry a fill here, so the
     primary lightens and its text goes DARK — a light-green button with near-black
     ink, which is the only pairing that clears 4.5:1 at this lightness. v1 keeps
     white on navy because its navy stays dark enough for that to work. */
  --bw-primary:            #2FA97F;
  --bw-primary-rgb:        47, 169, 127;
  --bw-primary-hover:      #3CBB8E;
  --bw-primary-active:     #268C68;
  --bw-primary-soft:       #16302A;
  --bw-primary-softer:     #12241F;
  --bw-primary-soft-line:  #2A4A3F;
  --bw-on-primary:         #04140F;
  /* Derived to match v1 dark's relationship: lighter than the fill, so it carries text on
     both the surface and the soft fill. */
  --bw-link:               #6FD3AA;
  --bw-chart-mark:         #6FD3AA;
  --bw-chart-track:        #1B2A25;
  /* The same six hues stepped for a dark surface — not a different palette. Validated against
     BOTH BWDesignSystem dark surfaces (#141C2E and #13201C): all six clear 3:1 there, so unlike light
     mode there is no contrast WARN to discharge. Worst adjacent CVD ΔE 8.4, normal-vision 19.3. */
  --bw-cat-1:              #3987e5;
  --bw-cat-2:              #d95926;
  --bw-cat-3:              #199e70;
  --bw-cat-4:              #c98500;
  --bw-cat-5:              #d55181;
  --bw-cat-6:              #008300;
  --bw-link-rgb:           111, 211, 170;
  --bw-link-hover:         #8FE0BF;

  --bw-canvas:             #0B1512;
  --bw-surface:            #13201C;
  --bw-field:              #101A17;
  --bw-input-bg:           #101A17;
  --bw-readonly-bg:        #0E1714;

  --bw-text:               #E4EFE9;
  --bw-text-strong:        #F0F6F3;
  --bw-muted:              #A3B5AC;
  --bw-faint:              #91A49B;
  --bw-placeholder:        #91A49B;
  --bw-disabled:           #63756C;

  --bw-border:             #223A32;
  --bw-border-strong:      #2E4A40;
  --bw-border-field:       #2E4A40;
  --bw-rule:               #1B2E28;
  --bw-row-line:           #182924;

  --bw-hover-bg:           #17251F;
  --bw-selected-bg:        #16302A;
  --bw-alt-row:            #14211D;

  --bw-success:            #45C393;
  --bw-success-bg:         #0F2A20;
  --bw-success-line:       #21503F;
  --bw-warning:            #E0B057;
  --bw-warning-bg:         #2A2211;
  --bw-warning-line:       #4C3D1C;
  --bw-danger:             #F0897E;
  --bw-danger-ink:         #F5ADA5;
  --bw-danger-bg:          #2B1715;
  --bw-danger-line:        #532924;
  --bw-info:               #8FC2AE;
  --bw-info-bg:            #14251F;
  --bw-info-line:          #2A4438;

  --bw-focus-ring:         0 0 0 3px rgba(47, 169, 127, .24);
  --bw-focus-ring-error:   0 0 0 3px rgba(240, 137, 126, .20);
  --bw-overlay:            rgba(2, 10, 8, .64);
  --bw-overlay-2:          rgba(2, 10, 8, .46);
  --bw-skeleton-a:         #1A2A24;
  --bw-skeleton-b:         #24382F;

  --bw-shadow:             0 1px 2px rgba(0, 0, 0, .40);
  --bw-shadow-lg:          0 10px 28px rgba(0, 0, 0, .52);
  --bw-shadow-pop:         -16px 0 48px rgba(0, 0, 0, .58);

  --bw-label-color:        #C9DAD2;
  --bw-eyebrow-color:      #A3B5AC;
  --bw-colhead-color:      #A3B5AC;

  --bw-topbar-bg:          #13201C;
  --bw-topbar-border:      #223A32;

  /* v2's dialog header is a light surface in light mode; in dark it becomes the
     dark surface, so its text has to flip with it. */
  --bw-dialog-head-bg:     #13201C;
  --bw-dialog-head-fg:     #F0F6F3;
  --bw-dialog-foot-bg:     #101A17;
}

/* ============================================================================
   4. FRAMEWORK BRIDGE — Bootstrap and Serenity variables, re-pointed.

   Everything below reads --bw-* only. Nothing here is a literal.

   This is what stops stock Serene colours leaking through components this file
   never names: Bootstrap derives buttons, alerts, tables, dropdowns, form
   controls, popovers and modals from --bs-*, and Serenity derives grids, dialogs,
   tabs, pagers, select2 and toolbars from --s-*. Point those at the BWDesignSystem
   tokens once and the whole surface moves together, in both versions.
   ============================================================================ */
:root[data-theme-version] {
  /* --- Bootstrap --- */
  --bs-primary:                     var(--bw-primary);
  --bs-primary-rgb:                 var(--bw-primary-rgb);
  --bs-primary-text-emphasis:       var(--bw-link);
  --bs-primary-bg-subtle:           var(--bw-primary-soft);
  --bs-primary-border-subtle:       var(--bw-border-strong);
  /* --bw-link, not --bw-primary: see the token's note. In light mode they are the same
     value, so this changes nothing there; in dark it is the difference between a
     readable link and navy text on a navy row. */
  --bs-link-color:                  var(--bw-link);
  --bs-link-color-rgb:              var(--bw-link-rgb);
  --bs-link-hover-color:            var(--bw-link-hover);

  --bs-secondary:                   var(--bw-muted);
  --bs-secondary-color:             var(--bw-muted);
  --bs-tertiary-color:              var(--bw-faint);
  --bs-emphasis-color:              var(--bw-text-strong);

  --bs-body-bg:                     var(--bw-surface);
  --bs-body-color:                  var(--bw-text);
  --bs-body-font-family:            var(--bw-font);
  --bs-body-font-size:              var(--bw-text-base);
  --bs-secondary-bg:                var(--bw-field);
  --bs-tertiary-bg:                 var(--bw-canvas);

  --bs-border-color:                var(--bw-border);
  --bs-border-color-translucent:    var(--bw-border);
  --bs-border-radius:               var(--bw-radius);
  --bs-border-radius-sm:            var(--bw-radius-sm);
  --bs-border-radius-lg:            var(--bw-radius-lg);
  --bs-border-radius-xl:            var(--bw-radius-xl);
  --bs-border-radius-pill:          var(--bw-pill);

  --bs-success:                     var(--bw-success);
  --bs-success-text-emphasis:       var(--bw-success);
  --bs-success-bg-subtle:           var(--bw-success-bg);
  --bs-success-border-subtle:       var(--bw-success-line);
  --bs-danger:                      var(--bw-danger);
  --bs-red:                         var(--bw-danger);
  --bs-danger-text-emphasis:        var(--bw-danger-ink);
  --bs-danger-bg-subtle:            var(--bw-danger-bg);
  --bs-danger-border-subtle:        var(--bw-danger-line);
  --bs-warning:                     var(--bw-warning);
  --bs-warning-text-emphasis:       var(--bw-warning);
  --bs-warning-bg-subtle:           var(--bw-warning-bg);
  --bs-warning-border-subtle:       var(--bw-warning-line);
  --bs-info:                        var(--bw-info);
  --bs-info-text-emphasis:          var(--bw-info);
  --bs-info-bg-subtle:              var(--bw-info-bg);
  --bs-info-border-subtle:          var(--bw-info-line);

  --bs-form-invalid-color:          var(--bw-danger);
  --bs-form-invalid-border-color:   var(--bw-danger);
  --bs-form-valid-color:            var(--bw-success);
  --bs-form-valid-border-color:     var(--bw-success);

  --bs-modal-bg:                    var(--bw-surface);
  --bs-modal-border-color:          var(--bw-border);
  --bs-modal-border-radius:         var(--bw-radius-lg);
  --bs-modal-header-border-color:   var(--bw-border);
  --bs-modal-footer-border-color:   var(--bw-border);
  --bs-modal-padding:               var(--bw-pad-lg);

  --bs-dropdown-bg:                 var(--bw-surface);
  --bs-dropdown-border-color:       var(--bw-border);
  --bs-dropdown-border-radius:      var(--bw-radius);
  --bs-dropdown-link-color:         var(--bw-text);
  --bs-dropdown-link-hover-bg:      var(--bw-hover-bg);
  --bs-dropdown-link-hover-color:   var(--bw-text-strong);
  --bs-dropdown-link-active-bg:     var(--bw-primary);
  --bs-dropdown-link-active-color:  var(--bw-on-primary);
  --bs-dropdown-font-size:          var(--bw-text-sm);

  --bs-nav-link-color:              var(--bw-muted);
  --bs-nav-link-hover-color:        var(--bw-text-strong);
  --bs-nav-tabs-border-color:       var(--bw-border);
  --bs-nav-tabs-link-active-color:  var(--bw-text-strong);
  --bs-nav-tabs-link-active-bg:     var(--bw-surface);

  --bs-table-bg:                    var(--bw-surface);
  --bs-table-color:                 var(--bw-text);
  --bs-table-border-color:          var(--bw-border);
  --bs-table-striped-bg:            var(--bw-alt-row);
  --bs-table-hover-bg:              var(--bw-hover-bg);

  --bs-heading-color:               var(--bw-text-strong);
  --bs-code-color:                  var(--bw-primary);
  --bs-highlight-bg:                var(--bw-accent-soft);
  --bs-focus-ring-color:            transparent;

  /* --- Serenity --- */
  --s-card-bg:                      var(--bw-surface);
  --s-card-border:                  var(--bw-border);
  --s-alt-row-bg:                   var(--bw-alt-row);
  --s-category-title:               var(--bw-eyebrow-color);
  --s-sidebar-band-bg:              var(--bw-primary);

  --s-input-bg:                     var(--bw-input-bg);
  --s-input-text:                   var(--bw-text);
  --s-input-readonly-bg:            var(--bw-readonly-bg);
  --s-input-outline:                transparent;
  --s-input-outline-error:          transparent;
  --s-input-inner-height:           calc(var(--bw-control-h) - 2px);
  --s-input-line-height:            calc(var(--bw-control-h) - 2px);
  /* ⚠ MUST carry a unit. common-theme.css consumes this inside calc() twice —
     `calc(var(--s-input-padding-v) - 3px)` on .select2-search-choice and
     `- 2px` on .select2-choices .select2-input. A UNITLESS 0 makes both calcs
     invalid, which drops the WHOLE padding shorthand to its 0 initial value and
     silently takes the chip's 18px close-button gutter with it. Written as `0`
     it looked identical in devtools and broke only those two declarations. */
  --s-input-padding-v:              0px;

  --s-placeholder:                  var(--bw-placeholder);
  --s-disabled:                     var(--bw-disabled);
  --s-inactive:                     var(--bw-faint);
  --s-deleted:                      var(--bw-faint);

  --s-modal-shadow:                 var(--bw-shadow-lg);

  --s-cell-border:                  var(--bw-row-line);
  --s-cell-input-border:            var(--bw-border-field);
  --s-column-header:                var(--bw-colhead-color);
  --s-column-header-border:         var(--bw-border);
  --s-column-header-hover-bg:       var(--bw-hover-bg);
  --s-column-footer:                var(--bw-muted);
  --s-table-title:                  var(--bw-text-strong);

  --s-tabs-border:                  var(--bw-border);
  --s-tabs-border-active:           var(--bw-primary);
  --s-tabs-text:                    var(--bw-muted);
  --s-tabs-text-active:             var(--bw-text-strong);

  --s-tool-icon:                    var(--bw-muted);
  --s-tool-icon-size:               1.1rem;
  --s-pager-icon:                   var(--bw-muted);
  --s-pager-text:                   var(--bw-muted);
  --pager-btn:                      var(--bw-muted);

  --s-col-gap:                      8px;
  --s-row-gap:                      var(--bw-gap);
  --s-tabular-font:                 var(--bw-font);

  /* Row height is deliberately NOT set here. SleekGrid writes --sg-row-height and
     --sg-cell-height inline on the grid container from gridDefaults.rowHeight, so a
     value at :root would be outranked, and if it did win the paint would disagree
     with the scroll maths. Modules/Common/ScriptInit.ts sets gridDefaults.rowHeight
     per version instead; --bw-row-h below is the token that must stay in step with it. */
}

/* ============================================================================
   5. COMPONENT LAYER — tokens only below this line.
   ============================================================================ */

/* ---- foundation ---- */
:root[data-theme-version] body {
  background-color: var(--bw-canvas);
  color: var(--bw-text);
  font-family: var(--bw-font);
  font-size: var(--bw-text-base);
  -webkit-font-smoothing: antialiased;
}
:root[data-theme-version] a { color: var(--bw-link); text-decoration: none; }
:root[data-theme-version] a:hover { color: var(--bw-primary-hover); }
:root[data-theme-version] h1,
:root[data-theme-version] h2,
:root[data-theme-version] h3,
:root[data-theme-version] h4 {
  color: var(--bw-text-strong);
  font-weight: 600;
  letter-spacing: -.01em;
}
:root[data-theme-version] ::placeholder { color: var(--bw-placeholder); opacity: 1; }
:root[data-theme-version] ::selection {
  background-color: var(--bw-primary-soft);
  color: var(--bw-text-strong);
}

/* Scrollbars are part of a dense enterprise UI's texture; the stock chunky grey
   ones read as "unstyled admin template" more than almost anything else. */
/* ⚠ THE TWO SCROLLBAR SYSTEMS ARE MUTUALLY EXCLUSIVE — do not set both.
   Specifying `scrollbar-width` or `scrollbar-color` makes Chromium ignore every
   ::-webkit-scrollbar rule on the page. So having both meant the custom bar below was
   never actually used in Chrome: what rendered was the platform scrollbar, complete with
   the stepper arrow buttons that the standard properties give no way to remove. It looked
   deliberate enough to miss, because `scrollbar-color` still tinted it.

   The feature query splits them cleanly — Chromium supports the legacy pseudo-elements
   and takes the branch below; Firefox does not, and gets the standard properties. */
@supports not selector(::-webkit-scrollbar) {
  :root[data-theme-version] * {
    scrollbar-color: var(--bw-border-strong) transparent;
    scrollbar-width: thin;
  }
}
:root[data-theme-version] ::-webkit-scrollbar { width: 10px; height: 10px; }
:root[data-theme-version] ::-webkit-scrollbar-track { background: transparent; }
:root[data-theme-version] ::-webkit-scrollbar-button { display: none; }
:root[data-theme-version] ::-webkit-scrollbar-corner { background: transparent; }
:root[data-theme-version] ::-webkit-scrollbar-thumb {
  background-color: var(--bw-border-strong);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: 99px;
}
:root[data-theme-version] ::-webkit-scrollbar-thumb:hover { background-color: var(--bw-faint); }

/* Figures read as data, not prose, in both designs. */
:root[data-theme-version] .m-mono,
:root[data-theme-version] .slick-cell.align-right,
:root[data-theme-version] .m-num {
  font-family: var(--bw-font-mono);
  font-variant-numeric: tabular-nums;
}

/* ---- shared micro-typography ---- */
:root[data-theme-version] .m-eyebrow {
  color: var(--bw-eyebrow-color);
  font-size: var(--bw-eyebrow-size);
  font-weight: var(--bw-eyebrow-weight);
  letter-spacing: var(--bw-eyebrow-spacing);
  text-transform: var(--bw-eyebrow-transform);
}
:root[data-theme-version] .m-kbd {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border-field);
  border-radius: var(--bw-radius-sm);
  color: var(--bw-faint);
  font-family: var(--bw-font-mono);
  font-size: var(--bw-text-xs);
  line-height: 1.4;
  padding: 1px 5px;
}

/* ---- chrome: sidebar ---- */
:root[data-theme-version] .s-sidebar,
:root[data-theme-version] .s-sidebar-pane {
  background-color: var(--bw-chrome);
  border: 0;
  color: var(--bw-chrome-text);
}
:root[data-theme-version] .s-sidebar-pane {
  overflow-x: hidden;
  overflow-y: auto;
  padding-bottom: var(--bw-gap);
}

/* The brand lockup at the top of the pane (Views/Shared/_Sidebar.cshtml). Both
   designs put identity in the sidebar, not the top bar — the top bar belongs to
   the page you are on. */
:root[data-theme-version] .m-brand {
  align-items: center;
  color: var(--bw-chrome-text);
  display: flex;
  flex-shrink: 0;
  gap: 11px;
  height: var(--bw-topbar-h);
  overflow: hidden;
  padding: 0 12px;
  white-space: nowrap;
}
:root[data-theme-version] .m-brand:hover { color: var(--bw-chrome-text); }
/* ---- the brand mark as an image ----
   ⚠ The coloured tile has to GO when an image is used. The tile exists to give a two-letter
   monogram a background; behind the four-colour hexagon it would frame the logo in an unrelated
   orange (v1) or green (v2) square. The classes are additive (.m-brand-mark-img on top of
   .m-brand-mark) so the monogram treatment survives untouched for anything still using it. */
:root[data-theme-version] .m-brand-mark.m-brand-mark-img,
:root[data-theme-version] .m-auth-mark.m-auth-mark-img {
  /* ⚠ COMPOUND selector — both classes on the one element — so this is (0,4,0) and beats the base
     .m-brand-mark rule at (0,3,0) whatever the order. Written as two separate class selectors first
     it TIED at (0,3,0) and lost on source order, because this block sits above the base rule: the
     logo rendered inside the orange tile it was meant to replace. */
  background-color: transparent;
  border-radius: 0;
  overflow: visible;
  padding: 0;
}
:root[data-theme-version] .m-brand-mark.m-brand-mark-img img,
:root[data-theme-version] .m-auth-mark.m-auth-mark-img img {
  display: block;
  height: 100%;
  object-fit: contain;
  width: 100%;
}

:root[data-theme-version] .m-brand-mark {
  align-items: center;
  background-color: var(--bw-auth-mark-bg);
  border-radius: var(--bw-auth-mark-radius);
  color: var(--bw-auth-mark-fg);
  display: flex;
  flex: 0 0 auto;
  font-family: var(--bw-font-mono);
  font-size: 13px;
  font-weight: 500;
  height: 32px;
  justify-content: center;
  width: 32px;
}
:root[data-theme-version] .m-brand-words { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
:root[data-theme-version] .m-brand-name { font-size: 14px; font-weight: 600; letter-spacing: -.01em; }
:root[data-theme-version] .m-brand-tag {
  color: var(--bw-nav-group);
  font-size: 9.5px;
  letter-spacing: .13em;
  text-transform: uppercase;
}

:root[data-theme-version] .s-sidebar-menu { padding: 4px 9px; }
:root[data-theme-version] .s-sidebar-link {
  align-items: center;
  border-radius: var(--bw-radius-sm);
  color: var(--bw-chrome-muted);
  display: flex;
  font-size: var(--bw-text-sm);
  font-weight: 400;
  gap: 10px;
  margin: 2px 0;
  min-height: var(--bw-nav-item-h);
  padding: 0 10px;
  white-space: nowrap;
}
:root[data-theme-version] .s-sidebar-link:hover {
  background-color: var(--bw-chrome-hover);
  color: var(--bw-chrome-text);
}
:root[data-theme-version] .s-sidebar-link:hover .s-sidebar-icon { color: inherit; }
:root[data-theme-version] .s-sidebar-item.active > .s-sidebar-link,
:root[data-theme-version] .s-sidebar-pane .active > .s-sidebar-link {
  background-color: var(--bw-nav-active-bg);
  color: var(--bw-nav-active-fg);
  font-weight: 600;
}
:root[data-theme-version] .active > .s-sidebar-link > .s-sidebar-icon { color: inherit; }
:root[data-theme-version] .s-sidebar-icon {
  color: inherit;
  flex: 0 0 18px;          /* fixed basis keeps icons on one axis whether or not
                              a label follows them */
  font-size: 15px;
  margin: 0;
  text-align: center;
}
:root[data-theme-version] .s-sidebar-menu-toggle { color: var(--bw-nav-count); font-size: 11px; }
:root[data-theme-version] .s-sidebar-menu .s-sidebar-menu { margin-left: 0; padding: 0 0 0 14px; }
:root[data-theme-version] .s-sidebar-menu .s-sidebar-menu .s-sidebar-link {
  font-size: var(--bw-text-xs);
  min-height: 30px;
}
:root[data-theme-version] .s-sidebar-group,
:root[data-theme-version] .s-sidebar-section {
  color: var(--bw-nav-group);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .13em;
  padding: 12px 10px 4px;
  text-transform: uppercase;
}

/* ---- chrome: top bar ----
   Both designs specify a WHITE top bar sitting beside the dark nav, carrying the
   page you are on plus global actions. Serenity ships it as a full-width blue
   navbar with a 48px !important height, so both have to be overridden. */
:root[data-theme-version] .s-sidebar-header {
  align-items: center;
  background-color: var(--bw-topbar-bg);
  border: 0;
  border-bottom: 1px solid var(--bw-topbar-border);
  color: var(--bw-text);
  gap: var(--bw-gap);
  height: var(--bw-topbar-h) !important;
  min-height: var(--bw-topbar-h);
  padding: 0 var(--bw-pad-lg);
}
:root[data-theme-version] .s-sidebar-toggler { color: var(--bw-muted); font-size: 20px; line-height: 1; }
:root[data-theme-version] .m-topbar-heading {
  align-items: center;
  display: flex;
  gap: 7px;
  font-size: var(--bw-text-base);
  min-width: 0;
}
:root[data-theme-version] .m-topbar-crumb { color: var(--bw-muted); white-space: nowrap; }
/* The breadcrumb chevron is decorative, so it is exempt from the text contrast rules — but
   at --bw-border-strong it measured 1.57:1 and read as barely there. --bw-disabled is close
   to the #AAB4C4 the handoff draws it in. */
:root[data-theme-version] .m-topbar-sep { color: var(--bw-disabled); font-size: 11px; }
:root[data-theme-version] .m-topbar-page {
  color: var(--bw-text-strong);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
:root[data-theme-version] .m-topbar-rule {
  background-color: var(--bw-topbar-border);
  flex: 0 0 1px;
  height: 20px;
}
:root[data-theme-version] .m-topbar-spacer { flex: 1 1 auto; }

/* The nav search moves into the top bar, where both designs draw it. It keeps
   its Serenity id and classes, so the menu-filtering script in common-theme.js —
   which binds by class and filters by document query — is untouched. */
:root[data-theme-version] .s-sidebar-search {
  align-items: center;
  background-color: var(--bw-field);
  border: 1px solid var(--bw-border-field);
  border-radius: var(--bw-radius);
  display: flex;
  flex: 1 1 auto;
  gap: 8px;
  height: var(--bw-control-h);
  margin: 0 !important;
  max-width: 420px;
  padding: 0 10px;
  position: relative;
}
:root[data-theme-version] .s-sidebar-search:focus-within {
  background-color: var(--bw-surface);
  border-color: var(--bw-primary);
}
:root[data-theme-version] .s-sidebar-search-icon {
  color: var(--bw-faint);
  font-size: 13px;
  position: static;
}
:root[data-theme-version] .s-sidebar-search-input {
  background: none;
  border: 0;
  color: var(--bw-text);
  flex: 1 1 auto;
  font-size: var(--bw-text-base);
  line-height: 1.4;
  min-width: 0;
  padding: 0;
}
:root[data-theme-version] .s-sidebar-search-input:focus { box-shadow: none; outline: 0; }

/* The V1 | V2 design-generation switch was styled here (.m-theme-switch / .m-theme-btn).
   Removed 2026-08-18 with the control itself — the generation is now set only by
   AppSettings:ThemeVersion. Not to be confused with .m-profile-theme, the Light/Dark/Auto
   segmented control in the profile menu, which is a different axis and is still here. */

:root[data-theme-version] .s-sidebar-header-actions { align-items: center; gap: 8px; }
:root[data-theme-version] .s-sidebar-header-link {
  align-items: center;
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius);
  color: var(--bw-muted);
  display: flex;
  font-size: 15px;
  gap: 8px;
  height: var(--bw-control-h);
  margin: 0;
  padding: 0 10px;
}
:root[data-theme-version] .s-sidebar-header-link:hover {
  background-color: var(--bw-hover-bg);
  border-color: var(--bw-border-strong);
  color: var(--bw-text-strong);
}
/* ---- the profile card ----
   Replaces Serene's stock dropdown (a generic fa-user glyph over the username and two
   plain rows). One markup in _Sidebar.cshtml; each generation styles it its own way —
   v1 puts a navy identity band with the orange accent hairline over a dense fact list,
   v2 a soft green band with a pill avatar and roomier rows.

   ⚠ Bootstrap positions `.dropdown-menu` and toggles `.show` on it, so this must not be
   given `display` or `position` of its own — overriding either stops the menu opening,
   or detaches it from the chip it belongs to. Only the inside is styled. */
:root[data-theme-version] .s-user-profile-menu.m-profile {
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius-lg);
  box-shadow: var(--bw-shadow-lg);
  min-width: 268px;
  overflow: hidden;
  padding: 0;
}
:root[data-theme-version] .m-profile-head {
  align-items: center;
  display: flex;
  gap: 10px;
  padding: 14px var(--bw-pad);
}
:root[data-theme-version] .m-profile-avatar {
  align-items: center;
  background-color: var(--bw-primary);
  border-radius: var(--bw-avatar-radius);
  color: var(--bw-on-primary);
  display: flex;
  flex: none;
  font-size: 15px;
  font-weight: 600;
  height: 42px;
  justify-content: center;
  letter-spacing: .02em;
  width: 42px;
}
:root[data-theme-version] .m-profile-id { display: flex; flex-direction: column; min-width: 0; }
:root[data-theme-version] .m-profile-name {
  color: var(--bw-text-strong);
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
:root[data-theme-version] .m-profile-sub {
  color: var(--bw-faint);
  font-size: var(--bw-text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Facts read as data — same treatment as the dashboard's installation panel. */
:root[data-theme-version] .m-profile-facts {
  border-top: 1px solid var(--bw-border);
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px var(--bw-pad);
}
:root[data-theme-version] .m-profile-fact { display: flex; gap: 10px; justify-content: space-between; }
:root[data-theme-version] .m-profile-fact-key { color: var(--bw-muted); font-size: var(--bw-text-sm); }
:root[data-theme-version] .m-profile-fact-val {
  color: var(--bw-text);
  font-family: var(--bw-font-mono);
  font-size: var(--bw-text-sm);
}
/* ---- the Theme control inside the profile card ----
   The prototype's layout: an uppercase eyebrow, a three-segment control whose active
   segment is filled with the primary, and a hint line under it. The segments are joined
   into one box (hairlines between, radius on the outer corners only) so they read as one
   choice rather than three buttons. */
:root[data-theme-version] .m-profile-theme {
  border-top: 1px solid var(--bw-border);
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 11px var(--bw-pad);
}
:root[data-theme-version] .m-profile-theme-label {
  color: var(--bw-eyebrow-color);
  font-size: var(--bw-eyebrow-size);
  font-weight: var(--bw-eyebrow-weight);
  letter-spacing: var(--bw-eyebrow-spacing);
  text-transform: var(--bw-eyebrow-transform);
}
:root[data-theme-version] .m-mode-switch {
  border: 1px solid var(--bw-border-strong);
  border-radius: var(--bw-radius);
  display: flex;
  overflow: hidden;
}
:root[data-theme-version] .m-mode-btn {
  align-items: center;
  background: none;
  border: 0;
  border-right: 1px solid var(--bw-border-strong);
  color: var(--bw-text);
  cursor: pointer;
  display: flex;
  flex: 1 1 0;
  font-family: var(--bw-font);
  font-size: var(--bw-text-sm);
  gap: 6px;
  justify-content: center;
  min-height: 30px;
  padding: 0 6px;
}
:root[data-theme-version] .m-mode-btn:last-child { border-right: 0; }
:root[data-theme-version] .m-mode-btn:hover { background-color: var(--bw-hover-bg); }
:root[data-theme-version] .m-mode-btn > i { font-size: 13px; }
:root[data-theme-version] .m-mode-btn.is-on {
  background-color: var(--bw-primary);
  color: var(--bw-on-primary);
  font-weight: 600;
}
:root[data-theme-version] .m-mode-btn.is-on:hover { background-color: var(--bw-primary-hover); }
:root[data-theme-version] .m-profile-theme-hint {
  color: var(--bw-faint);
  font-size: var(--bw-text-xs);
  line-height: 1.4;
}

:root[data-theme-version] .m-profile-actions {
  border-top: 1px solid var(--bw-border);
  display: flex;
  flex-direction: column;
  padding: 6px;
}
:root[data-theme-version] .m-profile-action {
  align-items: center;
  border-radius: var(--bw-radius-sm);
  color: var(--bw-text);
  display: flex;
  font-size: var(--bw-text-base);
  gap: 10px;
  padding: 8px 10px;
  text-decoration: none;
}
:root[data-theme-version] .m-profile-action i { color: var(--bw-faint); width: 15px; }
:root[data-theme-version] .m-profile-action:hover {
  background-color: var(--bw-hover-bg);
  color: var(--bw-link);
  text-decoration: none;
}
:root[data-theme-version] .m-profile-action:hover i { color: var(--bw-link); }
:root[data-theme-version] .m-profile-action.is-signout:hover { background-color: var(--bw-danger-bg); color: var(--bw-danger); }
:root[data-theme-version] .m-profile-action.is-signout:hover i { color: var(--bw-danger); }

/* v1 — the identity band is chrome-dark, with the orange accent as a hairline. That is
   the one thing v1 uses accent for: marking, never filling. */
:root[data-theme-version="v1"] .m-profile-head {
  background-color: var(--bw-chrome);
  border-bottom: 2px solid var(--bw-accent);
}
:root[data-theme-version="v1"] .m-profile-name { color: var(--bw-chrome-text); }
:root[data-theme-version="v1"] .m-profile-sub { color: var(--bw-chrome-muted); }
:root[data-theme-version="v1"] .m-profile-avatar {
  background-color: var(--bw-accent);
  color: var(--bw-chrome);
}

/* v2 — a soft tinted band instead of a dark one, matching its lighter chrome. */
:root[data-theme-version="v2"] .m-profile-head {
  background-color: var(--bw-primary-soft);
  border-bottom: 1px solid var(--bw-primary-soft-line);
}

/* The user chip: avatar plus name, as both designs draw it. */
:root[data-theme-version] .s-user-profile-link { border-radius: var(--bw-pill); padding-left: 3px; }
:root[data-theme-version] .m-user-avatar {
  align-items: center;
  background-color: var(--bw-primary);
  border-radius: 50%;
  color: var(--bw-on-primary);
  display: flex;
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 600;
  height: 26px;
  justify-content: center;
  width: 26px;
}
:root[data-theme-version] .m-user-name {
  font-size: var(--bw-text-sm);
  font-weight: 500;
  max-width: 12ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
:root[data-theme-version] .s-user-card { border-color: var(--bw-border) !important; }
:root[data-theme-version] .s-user-avatar { color: var(--bw-link); }

/* ---- page frame ---- */
:root[data-theme-version] section.content { padding: var(--bw-pad-lg); }
:root[data-theme-version] section.content-header { padding-bottom: var(--bw-gap); }
:root[data-theme-version] section.content-header > h1,
:root[data-theme-version] .page-title {
  font-size: var(--bw-h1-size);
  font-weight: 600;
  letter-spacing: -.02em;
  margin: 0;
}
:root[data-theme-version] .grid-title,
:root[data-theme-version] .panel-titlebar {
  color: var(--bw-text-strong);
  font-family: var(--bw-font);
  font-size: var(--bw-h1-size);
  font-weight: 600;
  letter-spacing: -.02em;
  margin-bottom: var(--bw-gap);
}

/* ---- cards / panels ---- */
:root[data-theme-version] .card,
:root[data-theme-version] .s-Panel {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius-xl);
  box-shadow: var(--bw-shadow);
  padding: 0;
}
:root[data-theme-version] .card-header {
  background-color: transparent;
  border-bottom: 1px solid var(--bw-rule);
  color: var(--bw-text-strong);
  font-size: var(--bw-h2-size);
  font-weight: 600;
  padding: 15px var(--bw-pad);
}
:root[data-theme-version] .card-body { padding: var(--bw-pad); }
:root[data-theme-version] .card-footer {
  background-color: var(--bw-field);
  border-top: 1px solid var(--bw-rule);
  padding: 11px var(--bw-pad);
}

/* ---- buttons ----
   One shape for every button in the app; only the fill changes. Both designs use
   a flat button with no gradient and no text shadow, so the Bootstrap defaults
   have to be flattened rather than recoloured. */
:root[data-theme-version] .btn {
  --bs-btn-focus-box-shadow: none;
  border-radius: var(--bw-radius);
  border-width: 1px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  font-size: var(--bw-text-base);
  font-weight: 500;
  line-height: 1;
  min-height: var(--bw-btn-h);
  padding: 0 13px;
  transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
:root[data-theme-version] .btn:focus,
:root[data-theme-version] .btn:focus-visible { box-shadow: var(--bw-focus-ring); outline: 0; }
:root[data-theme-version] .btn-sm { font-size: var(--bw-text-sm); min-height: var(--bw-control-h-sm); padding: 0 10px; }
:root[data-theme-version] .btn-lg { font-size: 14px; min-height: 44px; padding: 0 18px; }

:root[data-theme-version] .btn-primary {
  background-color: var(--bw-primary);
  border-color: var(--bw-primary);
  color: var(--bw-on-primary);
  font-weight: 600;
}
:root[data-theme-version] .btn-primary:hover,
:root[data-theme-version] .btn-primary:focus {
  background-color: var(--bw-primary-hover);
  border-color: var(--bw-primary-hover);
  color: var(--bw-on-primary);
}
:root[data-theme-version] .btn-primary:active,
:root[data-theme-version] .btn-primary.active {
  background-color: var(--bw-primary-active) !important;
  border-color: var(--bw-primary-active) !important;
  color: var(--bw-on-primary) !important;
}
:root[data-theme-version] .btn-secondary,
:root[data-theme-version] .btn-default,
:root[data-theme-version] .btn-light,
:root[data-theme-version] .btn-outline-secondary {
  background-color: var(--bw-surface);
  border-color: var(--bw-border-strong);
  color: var(--bw-muted);
}
:root[data-theme-version] .btn-secondary:hover,
:root[data-theme-version] .btn-default:hover,
:root[data-theme-version] .btn-light:hover,
:root[data-theme-version] .btn-outline-secondary:hover {
  background-color: var(--bw-hover-bg);
  border-color: var(--bw-border-strong);
  color: var(--bw-text-strong);
}
:root[data-theme-version] .btn-outline-primary {
  background-color: var(--bw-surface);
  border-color: var(--bw-primary);
  color: var(--bw-link);
  font-weight: 600;
}
:root[data-theme-version] .btn-outline-primary:hover {
  background-color: var(--bw-primary-soft);
  border-color: var(--bw-primary);
  color: var(--bw-link);
}
:root[data-theme-version] .btn-danger {
  background-color: var(--bw-danger);
  border-color: var(--bw-danger);
  color: var(--bw-on-primary);
}
:root[data-theme-version] .btn-danger:hover {
  background-color: var(--bw-danger-ink);
  border-color: var(--bw-danger-ink);
}
:root[data-theme-version] .btn-success { background-color: var(--bw-success); border-color: var(--bw-success); }
:root[data-theme-version] .btn-warning { background-color: var(--bw-warning); border-color: var(--bw-warning); color: var(--bw-on-primary); }
:root[data-theme-version] .btn-info    { background-color: var(--bw-info); border-color: var(--bw-info); color: var(--bw-on-primary); }
:root[data-theme-version] .btn-link    { color: var(--bw-link); font-weight: 500; text-decoration: none; }

/* ---- outline variants ----
   ⚠ Bootstrap compiles LITERAL hex values into each variant's own `--bs-btn-*` custom
   properties, so the `--bs-danger` / `--bs-success` bridge in section 4 never reaches
   them. Any variant the app can render therefore has to be pointed at the tokens by hand,
   or stock Bootstrap colours surface — the column picker's "Restore Defaults" button was
   rendering #DC3545 against a theme whose danger is #C0392B. Setting the custom
   properties rather than `color`/`border-color` means hover and active follow too. */
:root[data-theme-version] .btn-outline-danger {
  --bs-btn-color: var(--bw-danger);
  --bs-btn-border-color: var(--bw-danger);
  --bs-btn-hover-color: var(--bw-on-primary);
  --bs-btn-hover-bg: var(--bw-danger);
  --bs-btn-hover-border-color: var(--bw-danger);
  --bs-btn-active-color: var(--bw-on-primary);
  --bs-btn-active-bg: var(--bw-danger-ink);
  --bs-btn-active-border-color: var(--bw-danger-ink);
}
:root[data-theme-version] .btn-outline-success {
  --bs-btn-color: var(--bw-success);
  --bs-btn-border-color: var(--bw-success);
  --bs-btn-hover-color: var(--bw-on-primary);
  --bs-btn-hover-bg: var(--bw-success);
  --bs-btn-hover-border-color: var(--bw-success);
}
:root[data-theme-version] .btn-outline-warning {
  --bs-btn-color: var(--bw-warning);
  --bs-btn-border-color: var(--bw-warning);
  --bs-btn-hover-color: var(--bw-on-primary);
  --bs-btn-hover-bg: var(--bw-warning);
  --bs-btn-hover-border-color: var(--bw-warning);
}
:root[data-theme-version] .btn-outline-info,
:root[data-theme-version] .btn-outline-dark {
  --bs-btn-color: var(--bw-text-strong);
  --bs-btn-border-color: var(--bw-border-strong);
  --bs-btn-hover-color: var(--bw-text-strong);
  --bs-btn-hover-bg: var(--bw-hover-bg);
  --bs-btn-hover-border-color: var(--bw-border-strong);
}

/* ---- form controls ---- */
/* ⚠ The `[class*="Editor"]` selectors are not belt-and-braces. Serenity marks editors
   inside a property grid with `.editor`, but editors created anywhere ELSE — quick
   filters above all — carry only their type class (`s-StringEditor`, `s-SelectEditor`).
   Without these, a quick filter input inherits nothing from this file and renders as a
   bare 20px-tall browser input next to a 36px select2, which is exactly how the filter
   panel looked wrong. Both select2 helper inputs must be excluded: `.select2-offscreen`
   is a hidden 1px field and `.select2-input` is its internal search box, and styling
   either breaks the control. */
:root[data-theme-version] .form-control,
:root[data-theme-version] .form-select,
/* ⚠ Checkboxes and radios must be excluded. They are `input.editor` too, so without this
   they were handed the whole text-field treatment — border, padding, background and
   `min-height: var(--bw-control-h)`. The min-height in particular stretched a checkbox to
   36px, which is why it would not line up with the text input beside it however its own
   height was set: `min-height` always wins over `height`. */
:root[data-theme-version] input.editor:not([type="checkbox"]):not([type="radio"]),
:root[data-theme-version] select.editor,
:root[data-theme-version] textarea.editor,
:root[data-theme-version] input[class*="Editor"]:not(.select2-offscreen):not(.select2-input):not([type="checkbox"]):not([type="radio"]),
:root[data-theme-version] select[class*="Editor"],
:root[data-theme-version] textarea[class*="Editor"],
:root[data-theme-version] .field input[type="text"],
:root[data-theme-version] .field input[type="email"],
:root[data-theme-version] .field input[type="password"],
:root[data-theme-version] .field input[type="number"],
:root[data-theme-version] .field input[type="date"] {
  background-color: var(--bw-input-bg);
  border: 1px solid var(--bw-border-field);
  border-radius: var(--bw-radius-sm);
  box-shadow: none;
  color: var(--bw-text);
  font-family: var(--bw-font);
  font-size: var(--bw-text-base);
  line-height: 1.4;
  min-height: var(--bw-control-h);
  padding: 0 10px;
}
:root[data-theme-version] textarea.editor,
:root[data-theme-version] textarea.form-control { padding: 8px 10px; }

/* Focus is a border colour change plus a soft ring — both designs are flat, so
   the Bootstrap glow is replaced rather than recoloured. */
:root[data-theme-version] .form-control:focus,
:root[data-theme-version] .form-select:focus,
:root[data-theme-version] input.editor:not([type="checkbox"]):not([type="radio"]):focus,
:root[data-theme-version] select.editor:focus,
:root[data-theme-version] textarea.editor:focus,
:root[data-theme-version] input[class*="Editor"]:not(.select2-offscreen):not(.select2-input):not([type="checkbox"]):not([type="radio"]):focus,
:root[data-theme-version] select[class*="Editor"]:focus,
:root[data-theme-version] textarea[class*="Editor"]:focus,
:root[data-theme-version] input:focus-visible,
:root[data-theme-version] select:focus-visible,
:root[data-theme-version] textarea:focus-visible {
  background-color: var(--bw-surface);
  border-color: var(--bw-primary);
  box-shadow: var(--bw-focus-ring);
  outline: 0;
}
:root[data-theme-version] input.error,
:root[data-theme-version] select.error,
:root[data-theme-version] textarea.error,
:root[data-theme-version] .error .select2-choice { border-color: var(--bw-danger); }
:root[data-theme-version] input.error:focus,
:root[data-theme-version] select.error:focus,
:root[data-theme-version] textarea.error:focus { box-shadow: var(--bw-focus-ring-error); }
/* ⚠ A read-only field must LOOK read-only, and this rule silently did nothing.
   Two specificity facts, both counter-intuitive, and it took mirroring the base rule exactly
   to beat it:
     1. `:not()` contributes the specificity of its most specific ARGUMENT. So the base control
        rule's `input[class*="Editor"]:not(.select2-offscreen):not(.select2-input)
        :not([type="checkbox"]):not([type="radio"])` is **(0,7,1)** — :root, the version
        attribute, the class-substring match and FOUR :not()s, not the (0,2,1) it reads as.
     2. In a selector LIST each selector carries its own specificity, and a Serenity editor
        matches the `[class*="Editor"]` branch through `s-StringEditor` — so that is the branch
        that wins, not the `.editor` one.
   `.editor.readonly` (0,4,0), `.field input[readonly]` (0,4,1) and even
   `.field input.editor[readonly]:not(...)` (0,6,1) all lost at any position in the file. The
   symptom: a field with the editable background, the editable border and a caret — click it,
   type, and nothing happens. Found on the Features dialog's Key field; the navigation forms'
   Path field had it too.
   Adding `.readonly` to the base rule's own branch puts this at (0,8,1), which wins outright
   rather than by source order. */
:root[data-theme-version] input.readonly[class*="Editor"]:not(.select2-offscreen):not(.select2-input):not([type="checkbox"]):not([type="radio"]),
:root[data-theme-version] textarea.readonly[class*="Editor"],
:root[data-theme-version] input.editor.readonly:not([type="checkbox"]):not([type="radio"]),
:root[data-theme-version] textarea.editor.readonly,
:root[data-theme-version] .field input.readonly,
:root[data-theme-version] .field textarea.readonly {
  background-color: var(--bw-readonly-bg);
  border-color: var(--bw-border);
  color: var(--bw-muted);
  cursor: default;
}
/* No focus ring either — a ring says "type here". */
:root[data-theme-version] input.readonly[class*="Editor"]:not(.select2-offscreen):not(.select2-input):not([type="checkbox"]):not([type="radio"]):focus,
:root[data-theme-version] textarea.readonly[class*="Editor"]:focus {
  border-color: var(--bw-border);
  box-shadow: none;
}
:root[data-theme-version] input[type="checkbox"],
:root[data-theme-version] input[type="radio"] {
  accent-color: var(--bw-primary);
  border-color: var(--bw-border-field);
}
:root[data-theme-version] .form-check-input:checked {
  background-color: var(--bw-primary);
  border-color: var(--bw-primary);
}

/* Labels are sentence case in BOTH designs (v1 login and v1 create dialog both
   draw them that way). Uppercase is reserved for column heads and eyebrows. */
:root[data-theme-version] label,
:root[data-theme-version] .caption,
:root[data-theme-version] .quick-filter-label {
  color: var(--bw-label-color);
  font-size: var(--bw-label-size);
  font-weight: var(--bw-label-weight);
  letter-spacing: var(--bw-label-spacing);
  text-transform: none;
}
:root[data-theme-version] .caption sup { color: var(--bw-danger); }

/* ---- field validation ----
   ⚠ ROW ALIGNMENT. A `.field` is a wrapping flex container, and `align-content` defaults
   to `stretch`, so its free space is shared out between the wrapped lines. The moment one
   field in a row gains a validation message it has less free space than its neighbours —
   and its input lifts ~16px while theirs stay put, breaking the row. Packing the lines to
   the top makes a field's input sit under its label at the same height whether or not an
   error is showing.

   ⚠ MESSAGE TEXT. Serenity collapses the message into an 18x18 `overflow: hidden` box and
   leaves the sentence only in the `title` — so the error reads as a bare red dot and the
   reason is a tooltip away. Both handoffs print it as red text beneath the field, which is
   also the only version a keyboard or screen-reader user gets without hovering. */
:root[data-theme-version] .field { align-content: flex-start; }
:root[data-theme-version] .field > .vx {
  flex: 0 0 100%;
  height: auto;
  overflow: visible;
  width: 100%;
}
:root[data-theme-version] .field > .vx > label.error {
  color: var(--bw-danger-ink);
  display: block;
  font-size: var(--bw-text-xs);
  font-weight: 400;
  height: auto;
  line-height: 1.4;
  overflow: visible;
  padding-top: 4px;
  text-transform: none;
  white-space: normal;
  width: auto;
}
/* The glyph Serenity puts in front of the collapsed marker is redundant once the sentence
   itself is visible, and neither handoff draws one per field. */
:root[data-theme-version] .field > .vx > label.error::before { content: none; }
/* The errored control itself, per the handoff: danger border plus a soft danger ring.
   ⚠ The `[class*="Editor"]:not(…):not(…)` chain is not decoration — the base control rule
   above is (0,5,1), so a plain `input.error` at (0,4,1) loses to it and the errored field
   kept its ordinary grey border. Matching the same pattern and adding `:not(.select2-input)`
   puts this at (0,6,1), which wins outright rather than by source order. */
:root[data-theme-version] .field input.error[class*="Editor"]:not(.select2-input),
:root[data-theme-version] .field select.error[class*="Editor"]:not(.select2-input),
:root[data-theme-version] .field textarea.error[class*="Editor"]:not(.select2-input),
:root[data-theme-version] .field .select2-container.error {
  border-color: var(--bw-danger);
}
:root[data-theme-version] .field input.error[class*="Editor"]:not(.select2-input):focus,
:root[data-theme-version] .field select.error[class*="Editor"]:not(.select2-input):focus,
:root[data-theme-version] .field textarea.error[class*="Editor"]:not(.select2-input):focus {
  border-color: var(--bw-danger);
  box-shadow: var(--bw-focus-ring-error);
}

/* ---- property grid: fields and categories ---- */
:root[data-theme-version] .field { padding: 7px 8px; }
:root[data-theme-version] .field .caption {
  line-height: var(--bw-control-h);
  margin-right: var(--bw-gap);
}
:root[data-theme-version] .s-vertical-form .field .caption {
  line-height: 1.4;
  padding-bottom: 5px;
}
:root[data-theme-version] .category-title {
  color: var(--bw-eyebrow-color);
  font-size: var(--bw-eyebrow-size);
  font-weight: var(--bw-eyebrow-weight);
  letter-spacing: var(--bw-eyebrow-spacing);
  padding: 0 0 0 8px;
  text-transform: var(--bw-eyebrow-transform);
}
:root[data-theme-version] .category-title a { color: inherit; font-weight: inherit; }
/* The section rule beside the heading is in both docs; Serenity draws it dashed
   and grey, both designs draw it solid and faint. */
:root[data-theme-version] .category-title::before { border-bottom: 0; margin-right: 0; width: 0; }
:root[data-theme-version] .category-title::after {
  border-bottom: 1px solid var(--bw-rule);
  margin-left: 10px;
}
:root[data-theme-version] .categories > .category:not(:first-child) { margin-top: var(--bw-pad-lg); }

/* ---- select2 (lookup editors, quick filters, dropdowns) ---- */
/* ⚠ min-height, NOT height. Pinning the container to the control height while its
   inner .select2-choices computed to 34px let the child overflow the rounded border by
   a couple of pixels — which, once the focus ring was drawn around the parent, read as
   two mismatched boxes stacked on each other rather than one focused field. A multi
   select also has to be free to grow as chosen values wrap onto a second line. */
:root[data-theme-version] .select2-container {
  background-color: var(--bw-input-bg);
  border: 1px solid var(--bw-border-field);
  border-radius: var(--bw-radius-sm);
  min-height: var(--bw-control-h);
  position: relative;
}
:root[data-theme-version] .select2-container.select2-container-active,
:root[data-theme-version] .select2-container.select2-dropdown-open {
  border-color: var(--bw-primary);
  box-shadow: var(--bw-focus-ring);
}
/* Single-value control keeps a fixed height; the multi-value list grows. Both leave a
   gutter on the right for the chevron. */
:root[data-theme-version] .select2-choice {
  background-color: transparent;
  border: 0;
  color: var(--bw-text);
  font-size: var(--bw-text-base);
  height: calc(var(--bw-control-h) - 2px);
  line-height: calc(var(--bw-control-h) - 2px);
  padding: 0 26px 0 10px;
}
:root[data-theme-version] .select2-choices {
  align-items: center;
  background-color: transparent;
  border: 0;
  color: var(--bw-text);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--bw-text-base);
  gap: 4px;
  height: auto;
  min-height: calc(var(--bw-control-h) - 2px);
  padding: 2px 26px 2px 8px;
}
/* select2's own search input sits inside the control and must be invisible as a field —
   any border, ring or fill of its own is a second box inside the first. */
:root[data-theme-version] .select2-container .select2-search-field,
:root[data-theme-version] .select2-container input.select2-input {
  background: transparent;
  border: 0;
  box-shadow: none;
  margin: 0;
  min-height: 0;
  outline: 0;
  padding: 0;
}

/* ---- the dropdown affordance ----
   ⚠ select2 renders `.select2-arrow` for SINGLE-value controls only. A multi-value
   lookup — which is what most quick filters are — gets no indicator at all, so it reads
   as a plain empty text box with no sign it opens a list. This draws the same chevron on
   the container itself, and flips it to the primary colour while open so the control
   says what it is doing. */
:root[data-theme-version] .select2-arrow {
  border-left: 0;
  color: var(--bw-muted);
  width: 26px;
}
:root[data-theme-version] .select2-container-multi::after {
  color: var(--bw-muted);
  content: "\f107";
  font-family: var(--s-icon-font);
  font-weight: var(--s-icon-solid);
  font-size: 13px;
  line-height: 1;
  pointer-events: none;                /* the chevron must not eat the click */
  position: absolute;
  right: 9px;
  top: 50%;
  transform: translateY(-50%);
}
:root[data-theme-version] .select2-container-multi.select2-dropdown-open::after { content: "\f106"; }
:root[data-theme-version] .select2-dropdown-open .select2-arrow,
:root[data-theme-version] .select2-container-multi.select2-dropdown-open::after { color: var(--bw-link); }
:root[data-theme-version] .select2-drop {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius);
  box-shadow: var(--bw-shadow-lg);
  color: var(--bw-text);
  margin-top: 4px;
  overflow: hidden;
  padding: 4px;
}
:root[data-theme-version] .select2-drop.select2-drop-above { margin-top: -4px; }
:root[data-theme-version] .select2-results { font-size: var(--bw-text-base); padding: 0; }
:root[data-theme-version] .select2-results .select2-result-label {
  border-radius: var(--bw-radius-sm);
  padding: 7px 9px;
}
:root[data-theme-version] .select2-results .select2-highlighted {
  background-color: var(--bw-primary-soft);
  color: var(--bw-text-strong);
}
:root[data-theme-version] .select2-results .select2-highlighted .select2-result-label { background: none; }
:root[data-theme-version] .select2-search input {
  background-color: var(--bw-field);
  border: 1px solid var(--bw-border-field);
  border-radius: var(--bw-radius-sm);
  color: var(--bw-text);
  min-height: var(--bw-control-h-sm);
  padding: 0 8px;
}
/* A chosen value in a multi-value lookup ("Roles" on the user dialog). This rule
   used to set only colour, radius and font size and let common-theme.css supply the
   LAYOUT — which meant it inherited `display: list-item` and, once the --s-input-padding-v
   calc above died, `padding: 0`. The chip then shrank to roughly the width of its own
   text while the pill radius kept rounding it, so a selected value read as a small
   round blob sitting behind the word rather than a chip around it, with the ✕ printed
   on top of the first letter. Layout is declared here now so the chip does not depend
   on a framework value that can silently evaluate to nothing. */
:root[data-theme-version] .select2-search-choice {
  align-items: center;
  background-color: var(--bw-primary-soft);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-chip);
  color: var(--bw-text-strong);
  display: inline-flex;
  font-size: var(--bw-text-sm);
  line-height: 1;
  /* .select2-choices already spaces its children with gap: 4px; common-theme's
     `margin: 2px 0 2px 5px` would add a second, uneven gap on top of it. */
  margin: 0;
  max-width: 100%;
  min-height: calc(var(--bw-control-h) - 12px);
  /* right gutter is the ✕'s hit area — see the close rule below */
  padding: 0 20px 0 9px;
  position: relative;
}
/* select2 wraps the label in a bare <div>, so a long role name would otherwise push
   the ✕ out of the chip instead of truncating. */
:root[data-theme-version] .select2-search-choice > div {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* ⚠ Stock select2 pins the ✕ on the LEFT (`.select2-choices .select2-search-choice-close
   { left: 3px }`) and reserves room for it with the chip's 18px left padding. Every
   other removable chip in BWDesignSystem — the filter chips on the list page — puts the ✕ on
   the right, so it is moved to match. `left` must be reset to auto explicitly: with
   left, right and width all set the declaration is over-constrained and LTR keeps
   `left`, so setting `right` alone moves nothing. */
:root[data-theme-version] .select2-search-choice .select2-search-choice-close {
  align-items: center;
  color: var(--bw-muted);
  display: flex;
  height: 100%;
  justify-content: center;
  left: auto;
  right: 0;
  top: 0;
  width: 20px;
}
:root[data-theme-version] .select2-search-choice .select2-search-choice-close:hover,
:root[data-theme-version] .select2-search-choice .select2-search-choice-close:focus-visible {
  color: var(--bw-danger);
}
:root[data-theme-version] .select2-no-results,
:root[data-theme-version] .select2-searching,
:root[data-theme-version] .select2-more-results { color: var(--bw-faint); font-size: var(--bw-text-sm); padding: 8px 9px; }

/* ---- date editors: the calendar trigger belongs INSIDE the control ----
   ⚠ Serenity appends the picker as a `<button class="ui-datepicker-trigger">` that is the
   input's NEXT SIBLING (`flatPickrTrigger` in corelib), and common-theme.css styles it for
   an inline/float layout at `font-size: 1.5rem`. Our `.field` is a WRAPPING flex container
   whose editor is `flex: 1 1 100%`, so the button never fit on the input's line: it dropped
   to a line of its own, leaving a calendar glyph dangling below Licence Start / Licence End
   and making those fields 30px taller than the plain text field beside them — the whole row
   stopped lining up. Both handoffs draw the glyph *inside* the control at its right edge.

   ⚠ Pulled back onto the input's line with a NEGATIVE MARGIN rather than positioned
   absolutely, and this is the point of the fix: the wrapped line always begins directly
   below the input, so the offset stays right even when a long label wraps to two lines,
   whereas an absolute `top` would have to guess the label's height. `margin-left: auto`
   puts it at the right end of that full-width line, and because its outer height becomes
   zero the collapsed line adds nothing — the validation message still sits under the input.
   `position: relative` here is for stacking only; the button stays in flow.

   Uses --bw-control-h, so it follows the dialog's taller --bw-form-h override automatically.
   Scoped to `.field >` so the filter panel's own trigger (`.s-FilterPanel div.v`) is left
   alone. No form in this app uses a DateTimeEditor, which adds a time dropdown between the
   input and the button — if one is added, check this rule against it. */
:root[data-theme-version] .field > .ui-datepicker-trigger {
  align-items: center;
  color: var(--bw-muted);
  display: flex;
  height: var(--bw-control-h);
  justify-content: center;
  line-height: 1;
  margin-left: auto;
  margin-top: calc(-1 * var(--bw-control-h));
  padding: 0;
  position: relative;
  width: 30px;
  z-index: 1;
}
:root[data-theme-version] .field > .ui-datepicker-trigger:hover { color: var(--bw-link); }
/* The glyph is a ::before on the button, sized 1.5rem by common-theme.css — far larger
   than the 13px both docs draw. */
:root[data-theme-version] .field > .ui-datepicker-trigger::before { font-size: 13px; }
/* Reserve the glyph's room so a long date can never run underneath it. */
:root[data-theme-version] .field > input.flatpickr-input { padding-right: 30px; }

/* ---- flatpickr ---- */
:root[data-theme-version] .flatpickr-calendar {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius);
  box-shadow: var(--bw-shadow-lg);
  font-family: var(--bw-font);
}
:root[data-theme-version] .flatpickr-months .flatpickr-month,
:root[data-theme-version] .flatpickr-weekday { color: var(--bw-muted); fill: var(--bw-muted); }
:root[data-theme-version] .flatpickr-day { border-radius: var(--bw-radius-sm); color: var(--bw-text); }
:root[data-theme-version] .flatpickr-day:hover { background-color: var(--bw-hover-bg); border-color: transparent; }
:root[data-theme-version] .flatpickr-day.selected,
:root[data-theme-version] .flatpickr-day.selected:hover {
  background-color: var(--bw-primary);
  border-color: var(--bw-primary);
  color: var(--bw-on-primary);
}
:root[data-theme-version] .flatpickr-day.today { border-color: var(--bw-accent); }

/* ---- toolbar ----
   Serenity draws the toolbar as a joined button group with -1px margins. Both
   designs draw separate outlined buttons with an emphasised primary action, so
   the joining is undone rather than recoloured. */
:root[data-theme-version] div.s-Toolbar {
  align-items: center;
  background-color: transparent;
  border: 0;
  gap: 6px;
  margin-bottom: var(--bw-gap);
  padding: 0;
}
:root[data-theme-version] div.s-Toolbar > * { margin: 0; }
:root[data-theme-version] .tool-group { gap: 6px; }
:root[data-theme-version] div.s-Toolbar .tool-button {
  align-items: center;
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border-strong);
  border-radius: var(--bw-radius) !important;
  color: var(--bw-muted);
  display: inline-flex;
  font-size: var(--bw-text-sm);
  font-weight: 500;
  gap: 6px;
  margin: 0;
  min-height: var(--bw-btn-h);
  padding: 0 11px;
}
:root[data-theme-version] div.s-Toolbar .tool-button:hover {
  border-color: var(--bw-primary);
  color: var(--bw-link);
}
/* Serenity draws the tool icon as a ::before INSIDE .button-inner, so the flex gap
   on .tool-button never reaches it and the glyph sits on top of the first letter.
   The inner span has to be a flex container of its own. */
:root[data-theme-version] div.s-Toolbar .tool-button .button-inner {
  align-items: center;
  display: inline-flex;
  gap: 7px;
}
:root[data-theme-version] div.s-Toolbar .tool-button .button-inner:before { color: inherit; }
:root[data-theme-version] div.s-Toolbar .tool-button.add-button {
  background-color: var(--bw-primary);
  border-color: var(--bw-primary);
  color: var(--bw-on-primary);
  font-weight: 600;
  padding: 0 14px;
}
:root[data-theme-version] div.s-Toolbar .tool-button.add-button:hover {
  background-color: var(--bw-primary-hover);
  border-color: var(--bw-primary-hover);
  color: var(--bw-on-primary);
}
:root[data-theme-version] div.s-Toolbar .tool-button.delete-button:hover {
  border-color: var(--bw-danger);
  color: var(--bw-danger);
}
:root[data-theme-version] div.s-Toolbar .tool-button.disabled { opacity: .45; }
/* ⚠ Serenity puts .icon-tool-button on EVERY toolbar button in 10.3.6, not just the
   icon-only ones — .no-text is the class that actually means "no label". Squaring off
   on .icon-tool-button alone therefore stripped the horizontal padding from
   "New User" too and its label spilled past the button edge. */
:root[data-theme-version] div.s-Toolbar .tool-button.no-text {
  justify-content: center;
  min-width: var(--bw-btn-h);
  padding: 0;
  width: var(--bw-btn-h);
}
:root[data-theme-version] div.s-Toolbar .tool-button.no-text .button-inner { gap: 0; }

/* ---- quick search + quick filters ---- */
:root[data-theme-version] .s-QuickSearchBar { position: relative; }
:root[data-theme-version] .s-QuickSearchInput {
  background-color: var(--bw-field);
  border: 1px solid var(--bw-border-field);
  border-radius: var(--bw-radius);
  /* The search sits in the tools row, so it takes the list-control height — the same as
     the Filters toggle next to it (v1 32px, v2 34px). It was on --bw-btn-h, which is the
     page-action height and 2px taller in v2. */
  min-height: var(--bw-control-h);
  padding-left: 2.2rem;
}
:root[data-theme-version] .s-QuickSearchInput:focus { background-color: var(--bw-surface); }
:root[data-theme-version] .quick-search-icon { color: var(--bw-faint); font-size: 1rem; }
:root[data-theme-version] .quick-search-field { color: var(--bw-muted); font-size: var(--bw-text-sm); }
:root[data-theme-version] .quick-search-field > a { color: var(--bw-muted); }

:root[data-theme-version] .quick-filters-bar {
  align-items: flex-end;
  background-color: var(--bw-field);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius);
  gap: var(--bw-gap);
  margin-bottom: var(--bw-gap);
  padding: var(--bw-gap);
}
:root[data-theme-version] .quick-filter-item { margin: 0; }
:root[data-theme-version] .quick-filter-label {
  color: var(--bw-faint);
  display: block;
  font-size: var(--bw-text-xs);
  font-weight: 600;
  letter-spacing: var(--bw-colhead-spacing);
  padding-bottom: 4px;
  text-transform: uppercase;
}

/* ---- filter panel + display bar ---- */
:root[data-theme-version] .s-FilterPanel {
  background-color: var(--bw-field);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius);
  padding: var(--bw-gap);
}
:root[data-theme-version] .s-FilterDisplayBar {
  background-color: var(--bw-primary-soft);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius);
  color: var(--bw-text-strong);
  font-size: var(--bw-text-sm);
  margin-bottom: var(--bw-gap);
  padding: 8px var(--bw-gap);
}
:root[data-theme-version] .s-FilterPanel .filter-line { align-items: center; gap: 6px; }
:root[data-theme-version] .s-FilterPanel .andor,
:root[data-theme-version] .s-FilterPanel .leftparen,
:root[data-theme-version] .s-FilterPanel .rightparen { color: var(--bw-link); font-weight: 600; }

/* ---- data grid ----
   The grid is where the two generations diverge most: v1 is a 32px dense table
   flush inside a squared card, v2 a 44px roomy one inside an 11px-rounded card
   with no zebra striping. Row height itself is a JS value (ScriptInit.ts) because
   SleekGrid measures and caches it; everything else is here. */
/* ⚠ The second selector is not redundant. Serenity ships
   `.full-height-page section.content > .s-DataGrid { border: none; box-shadow: none }`
   at (0,3,1), which outranks a plain `:root[data-theme-version] .s-DataGrid` at
   (0,3,0) — so on every list page (they all set full-height-page on <html>) the card
   silently lost its border and the grid bled into the canvas. Matching the ancestor
   explicitly takes it to (0,5,1) and settles it. */
:root[data-theme-version] .s-DataGrid,
:root[data-theme-version].full-height-page section.content > .s-DataGrid {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius-xl);
  box-shadow: var(--bw-shadow);
  overflow: hidden;
  padding: 0;
}
/* The toolbar and quick filters sit above the table but inside the card, exactly
   as both docs draw them, so they get the card's padding rather than the page's. */
:root[data-theme-version] .s-DataGrid > div.s-Toolbar,
:root[data-theme-version] .s-DataGrid > .quick-filters-bar,
:root[data-theme-version] .s-DataGrid > .s-QuickSearchBar {
  background-color: transparent;
  border: 0;
  border-bottom: 1px solid var(--bw-rule);
  border-radius: 0;
  margin: 0;
  padding: 11px var(--bw-gap);
}
:root[data-theme-version] .s-DataGrid > .grid-title {
  border-bottom: 1px solid var(--bw-rule);
  margin: 0;
  padding: var(--bw-gap) var(--bw-gap) 11px;
}
:root[data-theme-version] .grid-container { border: 0; }

:root[data-theme-version] .slick-header,
:root[data-theme-version] .slick-header-columns {
  background: var(--bw-field);
  border: 0;
  border-bottom: 1px solid var(--bw-border);
  height: var(--bw-head-h);
}
:root[data-theme-version] .slick-header-column {
  align-items: center;
  background: transparent;
  border-right: 0;
  color: var(--bw-colhead-color);
  display: flex;
  font-size: var(--bw-colhead-size);
  font-weight: 600;
  height: var(--bw-head-h);
  letter-spacing: var(--bw-colhead-spacing);
  padding: 0 10px;
  text-transform: uppercase;
}
:root[data-theme-version] .slick-header-column:hover,
:root[data-theme-version] .slick-header-column-active { background: var(--bw-hover-bg); }
:root[data-theme-version] .slick-sort-indicator { color: var(--bw-link); }
:root[data-theme-version] .slick-row { background-color: var(--bw-surface); }
:root[data-theme-version] .slick-row.odd { background-color: var(--bw-alt-row); }
:root[data-theme-version] .slick-cell {
  border-right: 0;
  border-bottom: 1px solid var(--bw-row-line);
  color: var(--bw-text);
  font-size: var(--bw-text-base);
  padding: 0 10px;
  display: flex;
  align-items: center;
}
/* ⚠ The rule above makes every cell a FLEX CONTAINER so its text centres vertically at
   any row height — and that silently breaks column alignment. SleekGrid aligns a column
   by putting `text-align` on the cell, but the cell's content is a bare text node, i.e.
   an ANONYMOUS FLEX ITEM, and text-align does not position flex items. So [AlignRight]
   and [AlignCenter] resolved to `text-align: right/center` that computed correctly, read
   back correctly in devtools, and moved nothing.
   It went unnoticed because no grid had a numeric column until Franchisees: Users,
   Roles, Languages and Translations are all left-aligned text. Mapping the alignment
   onto justify-content is what actually moves the glyphs. */
:root[data-theme-version] .slick-cell.align-right { justify-content: flex-end; }
:root[data-theme-version] .slick-cell.align-center { justify-content: center; }
/* Column HEADS are flex containers too, and SleekGrid aligns them with the same classes. */
:root[data-theme-version] .slick-header-column.align-right .slick-column-name { justify-content: flex-end; }
:root[data-theme-version] .slick-header-column.align-center .slick-column-name { justify-content: center; }
:root[data-theme-version] .slick-cell a { color: var(--bw-link); font-weight: 600; }
:root[data-theme-version] .slick-row:hover .slick-cell { background-color: var(--bw-hover-bg); }
:root[data-theme-version] .slick-row.active .slick-cell,
:root[data-theme-version] .slick-cell.selected,
:root[data-theme-version] .slick-row.selected .slick-cell { background-color: var(--bw-selected-bg); }
:root[data-theme-version] .slick-cell.editable {
  background-color: var(--bw-surface);
  box-shadow: inset 3px 0 0 var(--bw-accent);
}
:root[data-theme-version] .slick-group,
:root[data-theme-version] .slick-group-totals {
  background-color: var(--bw-field);
  color: var(--bw-text-strong);
  font-weight: 600;
}
:root[data-theme-version] .slick-viewport { background-color: var(--bw-surface); }
:root[data-theme-version] .slick-headerrow,
:root[data-theme-version] .slick-headerrow-columns {
  background-color: var(--bw-field);
  border-bottom: 1px solid var(--bw-border);
}
:root[data-theme-version] .slick-footerrow-column {
  background-color: var(--bw-field);
  border-top: 1px solid var(--bw-border);
  color: var(--bw-text-strong);
  font-weight: 600;
}

/* status chips inside cells — the radius token alone separates the versions */
:root[data-theme-version] .slick-cell .badge,
:root[data-theme-version] .s-DataGrid .badge {
  border-radius: var(--bw-pill);
  font-size: var(--bw-text-xs);
  font-weight: 600;
  letter-spacing: 0;
  padding: 3px 8px;
}

/* ---- list page chrome ----
   The designed list screen, built by Modules/Common/UI/ListPageChrome.ts, which moves
   Serenity's existing controls into these slots and adds the two it does not ship (the
   filter chips and the density switch). Same markup for both generations; v1 comes out
   dense and square, v2 roomy and pill-shaped, purely from the tokens. */
:root[data-theme-version] .m-list-head {
  align-items: flex-end;
  display: flex;
  flex-wrap: wrap;
  gap: var(--bw-gap);
  justify-content: space-between;
  margin-bottom: var(--bw-gap);
}
:root[data-theme-version] .m-list-heading { align-items: center; display: flex; gap: 10px; min-width: 0; }
/* The title keeps its Serenity class; inside the header it stops being a block heading
   with its own bottom margin. */
:root[data-theme-version] .m-list-heading > .grid-title {
  font-size: var(--bw-h1-size);
  margin: 0;
}
:root[data-theme-version] .m-list-count {
  background-color: var(--bw-rule);
  border-radius: var(--bw-radius-sm);
  color: var(--bw-muted);
  font-family: var(--bw-font-mono);
  font-size: var(--bw-text-sm);
  padding: 3px 8px;
  white-space: nowrap;
}
:root[data-theme-version] .m-list-count:empty { display: none; }
:root[data-theme-version] .m-list-actions { display: flex; flex-wrap: wrap; gap: 8px; }
/* Toolbar buttons keep working from their new home in the header — only their container
   changed, not the buttons themselves. */
:root[data-theme-version] .m-list-actions .tool-button {
  align-items: center;
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border-strong);
  border-radius: var(--bw-radius);
  color: var(--bw-muted);
  display: inline-flex;
  font-size: var(--bw-text-base);
  font-weight: 500;
  gap: 7px;
  margin: 0;
  min-height: var(--bw-btn-h);
  padding: 0 13px;
}
:root[data-theme-version] .m-list-actions .tool-button:hover {
  border-color: var(--bw-primary);
  color: var(--bw-link);
}
:root[data-theme-version] .m-list-actions .tool-button.add-button {
  background-color: var(--bw-primary);
  border-color: var(--bw-primary);
  color: var(--bw-on-primary);
  font-weight: 600;
  padding: 0 14px;
}
:root[data-theme-version] .m-list-actions .tool-button.add-button:hover {
  background-color: var(--bw-primary-hover);
  border-color: var(--bw-primary-hover);
  color: var(--bw-on-primary);
}
:root[data-theme-version] .m-list-actions .tool-button.no-text {
  justify-content: center;
  min-width: var(--bw-btn-h);
  padding: 0;
  width: var(--bw-btn-h);
}
/* The prototype separates the four secondary actions from the primary one with a hairline
   rather than with spacing alone, so New reads as a different kind of thing. */
:root[data-theme-version] .m-list-actions-sep {
  align-self: center;
  background-color: var(--bw-border);
  flex: 0 0 1px;
  height: 22px;
  margin: 0 2px;
  width: 1px;
}
/* Emptied by the move; kept in the DOM so nothing Serenity holds a reference to breaks. */
:root[data-theme-version] .s-Toolbar.m-empty-toolbar { display: none; }

/* the tools row inside the card */
:root[data-theme-version] .m-grid-tools {
  align-items: center;
  border-bottom: 1px solid var(--bw-rule);
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 11px var(--bw-gap);
}
:root[data-theme-version] .m-grid-tools-spacer { flex: 1 1 auto; }
:root[data-theme-version] .m-grid-tools .s-QuickSearchBar { margin: 0; width: 280px; }
:root[data-theme-version] .m-grid-tools .s-QuickSearchInput { width: 100%; }

:root[data-theme-version] .m-filter-toggle {
  align-items: center;
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border-strong);
  border-radius: var(--bw-radius);
  color: var(--bw-muted);
  display: inline-flex;
  font-size: var(--bw-text-base);
  font-weight: 500;
  gap: 8px;
  /* A tools-row control, not a page action: both docs draw it the same height as the
     search box beside it (v1 32px, v2 34px), not as the New button above the card. */
  min-height: var(--bw-control-h);
  padding: 0 12px;
}
:root[data-theme-version] .m-filter-toggle:hover { border-color: var(--bw-primary); }
:root[data-theme-version] .m-filter-toggle.is-open,
:root[data-theme-version] .m-filter-toggle.has-filters {
  background-color: var(--bw-primary-soft);
  border-color: var(--bw-primary);
  color: var(--bw-text-strong);
}
:root[data-theme-version] .m-filter-count {
  background-color: var(--bw-accent-soft);
  border-radius: var(--bw-radius-sm);
  color: var(--bw-accent-ink);
  font-family: var(--bw-font-mono);
  font-size: var(--bw-text-xs);
  padding: 1px 5px;
}
:root[data-theme-version="v2"] .m-filter-count {
  background-color: var(--bw-success-line);
  color: var(--bw-text-strong);
}

/* active filter chips — the clearest radius tell between the versions after the badges */
:root[data-theme-version] .m-chips { display: flex; flex-wrap: wrap; gap: 6px; }
:root[data-theme-version] .m-chip {
  align-items: center;
  background-color: var(--bw-primary-soft);
  border: 1px solid var(--bw-border-strong);
  border-radius: var(--bw-chip);
  color: var(--bw-text-strong);
  display: inline-flex;
  font-size: var(--bw-text-sm);
  gap: 6px;
  min-height: 26px;
  padding: 0 4px 0 9px;
}
:root[data-theme-version="v2"] .m-chip { border-color: var(--bw-success-line); }
:root[data-theme-version] .m-chip-key { color: var(--bw-muted); }
:root[data-theme-version] .m-chip-val { font-weight: 500; max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
:root[data-theme-version] .m-chip-x {
  align-items: center;
  background: none;
  border: 0;
  border-radius: 50%;
  color: var(--bw-muted);
  display: inline-flex;
  font-size: 10px;
  height: 18px;
  justify-content: center;
  padding: 0;
  width: 18px;
}
:root[data-theme-version] .m-chip-x:hover { background-color: var(--bw-accent-soft); color: var(--bw-text-strong); }
:root[data-theme-version] .m-chip-clear {
  background: none;
  border: 0;
  color: var(--bw-link);
  font-size: var(--bw-text-sm);
  font-weight: 500;
  min-height: 26px;
  padding: 0 6px;
  text-decoration: underline;
  text-underline-offset: 2px;
}
:root[data-theme-version] .m-chip-clear:hover { color: var(--bw-primary-hover); }

/* density switch */
:root[data-theme-version] .m-density {
  align-items: center;
  background-color: var(--bw-field);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius);
  display: inline-flex;
  gap: 2px;
  padding: 2px;
}
:root[data-theme-version] .m-density-btn {
  background: none;
  border: 0;
  border-radius: var(--bw-radius-sm);
  color: var(--bw-muted);
  font-size: var(--bw-text-sm);
  font-weight: 500;
  min-height: 28px;
  padding: 0 10px;
}
:root[data-theme-version] .m-density-btn.is-on {
  background-color: var(--bw-surface);
  color: var(--bw-text-strong);
}
:root[data-theme-version="v1"] .m-density-btn.is-on {
  background-color: var(--bw-primary);
  color: var(--bw-on-primary);
}

/* The Features dialog's "always on" note, under a disabled Enabled checkbox.
   A control that is switched off with no reason given reads as a bug; this is the sentence
   that makes it read as a rule instead. Same muted hint treatment as the theme-mode line in
   the profile card, not an error tone — nothing has gone wrong. */
:root[data-theme-version] .m-always-on {
  color: var(--bw-faint);
  flex: 1 1 100%;
  font-size: var(--bw-text-sm);
  line-height: 1.45;
  margin-top: 4px;
}

/* Serenity's grid toggles, moved into the tools row by ListPageChrome.
   Currently just "include deleted", which corelib draws for any soft-deletable row.
   ⚠ Serenity emits <div class="s-ToggleButton"><a title="..."></a></div> — an EMPTY anchor
   whose only content is a tooltip. ListPageChrome copies the title into the anchor as real
   text, so this styles a labelled control rather than the bare 16px glyph Serenity intends.
   Shaped as a single density button so the two read as one family across the tools row. */
:root[data-theme-version] .m-grid-toggle {
  align-items: center;
  background-color: var(--bw-field);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius);
  display: inline-flex;
  padding: 2px;
  /* ⚠ common-theme.css pins .s-ToggleButton to a square ~30x30 — it is drawn as an icon
     there, with the meaning in a title attribute. Once the label is real text the anchor
     is 82px wide and simply hung outside that square. Released to content size, and the
     label held on one line so the tools row cannot grow a second one. */
  width: auto;
  height: auto;
  white-space: nowrap;
}
:root[data-theme-version] .m-grid-toggle > a {
  border-radius: var(--bw-radius-sm);
  color: var(--bw-muted);
  display: inline-flex;
  align-items: center;
  font-size: var(--bw-text-sm);
  font-weight: 500;
  /* A fixed height, not min-height: the density switch beside this is a <button>, whose
     default line-height differs from an inline-flex <a>, and the pair measured 34 vs 35.
     One pixel between two adjacent controls on the same row is visible as a misalignment. */
  height: 28px;
  padding: 0 10px;
  text-decoration: none;
  white-space: nowrap;
}

:root[data-theme-version] .m-grid-toggle > a:hover { color: var(--bw-text-strong); }
:root[data-theme-version] .m-grid-toggle.pressed > a {
  background-color: var(--bw-surface);
  color: var(--bw-text-strong);
}
:root[data-theme-version="v1"] .m-grid-toggle.pressed > a {
  background-color: var(--bw-primary);
  color: var(--bw-on-primary);
}

/* the collapsible filter panel — the doc's auto-fitting grid of labelled selects */
/* ---- v1 ONLY: the filter pill row ----
   From the interactive prototype: a FILTERS eyebrow, then one pill per filter reading
   "Type All ⌄" — outlined while unset, tinted and primary-bordered once set — each opening
   a 220px popover, with "✕ Clear all" appearing only when something is actually filtered.
   v2 keeps its own Filters toggle and panel, so every rule here is scoped to v1; the branch
   that decides which one gets built is in ListPageChrome.ts. */
:root[data-theme-version="v1"] .m-filterbar {
  align-items: center;
  border-bottom: 1px solid var(--bw-rule);
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px var(--bw-gap);
}
:root[data-theme-version="v1"] .m-filterbar-label {
  color: var(--bw-faint);
  font-size: var(--bw-text-xs);
  font-weight: 600;
  letter-spacing: var(--bw-colhead-spacing);
  text-transform: uppercase;
}
:root[data-theme-version="v1"] .m-fpill-wrap { position: relative; }
:root[data-theme-version="v1"] .m-fpill {
  align-items: center;
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border-strong);
  /* A true pill even though v1 squares off everything else: the prototype draws these at
     radius 14 on a 28px control. It is not an exception — v1's filter CHIPS are already the
     one rounded thing in that generation, which is what --bw-chip exists for. */
  border-radius: var(--bw-chip);
  color: var(--bw-text);
  cursor: pointer;
  display: inline-flex;
  font-family: var(--bw-font);
  font-size: var(--bw-text-sm);
  gap: 6px;
  min-height: 28px;
  padding: 0 10px;
}
:root[data-theme-version="v1"] .m-fpill:hover { border-color: var(--bw-primary); }
:root[data-theme-version="v1"] .m-fpill-key { color: var(--bw-faint); }
:root[data-theme-version="v1"] .m-fpill-val { font-weight: 600; }
:root[data-theme-version="v1"] .m-fpill-caret { color: var(--bw-muted); font-size: 12px; }
/* Set and open are different states: set says "this filter is doing something", open says
   "its popover is showing". Both change the border, only set tints the fill. */
:root[data-theme-version="v1"] .m-fpill.is-set {
  background-color: var(--bw-primary-soft);
  border-color: var(--bw-primary-soft-line);
}
:root[data-theme-version="v1"] .m-fpill.is-open {
  border-color: var(--bw-primary);
  box-shadow: var(--bw-focus-ring);
}
:root[data-theme-version="v1"] .m-fpill.is-open .m-fpill-caret { color: var(--bw-link); transform: rotate(180deg); }
:root[data-theme-version="v1"] .m-fpill-pop {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius-lg);
  box-shadow: var(--bw-shadow-lg);
  left: 0;
  max-height: 300px;
  min-width: 220px;
  overflow: visible;                  /* select2 renders its own drop at body level */
  padding: 10px;
  position: absolute;
  top: 34px;
  z-index: 60;
}
/* The editor came across inside its own .quick-filter-item, so the grid layout that the
   panel applies to it has to be undone — in a popover it is one field, full width. */
:root[data-theme-version="v1"] .m-fpill-pop .quick-filter-item {
  display: block;
  margin: 0;
  width: 100%;
}
/* ⚠ select2 writes an INLINE width onto its container, copied from the element it replaced
   — inside the panel that came from the grid track, and in a popover it collapsed to about
   60px, a chevron with no room for a value. An inline style beats any selector, so this is
   one of the few places !important is unavoidable rather than lazy. */
:root[data-theme-version="v1"] .m-fpill-pop .select2-container,
:root[data-theme-version="v1"] .m-fpill-pop input[class*="Editor"]:not(.select2-offscreen):not(.select2-input),
:root[data-theme-version="v1"] .m-fpill-pop select[class*="Editor"] { width: 100% !important; }
:root[data-theme-version="v1"] .m-fpill-pop .quick-filter-label {
  color: var(--bw-label-color);
  display: block;
  font-size: var(--bw-label-size);
  font-weight: var(--bw-label-weight);
  letter-spacing: var(--bw-label-spacing);
  padding-bottom: 5px;
  text-transform: none;
}
:root[data-theme-version="v1"] .m-fpill-clear {
  align-items: center;
  background: none;
  border: 0;
  color: var(--bw-link);
  cursor: pointer;
  display: inline-flex;
  font-family: var(--bw-font);
  font-size: var(--bw-text-sm);
  font-weight: 500;
  gap: 5px;
  min-height: 28px;
  padding: 0 8px;
}
:root[data-theme-version="v1"] .m-fpill-clear:hover { text-decoration: underline; }
:root[data-theme-version="v1"] .m-fpill-clear > i { font-size: 11px; }

:root[data-theme-version] .m-filter-panel {
  background-color: var(--bw-field);
  border-bottom: 1px solid var(--bw-rule);
  padding: 15px var(--bw-gap);
}
:root[data-theme-version] .m-filter-panel .quick-filters-bar {
  background: none;
  border: 0;
  display: grid;
  gap: 14px 16px;
  /* ⚠ auto-FILL with a max track, not auto-FIT with 1fr. With `auto-fit … 1fr` the
     populated tracks absorb all the leftover space, so a panel with two filters on a wide
     card gave two 785px-wide inputs — a filter field the width of half the screen. Capped
     tracks keep a filter field looking like a filter field at any count, and the row
     simply ends where the fields end. */
  grid-template-columns: repeat(auto-fill, minmax(200px, 250px));
  /* Fields are different heights (a plain input vs a select2), and the bar inherits
     `align-items: flex-end` from Serenity, which sat the shorter one on the taller one's
     baseline and left their labels on different lines. */
  align-items: start;
  margin: 0;
  padding: 0;
}
:root[data-theme-version] .m-filter-panel .quick-filter-item {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 0;
  min-width: 0;
}
/* A date/number range is two editors and a separator; one track is not enough for it. */
:root[data-theme-version] .m-filter-panel .quick-filter-item:has(.range-separator) { grid-column: span 2; }
:root[data-theme-version] .m-filter-panel .quick-filter-label {
  color: var(--bw-label-color);
  font-size: var(--bw-label-size);
  font-weight: var(--bw-label-weight);
  letter-spacing: 0;
  padding: 0;
  text-transform: none;
}
/* Whatever kind of editor the filter turns out to be, it fills its track. Matching by
   tag and by the select2 container covers every editor Serenity builds here — the
   `.editor` class does not exist on quick filters (see the form-control note above). */
:root[data-theme-version] .m-filter-panel .quick-filter-item > input:not(.select2-offscreen),
:root[data-theme-version] .m-filter-panel .quick-filter-item > select,
:root[data-theme-version] .m-filter-panel .quick-filter-item > .select2-container,
:root[data-theme-version] .m-filter-panel .quick-filter-item > .widget-wrapper { width: 100%; }

/* ---- pager ---- */
:root[data-theme-version] .slick-pg {
  background-color: var(--bw-field);
  border-top: 1px solid var(--bw-border);
  color: var(--bw-muted);
  font-size: var(--bw-text-sm);
  margin: 0;
  padding: 9px var(--bw-gap);
}
:root[data-theme-version] .slick-pg-in { gap: 6px; }
:root[data-theme-version] .slick-pg-btn {
  align-items: center;
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius-sm);
  color: var(--bw-muted);
  display: inline-flex;
  height: var(--bw-control-h-sm);
  justify-content: center;
  min-width: var(--bw-control-h-sm);
  width: auto;
}
:root[data-theme-version] .slick-pg-btn:hover { border-color: var(--bw-primary); color: var(--bw-link); }
:root[data-theme-version] .slick-pg-btn > span:before { color: inherit; font-size: 13px; }
:root[data-theme-version] .slick-pg-reload { color: var(--bw-muted); }
:root[data-theme-version] .slick-pg-current,
:root[data-theme-version] .slick-pg-size {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius-sm);
  color: var(--bw-text-strong);
  font-family: var(--bw-font-mono);
  font-weight: 600;
  min-height: var(--bw-control-h-sm);
  padding: 0 6px;
}
:root[data-theme-version] .slick-pg-grp { align-items: center; display: flex; gap: 6px; }
/* Both docs read "Showing 1–N of N records" on the LEFT, with the rows-per-page select
   and the page controls grouped on the right. Serenity emits six groups in the opposite
   arrangement. They carry semantic classes (-stat, -size, -firstprev, -control,
   -nextlast, -reload), so the order is set by name rather than by counting children —
   which would silently rearrange itself the day a group is added or hidden. */
:root[data-theme-version] .slick-pg-in { gap: 6px; width: 100%; }
:root[data-theme-version] .slick-pg-grp-stat {
  margin: 0 auto 0 0;               /* the auto margin is what pushes the rest right */
  order: 1;
}
:root[data-theme-version] .slick-pg-grp-size     { margin: 0; order: 2; }
:root[data-theme-version] .slick-pg-grp-firstprev{ margin: 0; order: 3; }
:root[data-theme-version] .slick-pg-grp-control  { margin: 0; order: 4; }
:root[data-theme-version] .slick-pg-grp-nextlast { margin: 0; order: 5; }
:root[data-theme-version] .slick-pg-grp-reload   { margin: 0; order: 6; }

/* ---- pager colour, per the prototype ----
   It draws the page controls as mono figures with the CURRENT page filled --primary in
   white, and anything you cannot act on in the faint tone. Serenity's pager is not a strip
   of numbered buttons — it is first/prev, an editable page box, next/last — so the
   treatment maps onto that box: it is the element that says which page you are on, and it
   stays typeable.

   ⚠ Must come AFTER the shared `.slick-pg-current, .slick-pg-size` rule above. Same
   selector weight, so source order decides, and placed before it the fill was silently
   overwritten by that rule's --bw-surface background. */
:root[data-theme-version] .slick-pg-current {
  background-color: var(--bw-primary);
  border-color: var(--bw-primary);
  color: var(--bw-on-primary);
  text-align: center;
}
:root[data-theme-version] .slick-pg-current:focus {
  border-color: var(--bw-primary-active);
  box-shadow: var(--bw-focus-ring);
  outline: 0;
}
/* ⚠ `.slick-pg-disabled` is what SleekGrid puts on a control that cannot act — on page 1
   of 1 that is all four. Without this they read as live buttons that do nothing. */
:root[data-theme-version] .slick-pg-btn.slick-pg-disabled,
:root[data-theme-version] .slick-pg-disabled .slick-pg-btn,
:root[data-theme-version] .slick-pg-btn.slick-pg-disabled:hover,
:root[data-theme-version] .slick-pg-disabled .slick-pg-btn:hover {
  border-color: var(--bw-rule);
  color: var(--bw-disabled);
  cursor: default;
}
/* The figures around the box are labels, not controls: the total in mono like the rest of
   the app's numbers, the words in the faint tone. */
:root[data-theme-version] .slick-pg-total {
  color: var(--bw-text-strong);
  font-family: var(--bw-font-mono);
  font-weight: 600;
}
:root[data-theme-version] .slick-pg-pagetext,
:root[data-theme-version] .slick-pg-pagesep { color: var(--bw-faint); }

/* ---- dialogs (shared chrome; the shell section positions them) ---- */
/* ⚠ Serenity 10.3.6 opens dialogs WITHOUT a Bootstrap backdrop — no .modal-backdrop
   element is ever created — so styling that class alone left both designs without the
   scrim they specify, and the page behind a dialog stayed at full contrast. The
   .modal element is itself a fixed, full-viewport container, so painting it is the
   scrim; no extra element and no JS needed. */
:root[data-theme-version] .modal {
  background-color: var(--bw-overlay);
}
:root[data-theme-version] .modal-backdrop,
:root[data-theme-version] .ui-widget-overlay {
  background-color: var(--bw-overlay);
  opacity: 1;
}
@media (prefers-reduced-motion: no-preference) {
  :root[data-theme-version] .modal.show { animation: m-scrim-in .14s ease-out; }
}
@keyframes m-scrim-in { from { background-color: transparent; } }
/* ⚠ This animation was once removed as "stuck", on a bad diagnosis. Inspected through
   the browser-automation tab it reported `playState: "running"` with `currentTime: 0`
   forever — but that is what EVERY animation and transition reports in a tab whose
   `document.visibilityState` is "hidden": the document timeline is frozen, so
   getComputedStyle returns the animation's start value indefinitely. Nothing was wrong
   with the scrim. When checking an animated or transitioned property while driving the
   browser, take a screenshot — the compositor still paints — or read the specified value
   from CSSOM. Do not trust getComputedStyle for those properties. */
:root[data-theme-version] .modal-content,
:root[data-theme-version] .ui-dialog {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius-lg);
  box-shadow: var(--bw-shadow-pop);
  overflow: hidden;
}
:root[data-theme-version] .modal-header,
:root[data-theme-version] .ui-dialog-titlebar {
  align-items: center;
  background-color: var(--bw-dialog-head-bg);
  border: 0;
  border-radius: 0;
  color: var(--bw-dialog-head-fg);
  gap: 10px;
  min-height: var(--bw-dialog-head-h);
  padding: 0 var(--bw-pad-lg);
}
:root[data-theme-version] .modal-title,
:root[data-theme-version] .ui-dialog-title {
  color: inherit;
  font-size: var(--bw-h2-size);
  font-weight: 600;
  letter-spacing: -.01em;
  margin: 0;
}
/* ⚠ Bootstrap's .btn-close is a background-image SVG with a HARDCODED BLACK fill; it is
   only ever visible on a light header, and `.btn-close-white` exists solely to invert it.
   v1's dialog header is navy and v2's is white, so no single filter is right — and setting
   `filter: none` (as this file first did) leaves a black cross on navy, effectively
   invisible.

   Drawing the mark as a glyph in `currentColor` instead removes the problem rather than
   patching it: the button inherits `--bw-dialog-head-fg` from the header, so it is white on
   v1's navy and dark on v2's white, with no per-version rule and no filter at all. */
:root[data-theme-version] .modal-header .btn-close,
:root[data-theme-version] .ui-dialog-titlebar-close,
:root[data-theme-version] .ui-dialog-titlebar .ui-button {
  align-items: center;
  background-color: transparent;
  background-image: none;
  border: 0;
  border-radius: var(--bw-radius-sm);
  color: inherit;
  display: flex;
  filter: none;
  height: 30px;
  justify-content: center;
  opacity: .8;
  padding: 0;
  width: 30px;
}
:root[data-theme-version] .modal-header .btn-close::before,
:root[data-theme-version] .ui-dialog-titlebar-close::before {
  content: "\f00d";
  font-family: var(--s-icon-font);
  font-size: 15px;
  font-weight: var(--s-icon-solid);
  line-height: 1;
}
/* jQuery-UI hides its own label in a sprite span that would otherwise sit beside the
   glyph. */
:root[data-theme-version] .ui-dialog-titlebar-close .ui-icon,
:root[data-theme-version] .ui-dialog-titlebar-close .ui-button-text { display: none; }
:root[data-theme-version] .modal-header .btn-close:hover,
:root[data-theme-version] .ui-dialog-titlebar-close:hover {
  background-color: var(--bw-chrome-hover);
  opacity: 1;
}
/* v2's header is a light surface, so the dark-on-dark hover tint would not read there. */
:root[data-theme-version="v2"] .modal-header .btn-close:hover,
:root[data-theme-version="v2"] .ui-dialog-titlebar-close:hover { background-color: var(--bw-hover-bg); }
:root[data-theme-version] .modal-body,
:root[data-theme-version] .ui-dialog-content { padding: var(--bw-pad-lg); }
:root[data-theme-version] .modal-footer,
:root[data-theme-version] .ui-dialog-buttonpane {
  background-color: var(--bw-dialog-foot-bg);
  border-top: 1px solid var(--bw-border);
  gap: 8px;
  margin: 0;
  padding: 12px var(--bw-pad-lg);
}
:root[data-theme-version] .ui-dialog-buttonpane button {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border-strong);
  border-radius: var(--bw-radius);
  color: var(--bw-muted);
  font-size: var(--bw-text-base);
  font-weight: 500;
  margin: 0 0 0 8px;
  min-height: var(--bw-btn-h);
  padding: 0 14px;
}
:root[data-theme-version] .ui-dialog-buttonpane button:hover {
  background-color: var(--bw-hover-bg);
  color: var(--bw-text-strong);
}
/* The save/OK button is the first in Serenity's button set. */
:root[data-theme-version] .ui-dialog-buttonpane .ui-dialog-buttonset button:first-child,
:root[data-theme-version] .modal-footer .btn-primary {
  background-color: var(--bw-primary);
  border-color: var(--bw-primary);
  color: var(--bw-on-primary);
  font-weight: 600;
}
:root[data-theme-version] .ui-dialog-buttonpane .ui-dialog-buttonset button:first-child:hover {
  background-color: var(--bw-primary-hover);
  color: var(--bw-on-primary);
}
:root[data-theme-version] .ui-dialog-buttonpane .delete-button,
:root[data-theme-version] .modal-footer .delete-button { color: var(--bw-danger); }

/* ---- dialog forms: stacked labels in a column grid ----
   Serenity lays a field out horizontally — a fixed 150px caption column, right
   aligned, editor beside it — and stacks fields one per row. Neither handoff draws
   that: v1's create dialog is a 3-across grid of stacked label-over-input pairs
   under a section heading, v2's drawer the same thing 2-across. Re-flowing the
   generated markup here keeps every *Form.cs untouched and applies to every dialog
   in the app at once, including ones added later.

   Scoped to dialogs and panels deliberately. Quick filters and the filter panel are
   also .field-based but are horizontal by design in both docs, so they keep
   Serenity's flow. */
/* ⚠ The editor body is .modal-body in modal mode and .s-DialogContent in the
   jQuery-UI/panel modes. Scoping to only one silently misses the other — this file
   originally named just .s-DialogContent and nothing applied, because a Bootstrap
   modal has no such element anywhere in the chain. */
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .category {
  display: grid;
  gap: 14px 18px;
  grid-template-columns: repeat(var(--bw-form-cols), minmax(0, 1fr));
}
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .category-title { grid-column: 1 / -1; }
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field {
  flex-wrap: wrap;
  padding: 0;
}
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field .caption {
  display: flex;
  flex: 0 0 100%;
  line-height: 1.4;
  margin: 0 0 5px;
  text-align: left;
  width: auto;
}
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field .caption sup { order: 2; padding-left: 3px; }
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field .editor,
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field .widget-wrapper,
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field .select2-container {
  flex: 1 1 100%;
  width: 100%;
}
/* A checkbox is its own control, so it must not stretch to the column width — and it has
   to line up with the text inputs beside it. Serenity sizes it `1.5em` with a `0.4em` top
   margin, which left it 5px above its neighbours' boxes in a two-column row. Sizing it
   explicitly and centring it inside a control-height slot puts its middle on the same line
   as the inputs it sits beside, at either version's control height. */
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field input[type="checkbox"].editor {
  accent-color: var(--bw-primary);
  flex: 0 0 auto;
  height: 16px;
  margin: calc((var(--bw-control-h) - 16px) / 2) 0;
  width: 16px;
}
/* Editors that need room take the whole row rather than being squeezed into a
   third of it. Explicit col-* classes mean the form author already chose a width,
   so those are honoured too. */
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field:has(.s-ImageUploadEditor),
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field:has(.s-MultipleImageUploadEditor),
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field:has(textarea),
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field.col-12,
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field.col-lg-12 { grid-column: 1 / -1; }
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field.col-6,
:root[data-theme-version] :is(.modal-body, .s-DialogContent) .s-PropertyGrid .field.col-lg-6 { grid-column: span 2; }

/* ---- the form-level validation banner ----
   The prototype's `formHasErrors` block: an icon, then "N fields need attention." over the
   names, on a danger-tinted fill (its literals #FDECEA / #F2BDB6 / #8E241A are exactly
   --bw-danger-bg / --bw-danger-line / --bw-danger-ink, so no new tokens were needed).

   It replaces the toast AND the tooltip Serenity raised for the same thing — see
   Modules/Common/UI/FormValidation.ts. It sits at the top of the form rather than in the
   dialog header so it works on the sign-in screen too, which has no dialog chrome. */
:root[data-theme-version] .m-form-errors {
  align-items: flex-start;
  background-color: var(--bw-danger-bg);
  border: 1px solid var(--bw-danger-line);
  border-radius: var(--bw-radius);
  color: var(--bw-danger-ink);
  display: flex;
  font-size: var(--bw-text-base);
  gap: 10px;
  line-height: 1.45;
  margin-bottom: var(--bw-gap);
  padding: 9px 12px;
}
:root[data-theme-version] .m-form-errors > i {
  color: var(--bw-danger);
  font-size: 15px;
  line-height: 1.35;
}
:root[data-theme-version] .m-form-errors-text > strong { font-weight: 600; }

/* ---- Serenity's ERROR page ----
   Reached by every unhandled error, and notably by a **stale password-reset link** —
   /Account/ResetPassword without a valid token renders "Your token to reset your password is
   invalid or has expired!" here rather than showing the form. Found while checking that page
   on 2026-08-18.

   ⚠ The view ships its own INLINE `<style>` inside `section.content`, which is why this looked
   stock while the rest of the app was themed: `div.message { background: #FFDFDF url(
   /Content/serenity/images/dialog-alert.png) ...; border: 2px solid #ECC2C2 }` with
   `h1 { color: red }` and `h3 { color: maroon }`. The text colours were already being won by
   this file; the pale-pink slab and the PNG were not, and in DARK mode a `#FFDFDF` panel is
   badly out of place.

   No `!important` needed: an inline `<style>` element carries no extra weight, so
   `:root[data-theme-version] .page-content > .message` at (0,3,0) simply outranks
   `div.message` at (0,1,1). Reuses the --bw-danger-* trio the validation banner already uses,
   so the two danger surfaces agree and dark mode is covered for free. */
:root[data-theme-version] .page-content > .message {
  background-color: var(--bw-danger-bg);
  background-image: none;               /* drops the stock dialog-alert.png */
  border: 1px solid var(--bw-danger-line);
  border-radius: var(--bw-radius-lg);
  box-shadow: var(--bw-shadow);
  color: var(--bw-danger-ink);
  margin: 48px auto;
  max-width: 640px;
  min-height: 0;                        /* stock reserves 170px for the removed icon */
  min-width: 0;
  padding: var(--bw-pad-lg);
}
/* 30px shouting "ERROR" is the stock treatment; the message matters more than the label. */
:root[data-theme-version] .page-content > .message h1 {
  color: var(--bw-text-strong);
  font-size: 20px;
  font-weight: 600;
  margin: 0;
}
:root[data-theme-version] .page-content > .message h3 {
  color: var(--bw-text);
  font-size: var(--bw-text-base);
  font-weight: 500;
  line-height: 1.5;
  margin: 8px 0 0;
}
:root[data-theme-version] .page-content > .message a.redirect { color: var(--bw-link); }
:root[data-theme-version] .page-content > .message .date-time {
  color: var(--bw-faint);
  font-size: var(--bw-text-sm);
  font-style: normal;
  font-weight: 400;
}

/* ---- dialog action bar ----
   Serenity puts Save / Apply / Delete in a toolbar at the TOP of the dialog body and
   leaves .modal-footer empty. Both designs put those actions in a footer bar along
   the bottom edge, right aligned, primary last. Flex order moves the existing
   toolbar there without touching how it is built, so every button, permission check
   and keyboard shortcut still works. */
/* ⚠ THE DIALOG BODY must be the scroll container, not Serenity's inner `.categories`.
   Serenity sets `.categories { overflow-y: auto }` and makes the form chain flex-grow,
   so a tall form scrolled *inside* the property grid: the scrollbar appeared inset in the
   middle of the dialog beside the fields rather than at the dialog's edge, and the form
   clipped part-way through a row while the padding below it stayed put. Releasing the
   inner containers to their natural height moves the scroll out to .modal-body, where the
   bar sits on the dialog edge and the whole form moves as one. */
:root[data-theme-version] .modal-content .modal-body { display: flex; flex-direction: column; overflow-y: auto; }
:root[data-theme-version] .modal-content .modal-body > .s-Form { order: 1; }
:root[data-theme-version] .modal-body .s-Form,
:root[data-theme-version] .modal-body form,
:root[data-theme-version] .modal-body .s-PropertyGrid,
:root[data-theme-version] .modal-body .s-PropertyGrid .categories {
  flex: 0 0 auto;
  overflow: visible;
}
/* Pinned to the bottom of the scrollport so the actions stay reachable however long the
   form is — the fixed footer both handoffs draw. The negative offset cancels the body's
   own padding so the bar still meets the dialog edges. */
:root[data-theme-version] .modal-content .modal-body > div.s-Toolbar {
  background-color: var(--bw-dialog-foot-bg);
  border-top: 1px solid var(--bw-border);
  bottom: calc(-1 * var(--bw-pad-lg));
  justify-content: flex-end;
  margin: var(--bw-pad-lg) calc(-1 * var(--bw-pad-lg)) calc(-1 * var(--bw-pad-lg));
  order: 2;
  padding: 12px var(--bw-pad-lg);
  position: sticky;
  z-index: 2;
}
/* Serenity emits .modal-footer whether or not anything goes in it; an empty bar
   below the action bar reads as a rendering fault. */
:root[data-theme-version] .modal-footer:empty { display: none; }
/* Save is the primary action in that bar, and the only one that gets the fill. */
:root[data-theme-version] .modal-content .modal-body > div.s-Toolbar .tool-button.save-and-close-button,
:root[data-theme-version] .modal-content .modal-body > div.s-Toolbar .tool-button.apply-changes-button {
  background-color: var(--bw-primary);
  border-color: var(--bw-primary);
  color: var(--bw-on-primary);
  font-weight: 600;
}
:root[data-theme-version] .modal-content .modal-body > div.s-Toolbar .tool-button.save-and-close-button:hover,
:root[data-theme-version] .modal-content .modal-body > div.s-Toolbar .tool-button.apply-changes-button:hover {
  background-color: var(--bw-primary-hover);
  border-color: var(--bw-primary-hover);
  color: var(--bw-on-primary);
}
/* Note: pushing Delete to the far left of the action bar with `margin-right: auto`
   does NOT work — Serenity wraps toolbar buttons in .tool-group, so the auto margin
   is absorbed inside the group instead of spanning the bar. Left alone rather than
   worked around; both docs group the actions at the right anyway. */

/* ---- tabs (dialog tabs and page tabs) ----
   Both designs use an underlined tab strip on a tinted bar, never Bootstrap's
   raised folder tabs. */
:root[data-theme-version] .nav-tabs,
:root[data-theme-version] .ui-tabs-nav {
  background-color: var(--bw-field);
  border-bottom: 1px solid var(--bw-border);
  gap: 2px;
  margin: 0 calc(-1 * var(--bw-pad-lg)) var(--bw-pad-lg);
  padding: 0 var(--bw-pad-lg);
}
:root[data-theme-version] .nav-tabs .nav-link,
:root[data-theme-version] .ui-tabs-nav a {
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  color: var(--bw-muted);
  font-size: var(--bw-text-base);
  font-weight: 400;
  padding: 11px 14px;
}
:root[data-theme-version] .nav-tabs .nav-link:hover,
:root[data-theme-version] .ui-tabs-nav a:hover { color: var(--bw-text-strong); }
:root[data-theme-version] .nav-tabs .nav-link.active,
:root[data-theme-version] .ui-tabs-nav .ui-tabs-active a {
  background: none;
  border-bottom-color: var(--bw-primary);
  color: var(--bw-text-strong);
  font-weight: 600;
}
:root[data-theme-version] .ui-tabs-nav .ui-tabs-active,
:root[data-theme-version] .ui-tabs-nav .ui-tabs-selected { border-bottom: 0; }
:root[data-theme-version] .ui-tabs-panel { padding-top: 0; }

/* ---- toasts ----
   The prototype (eFMS Prototype.dc.html, TOAST section) draws a 380px card pinned below
   the top bar at the right: tone-tinted border, a 3px tone edge on the left, a 17px tone
   icon, a 13/600 title over a 12px muted line, and a 0.18s slide-and-fade in.

   ⚠ Serenity's toastr2 defaults its container to `toast-top-full-width`, which stretches
   the card across the whole viewport and drops it over the top bar. The container is
   created by script with that class baked in, so the position is corrected here rather
   than by passing options at every call site — every notifySuccess/Error/Warning/Info in
   the app then lands in the designed place, including the ones Serenity raises itself. */
:root[data-theme-version] #toast-container {
  left: auto;
  right: 20px;
  top: calc(var(--bw-topbar-h) + 8px);
  width: var(--bw-toast-w);
}
:root[data-theme-version] #toast-container > div { margin: 0 0 10px; width: 100%; }
:root[data-theme-version] .toast {
  background-color: var(--bw-surface);
  /* The tone tints the border too, not just the left edge — a neutral border beside a
     coloured edge reads as two unrelated decisions. Defaults to the info tone. */
  border: 1px solid var(--bw-info-line);
  border-left: 3px solid var(--bw-info);
  border-radius: var(--bw-radius);
  box-shadow: var(--bw-shadow-lg);
  color: var(--bw-text);
  /* ⚠ GRID, not flex. toastr emits the close button, the title and the message as three
     SIBLINGS with no wrapper, so `display: flex` laid the title and the message out side
     by side instead of stacking them. A two-column grid puts the glyph in column 1 and
     everything textual in column 2, in DOM order, with no extra markup. */
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  column-gap: 10px;
  row-gap: 4px;
  opacity: 1;
  padding: 12px 14px;
  position: relative;
  width: 100%;
}
@media (prefers-reduced-motion: no-preference) {
  :root[data-theme-version] .toast { animation: m-toast-in .18s ease-out; }
}
@keyframes m-toast-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* The tone icon. toastr renders no icon element — `iconClass` only sets the tone class on
   the card — so it is drawn as a ::before glyph in the tone colour, which also means it
   cannot get out of step with the tone. */
:root[data-theme-version] .toast::before {
  color: var(--bw-info);
  content: "\f05a";                     /* circle-info */
  font-family: var(--s-icon-font);
  font-size: 17px;
  font-weight: var(--s-icon-solid);
  grid-column: 1;
  grid-row: 1;
  line-height: 1.25;
}
:root[data-theme-version] .toast-title {
  color: var(--bw-text-strong);
  font-size: var(--bw-text-base);
  font-weight: 600;
  grid-column: 2;
}
:root[data-theme-version] .toast-message {
  color: var(--bw-muted);
  font-size: var(--bw-text-sm);
  grid-column: 2;
  line-height: 1.5;
}
/* Taken out of the grid flow entirely — in DOM order it is the FIRST child, so left in
   flow it sat between the glyph and the title. The card's right padding keeps a long
   title clear of it. */
:root[data-theme-version] .toast:has(.toast-close-button) { padding-right: 32px; }
:root[data-theme-version] .toast-close-button {
  color: var(--bw-faint);
  font-size: 17px;
  font-weight: 400;
  line-height: 1;
  opacity: 1;
  position: absolute;
  right: 11px;
  text-shadow: none;
  top: 9px;
}
:root[data-theme-version] .toast-close-button:hover { color: var(--bw-text-strong); }
/* Per tone: border, left edge and glyph move together. The title stays --bw-text-strong —
   the prototype colours the mark, not the words, so a long title stays readable. */
:root[data-theme-version] .toast-success {
  border-color: var(--bw-success-line);
  border-left-color: var(--bw-success);
}
:root[data-theme-version] .toast-success::before { color: var(--bw-success); content: "\f058"; }
:root[data-theme-version] .toast-error {
  border-color: var(--bw-danger-line);
  border-left-color: var(--bw-danger);
}
:root[data-theme-version] .toast-error::before { color: var(--bw-danger); content: "\f06a"; }
:root[data-theme-version] .toast-warning {
  border-color: var(--bw-warning-line);
  border-left-color: var(--bw-warning);
}
:root[data-theme-version] .toast-warning::before { color: var(--bw-warning); content: "\f071"; }
:root[data-theme-version] .toast-info {
  border-color: var(--bw-info-line);
  border-left-color: var(--bw-info);
}
:root[data-theme-version] .toast-progress { background-color: var(--bw-primary); opacity: .3; }

/* ---- alerts ---- */
:root[data-theme-version] .alert {
  border: 1px solid var(--bw-border);
  border-top-width: 1px;
  border-radius: var(--bw-radius);
  font-size: var(--bw-text-base);
  padding: 10px 13px;
}
:root[data-theme-version] .alert-danger  { background-color: var(--bw-danger-bg);  border-color: var(--bw-danger-line);  color: var(--bw-danger-ink); }
:root[data-theme-version] .alert-warning { background-color: var(--bw-warning-bg); border-color: var(--bw-warning-line); color: var(--bw-warning); }
:root[data-theme-version] .alert-success { background-color: var(--bw-success-bg); border-color: var(--bw-success-line); color: var(--bw-success); }
:root[data-theme-version] .alert-info    { background-color: var(--bw-info-bg);    border-color: var(--bw-info-line);    color: var(--bw-info); }

/* ---- dropdown menus ---- */
:root[data-theme-version] .dropdown-menu {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius-lg);
  box-shadow: var(--bw-shadow-lg);
  font-size: var(--bw-text-base);
  padding: 6px;
}
:root[data-theme-version] .dropdown-item {
  border-radius: var(--bw-radius-sm);
  color: var(--bw-text);
  padding: 8px 10px;
}
:root[data-theme-version] .dropdown-item:hover,
:root[data-theme-version] .dropdown-item:focus {
  background-color: var(--bw-hover-bg);
  color: var(--bw-text-strong);
}
:root[data-theme-version] .dropdown-item.active { background-color: var(--bw-primary); color: var(--bw-on-primary); }
:root[data-theme-version] .dropdown-divider { border-color: var(--bw-rule); }

/* ---- badges / chips ---- */
:root[data-theme-version] .badge {
  border-radius: var(--bw-pill);
  font-size: var(--bw-text-xs);
  font-weight: 600;
  letter-spacing: 0;
  padding: 3px 8px;
}
:root[data-theme-version] .badge.bg-primary { background-color: var(--bw-primary-soft) !important; color: var(--bw-link); }
:root[data-theme-version] .badge.bg-success { background-color: var(--bw-success-bg) !important; color: var(--bw-success); }
:root[data-theme-version] .badge.bg-danger  { background-color: var(--bw-danger-bg)  !important; color: var(--bw-danger-ink); }
:root[data-theme-version] .badge.bg-warning { background-color: var(--bw-warning-bg) !important; color: var(--bw-warning); }
:root[data-theme-version] .badge.bg-info    { background-color: var(--bw-info-bg)    !important; color: var(--bw-info); }
:root[data-theme-version] .badge.bg-secondary { background-color: var(--bw-field) !important; color: var(--bw-muted); }

/* ---- icons inside buttons ----
   ⚠ Serenity hardcodes stock colour utilities onto tool-button icons — the New button
   ships as `<i class="fa fa-plus-circle text-green">` and the dialog's Save icon carries
   `text-purple`. That renders a Bootstrap-green plus on a navy primary button and a
   purple disc on Save: two colours that exist in neither design generation, sitting on a
   fill they were never chosen against.

   An icon inside a button is part of the button, so it takes the button's own colour —
   white on a primary fill, muted on an outline, danger on a delete. The utilities are
   declared `!important`, so overriding them requires the same. */
:root[data-theme-version] .tool-button .button-inner > i,
:root[data-theme-version] .tool-button .button-inner > em,
:root[data-theme-version] .tool-button .button-inner > span.fa,
:root[data-theme-version] .btn > i,
:root[data-theme-version] .btn > em,
:root[data-theme-version] .m-list-actions .tool-button i,
:root[data-theme-version] .dropdown-item > .icon { color: inherit !important; }

/* ---- stock colour utilities, re-pointed ----
   The same AdminLTE-era palette (#00a65a green, #605ca8 purple, #f39c12 orange…) reaches
   anything else that carries these classes. Rather than leave those literals showing
   wherever a column formatter or a text block uses one, each is mapped onto the nearest
   token so it lands inside whichever generation is active. */
:root[data-theme-version] .text-green,
:root[data-theme-version] .text-lime,
:root[data-theme-version] .text-olive,
:root[data-theme-version] .text-teal      { color: var(--bw-success) !important; }
:root[data-theme-version] .text-red,
:root[data-theme-version] .text-maroon    { color: var(--bw-danger) !important; }
:root[data-theme-version] .text-yellow,
:root[data-theme-version] .text-orange    { color: var(--bw-warning) !important; }
:root[data-theme-version] .text-blue,
:root[data-theme-version] .text-light-blue,
:root[data-theme-version] .text-aqua,
:root[data-theme-version] .text-navy,
:root[data-theme-version] .text-purple,
:root[data-theme-version] .text-fuchsia   { color: var(--bw-link) !important; }
:root[data-theme-version] .text-gray      { color: var(--bw-faint) !important; }

/* ---- semantic text ---- */
:root[data-theme-version] .text-success { color: var(--bw-success) !important; }
:root[data-theme-version] .text-danger  { color: var(--bw-danger)  !important; }
:root[data-theme-version] .text-warning { color: var(--bw-warning) !important; }
:root[data-theme-version] .text-primary { color: var(--bw-link) !important; }
:root[data-theme-version] .text-muted   { color: var(--bw-faint)   !important; }
:root[data-theme-version] .inactive,
:root[data-theme-version] .deleted { color: var(--bw-faint); }

/* ---- permission / checkbox trees (role and user permission dialogs) ---- */
:root[data-theme-version] .s-PropertyGrid .check-box,
:root[data-theme-version] .grant,
:root[data-theme-version] .revoke { accent-color: var(--bw-primary); }
:root[data-theme-version] .effective-permission { color: var(--bw-success); }
:root[data-theme-version] .deny { color: var(--bw-danger); }

/* ---- progress ---- */
:root[data-theme-version] .progress {
  background-color: var(--bw-rule);
  border-radius: var(--bw-pill);
  height: 6px;
}
:root[data-theme-version] .progress-bar { background-color: var(--bw-primary); border-radius: var(--bw-pill); }

/* ---- empty and loading states ----
   The docs specify both; without them a filtered-to-nothing grid falls back to a
   bare white rectangle, which is exactly the "stock template" look this theme
   exists to remove. */
:root[data-theme-version] .m-empty {
  align-items: center;
  color: var(--bw-muted);
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 48px 24px;
  text-align: center;
}
:root[data-theme-version] .m-empty-title { color: var(--bw-text-strong); font-size: 16px; font-weight: 600; }
:root[data-theme-version] .m-empty-body { font-size: var(--bw-text-base); line-height: 1.5; max-width: 380px; }
/* ---- 07 STATES: the empty grid and the loading skeleton ----
   Built by Modules/Common/UI/GridStates.ts, which appends both overlays to SleekGrid's
   own container so they cover the table (column heads included, as the handoff draws it)
   without covering the tools row above — the empty state offers "Clear filters", so it
   must not sit on top of the filters. The host therefore has to establish a containing
   block; SleekGrid does not set one itself. */
:root[data-theme-version] .m-state-host { position: relative; }

:root[data-theme-version] .m-empty-state,
:root[data-theme-version] .m-skeleton {
  background-color: var(--bw-surface);
  inset: 0;
  position: absolute;
  z-index: 3;
}
:root[data-theme-version] .m-empty-state[hidden],
:root[data-theme-version] .m-skeleton[hidden] { display: none; }

:root[data-theme-version] .m-empty-state {
  align-items: center;
  display: flex;
  flex-direction: column;
  gap: 10px;
  justify-content: center;
  padding: 40px 24px;
  text-align: center;
}
/* A quiet mark rather than an illustration — the handoff draws a small outlined square,
   and anything heavier competes with the primary action below it. */
:root[data-theme-version] .m-empty-mark {
  border: 2px solid var(--bw-border);
  border-radius: var(--bw-radius-lg);
  height: 40px;
  margin-bottom: 4px;
  width: 40px;
}
:root[data-theme-version] .m-empty-actions { display: flex; gap: 8px; margin-top: 6px; }
:root[data-theme-version] .m-empty-clear,
:root[data-theme-version] .m-empty-create {
  border-radius: var(--bw-radius-sm);
  cursor: pointer;
  font-family: var(--bw-font);
  font-size: var(--bw-text-base);
  font-weight: 600;
  /* These are buttons, and the empty state draws them at button height (v2's doc: 36px) —
     not at the list-control height, which is 2px shorter. */
  min-height: var(--bw-btn-h);
  padding: 0 16px;
}
:root[data-theme-version] .m-empty-clear {
  background-color: transparent;
  border: 1px solid var(--bw-border-strong);
  color: var(--bw-text);
}
:root[data-theme-version] .m-empty-clear:hover { background-color: var(--bw-hover-bg); }
:root[data-theme-version] .m-empty-create {
  background-color: var(--bw-primary);
  border: 1px solid var(--bw-primary);
  color: var(--bw-on-primary);
}
:root[data-theme-version] .m-empty-create:hover { background-color: var(--bw-primary-hover); }
:root[data-theme-version] .m-empty-clear[hidden],
:root[data-theme-version] .m-empty-create[hidden] { display: none; }

/* Skeleton rows keep the table's shape while the first request is in flight, so the page
   does not collapse to nothing and reflow when data lands. */
:root[data-theme-version] .m-skeleton { padding: 8px 0; }
:root[data-theme-version] .m-skeleton-row {
  align-items: center;
  display: flex;
  gap: 3%;
  height: var(--bw-row-h, 36px);
  padding: 0 14px;
}
:root[data-theme-version] .m-skeleton-cell {
  background-image: linear-gradient(90deg, var(--bw-skeleton-a) 25%, var(--bw-skeleton-b) 37%, var(--bw-skeleton-a) 63%);
  background-size: 400% 100%;
  border-radius: var(--bw-radius-sm);
  height: 9px;
  animation: m-shimmer 1.4s ease-in-out infinite;
}
@keyframes m-shimmer {
  from { background-position: 100% 50%; }
  to   { background-position: 0 50%; }
}
/* Respect a reduced-motion preference — a shimmer is decorative, the bars alone still
   communicate "loading". */
@media (prefers-reduced-motion: reduce) {
  :root[data-theme-version] .m-skeleton-cell { animation: none; }
}

/* ---- 03: the bulk-action bar ----
   ⚠ THE TWO GENERATIONS DRAW THIS DIFFERENTLY, and not as a palette swap:

     v1  a DARK NAVY STRIP (#0A1B3D, 40px) sitting directly on top of the table with its
         corners squared off against it, carrying quiet icon+label items in pale blue.
     v2  a LIGHT TINTED BAR (#F2F8F5) with a bottom rule, carrying OUTLINED BUTTONS on
         white, and a borderless text "Clear".

   Both are built from the same markup in Modules/Common/UI/BulkActionBar.ts; only the
   chrome is per-version. The shared rules come first, then each generation overrides. */
:root[data-theme-version] .m-bulkbar {
  align-items: center;
  display: flex;
  gap: 10px;
}
:root[data-theme-version] .m-bulkbar[hidden] { display: none; }
:root[data-theme-version] .m-bulkbar-actions { display: flex; gap: 7px; }
:root[data-theme-version] .m-bulkbar-btn,
:root[data-theme-version] .m-bulkbar-clear {
  cursor: pointer;
  font-family: var(--bw-font);
}
/* An escape, not a peer of the actions — always at the far end. */
:root[data-theme-version] .m-bulkbar-clear { margin-left: auto; }

/* --- v1: dark strip fused to the top of the table --- */
:root[data-theme-version="v1"] .m-bulkbar {
  background-color: var(--bw-chrome);
  border-radius: var(--bw-radius) var(--bw-radius) 0 0;
  margin: 0 var(--bw-pad) 0;
  min-height: 40px;
  padding: 0 12px;
}
:root[data-theme-version="v1"] .m-bulkbar-count {
  color: var(--bw-on-primary);
  font-size: var(--bw-text-base);
  font-weight: 600;
}
/* Items read as toolbar entries, not buttons: no border, no fill until hover. */
:root[data-theme-version="v1"] .m-bulkbar-btn {
  background-color: transparent;
  border: 0;
  border-radius: var(--bw-radius-sm);
  color: var(--bw-chrome-muted);
  font-size: var(--bw-text-sm);
  font-weight: 500;
  padding: 4px 8px;
}
:root[data-theme-version="v1"] .m-bulkbar-btn:hover {
  background-color: var(--bw-chrome-hover);
  color: var(--bw-on-primary);
}
:root[data-theme-version="v1"] .m-bulkbar-btn.is-danger { color: var(--bw-danger-on-chrome); }
:root[data-theme-version="v1"] .m-bulkbar-btn.is-danger:hover {
  background-color: var(--bw-danger);
  color: var(--bw-on-primary);
}
:root[data-theme-version="v1"] .m-bulkbar-clear {
  background-color: transparent;
  border: 0;
  color: var(--bw-nav-group);
  font-size: var(--bw-text-sm);
}
:root[data-theme-version="v1"] .m-bulkbar-clear:hover { color: var(--bw-on-primary); }

/* --- v2: light tinted bar with outlined buttons --- */
:root[data-theme-version="v2"] .m-bulkbar {
  background-color: var(--bw-primary-soft);
  border-bottom: 1px solid var(--bw-primary-soft-line);
  margin: 0;
  padding: 10px 14px;
}
:root[data-theme-version="v2"] .m-bulkbar-count {
  color: var(--bw-chrome);
  font-size: var(--bw-text-base);
  font-weight: 500;
}
:root[data-theme-version="v2"] .m-bulkbar-btn {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-primary-soft-line);
  border-radius: var(--bw-radius-sm);
  color: var(--bw-chrome);
  font-size: var(--bw-text-sm);
  font-weight: 500;
  height: 30px;
  padding: 0 11px;
}
:root[data-theme-version="v2"] .m-bulkbar-btn:hover { border-color: var(--bw-primary); }
:root[data-theme-version="v2"] .m-bulkbar-btn.is-danger {
  border-color: var(--bw-danger-line);
  color: var(--bw-danger);
}
:root[data-theme-version="v2"] .m-bulkbar-btn.is-danger:hover { background-color: var(--bw-danger-bg); }
:root[data-theme-version="v2"] .m-bulkbar-clear {
  background-color: transparent;
  border: 0;
  color: var(--bw-muted);
  font-size: var(--bw-text-sm);
  font-weight: 500;
}
:root[data-theme-version="v2"] .m-bulkbar-clear:hover { color: var(--bw-link); }

/* ---- v2 ONLY: the per-row action cell ----
   v1 has no row-action column at all; see FranchiseeGrid.getColumns.
   Sizes are the v2 handoff's: 28px tall, 6px radius, 4px gaps, right-aligned. */
:root[data-theme-version] .m-rowactions {
  display: flex;
  gap: 4px;
  justify-content: flex-end;
  width: 100%;
}
/* ⚠ The cell is itself a flex container with `padding: 0 10px`, so these buttons are
   flex items in a box narrower than the column. Left shrinkable they were squeezed
   proportionally and the ⋯ — which has no text to hold it open — collapsed from its
   declared 28px to 12.8px, reading as a broken sliver rather than a button. `flex: none`
   is what makes the declared sizes actually hold. */
:root[data-theme-version] .m-rowaction {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: 6px;
  color: var(--bw-muted);
  cursor: pointer;
  flex: none;
  font-family: var(--bw-font);
  font-size: var(--bw-text-sm);
  font-weight: 500;
  height: 28px;
  padding: 0 10px;
  white-space: nowrap;
}
:root[data-theme-version] .m-rowaction:hover { border-color: var(--bw-primary); color: var(--bw-link); }
/* Square, and the glyph sized to fill it — at label size the three dots read as dust. */
:root[data-theme-version] .m-rowaction.is-more {
  align-items: center;
  display: inline-flex;
  font-size: 17px;
  justify-content: center;
  line-height: 1;
  padding: 0;
  width: 28px;
}
/* The cell's own 10px side padding is what squeezed the row; the action cell needs less. */
:root[data-theme-version] .slick-cell.m-rowactions-cell { padding: 0 8px; }

/* ---- 05: row expand — nested programmes inline ----
   Two cooperating pieces, see Modules/Franchise/Franchisee/FranchiseeRowExpand.ts:
   the SPACER rows reserve height (SleekGrid has no variable row height, so the space has
   to be made of real rows), and .m-row-detail is a normal node absolutely positioned over
   them inside .grid-canvas. Because the canvas scrolls with the rows, the panel tracks
   its row with no scroll handler. */
/* ---- the row-selection checkbox ----
   ⚠ common-theme.css sizes it in EM (`.slick-cell .check-box { width: 1.4em; height: 1.4em }`),
   which against the cell's 13.5px text works out at ~19px — but the select column's
   content box is only ~14px wide once the cell's 10px side padding is taken off, so the
   box shrank horizontally while keeping its height and rendered as a 14x19 RECTANGLE.
   Sizing it explicitly in px with `flex: none`, and giving the cell its own tighter
   padding, is what makes it square. Both handoffs draw a square: v1 14px, v2 15px. */
:root[data-theme-version] .slick-cell.m-select-cell { justify-content: center; padding: 0 4px; }
:root[data-theme-version] .slick-cell.m-select-cell .check-box,
:root[data-theme-version] .slick-header-column .select-all-items {
  border-radius: var(--bw-checkbox-radius);
  flex: none;
  height: var(--bw-checkbox);
  margin: 0;
  width: var(--bw-checkbox);
}

:root[data-theme-version] .m-expand-cell { cursor: pointer; }
:root[data-theme-version] .m-expand-chevron {
  border-right: 1.5px solid var(--bw-muted);
  border-bottom: 1.5px solid var(--bw-muted);
  display: inline-block;
  height: 6px;
  transform: rotate(-45deg);
  transition: transform .12s ease;
  width: 6px;
}
:root[data-theme-version] .m-expand-cell:hover .m-expand-chevron { border-color: var(--bw-primary); }
:root[data-theme-version] .m-row-expanded .m-expand-chevron { transform: rotate(45deg); }
:root[data-theme-version] .m-row-expanded .slick-cell { background-color: var(--bw-primary-soft); }

/* The spacers exist only to occupy height — no borders, no hover, nothing to read. */
:root[data-theme-version] .slick-row.m-detail-spacer .slick-cell {
  background-color: transparent;
  border-bottom: 0;
}
:root[data-theme-version] .slick-row.m-detail-spacer:hover .slick-cell { background-color: transparent; }

:root[data-theme-version] .m-row-detail {
  background-color: var(--bw-field);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: absolute;
  /* left/width are set from script so the panel tracks the viewport rather than the
     (possibly much wider) canvas — see RowExpand.trackHorizontally. */
  /* Above the spacer rows it covers, below any dialog. */
  z-index: 2;
}
/* ---- the link rows inside an expanded navigation group ----
   The hierarchy the Menus screen shows: a group, then its links in rail order. Each row is a
   BUTTON, not a div with a click handler, so it is reachable and operable from the keyboard for
   free — a list of clickable divs is the usual way this gets shipped inaccessible. */
:root[data-theme-version] .m-navlinks {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
:root[data-theme-version] .m-navlink {
  align-items: center;
  background: none;
  border: 0;
  border-radius: var(--bw-radius-sm);
  color: var(--bw-text);
  cursor: pointer;
  display: grid;
  font-size: var(--bw-text-sm);
  gap: 10px;
  grid-template-columns: 3.5ch 16px minmax(120px, 1.1fr) minmax(0, 2fr) auto;
  padding: 7px 10px;
  text-align: left;
  width: 100%;
}
:root[data-theme-version] .m-navlink:hover { background-color: var(--bw-primary-soft); }
:root[data-theme-version] .m-navlink > i { color: var(--bw-muted); font-size: 13px; }
:root[data-theme-version] .m-navlink-order {
  color: var(--bw-faint);
  font-family: var(--bw-font-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
:root[data-theme-version] .m-navlink-title { font-weight: 500; }
:root[data-theme-version] .m-navlink-url {
  color: var(--bw-muted);
  font-family: var(--bw-font-mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* State in FORM as well as words: a hidden link is dimmed and labelled, so it reads at a glance
   without having to compare a column of ticks. */
:root[data-theme-version] .m-navlink.is-hidden { opacity: .55; }
:root[data-theme-version] .m-navlink-tag {
  background-color: var(--bw-field);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-pill);
  color: var(--bw-muted);
  font-size: 10.5px;
  letter-spacing: .02em;
  padding: 1px 7px;
  white-space: nowrap;
}

:root[data-theme-version] .m-detail-head {
  align-items: center;
  border-bottom: 1px solid var(--bw-border);
  display: flex;
  gap: 10px;
  padding: 8px 12px;
}
:root[data-theme-version] .m-detail-title {
  color: var(--bw-text-strong);
  font-size: var(--bw-text-base);
  font-weight: 600;
}
:root[data-theme-version] .m-detail-add {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border-strong);
  border-radius: var(--bw-radius-sm);
  color: var(--bw-text);
  cursor: pointer;
  font-family: var(--bw-font);
  font-size: var(--bw-text-sm);
  font-weight: 600;
  margin-left: auto;
  min-height: var(--bw-control-h-sm);
  padding: 0 12px;
}
:root[data-theme-version] .m-detail-add:hover { background-color: var(--bw-hover-bg); border-color: var(--bw-primary); }
:root[data-theme-version] .m-detail-body { flex: 1 1 auto; overflow: auto; }
:root[data-theme-version] .m-detail-table { border-collapse: collapse; width: 100%; }
/* Same treatment as .slick-header-column above — weight and transform are literals there
   too, because uppercase column heads are specified for BOTH generations. */
:root[data-theme-version] .m-detail-table th {
  color: var(--bw-colhead-color);
  font-size: var(--bw-colhead-size);
  font-weight: 600;
  letter-spacing: var(--bw-colhead-spacing);
  padding: 6px 12px;
  text-align: left;
  text-transform: uppercase;
}
:root[data-theme-version] .m-detail-table td {
  border-top: 1px solid var(--bw-row-line);
  color: var(--bw-text);
  font-size: var(--bw-text-sm);
  padding: 6px 12px;
}
:root[data-theme-version] .m-detail-table .num { font-family: var(--bw-font-mono); text-align: right; }
:root[data-theme-version] .m-detail-foot {
  border-top: 1px solid var(--bw-border);
  color: var(--bw-faint);
  font-family: var(--bw-font-mono);
  font-size: var(--bw-text-sm);
  padding: 6px 12px;
}
/* The empty panel is a prompt, not a dead end: a heading, one line of why, and the
   action repeated where the eye already is. */
:root[data-theme-version] .m-detail-empty {
  align-items: center;
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 18px 12px 20px;
  text-align: center;
}
:root[data-theme-version] .m-detail-empty-title {
  color: var(--bw-text-strong);
  font-size: var(--bw-text-base);
  font-weight: 600;
}
:root[data-theme-version] .m-detail-empty-body {
  color: var(--bw-faint);
  font-size: var(--bw-text-sm);
  max-width: 420px;
}
:root[data-theme-version] .m-detail-empty-cta {
  background-color: var(--bw-primary);
  border: 1px solid var(--bw-primary);
  border-radius: var(--bw-radius-sm);
  color: var(--bw-on-primary);
  cursor: pointer;
  font-family: var(--bw-font);
  font-size: var(--bw-text-sm);
  font-weight: 600;
  margin-top: 5px;
  min-height: var(--bw-control-h-sm);
  padding: 0 14px;
}
:root[data-theme-version] .m-detail-empty-cta:hover { background-color: var(--bw-primary-hover); }

/* ---- 03: inline edit — the "Unsaved · Save · Esc" chip ----
   Anchored to the dirty row inside .grid-canvas so it scrolls with it, and pinned to the
   viewport's right edge by script (the canvas can be far wider than the viewport). */
:root[data-theme-version] .m-inline-edit {
  align-items: center;
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-accent);
  border-radius: var(--bw-radius-sm);
  box-shadow: var(--bw-shadow);
  display: flex;
  gap: 6px;
  height: calc(var(--bw-row-h) - 6px);
  padding: 0 6px;
  position: absolute;
  z-index: 4;
}
/* The one place v1's orange accent is correct: it marks state, never a surface. */
:root[data-theme-version] .m-inline-edit-flag {
  color: var(--bw-accent-ink);
  font-size: var(--bw-text-xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}
:root[data-theme-version] .m-inline-edit-save,
:root[data-theme-version] .m-inline-edit-esc {
  border-radius: var(--bw-radius-sm);
  cursor: pointer;
  font-family: var(--bw-font);
  font-size: var(--bw-text-sm);
  font-weight: 600;
  height: calc(var(--bw-row-h) - 14px);
  padding: 0 8px;
}
:root[data-theme-version] .m-inline-edit-save {
  background-color: var(--bw-primary);
  border: 1px solid var(--bw-primary);
  color: var(--bw-on-primary);
}
:root[data-theme-version] .m-inline-edit-save:hover { background-color: var(--bw-primary-hover); }
:root[data-theme-version] .m-inline-edit-esc {
  background-color: transparent;
  border: 1px solid var(--bw-border-strong);
  color: var(--bw-muted);
  font-family: var(--bw-font-mono);
}
:root[data-theme-version] .m-inline-edit-esc:hover { color: var(--bw-text); }
/* SleekGrid's own editor box inside a cell, so it reads as part of the table. */
:root[data-theme-version] .slick-cell.editable input,
:root[data-theme-version] .slick-cell .editor-text {
  background-color: var(--bw-input-bg);
  border: 1px solid var(--bw-primary);
  border-radius: var(--bw-radius-sm);
  color: var(--bw-text);
  font-family: var(--bw-font);
  font-size: var(--bw-text-base);
  height: calc(var(--bw-row-h) - 8px);
  outline: 0;
  padding: 0 6px;
  width: 100%;
}

/* ---- 06: stacked dialogs — the level breadcrumb ----
   Built by Modules/Common/UI/StackedDialogs.ts, which only annotates depth; it never
   changes how a dialog behaves. Sits directly under the header of the TOP dialog. */
:root[data-theme-version] .m-dialog-levels {
  align-items: center;
  background-color: var(--bw-field);
  border-bottom: 1px solid var(--bw-border);
  color: var(--bw-muted);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--bw-text-sm);
  gap: 6px;
  padding: 7px var(--bw-pad);
}
:root[data-theme-version] .m-dialog-levels-crumb { color: var(--bw-faint); }
:root[data-theme-version] .m-dialog-levels-crumb.is-current { color: var(--bw-text-strong); font-weight: 600; }
:root[data-theme-version] .m-dialog-levels-sep { color: var(--bw-faint); }
:root[data-theme-version] .m-dialog-levels-hint {
  color: var(--bw-faint);
  font-family: var(--bw-font-mono);
  font-size: var(--bw-text-xs);
  margin-left: auto;
}
/* Depth cue on the dialog itself — the buried level recedes rather than competing. */
:root[data-theme-version] .modal[data-m-level] .modal-content { transition: filter .15s ease; }
:root[data-theme-version] .modal[data-m-level="1"] .modal-content { filter: brightness(.97); }

/* ---- 07: the typed-DELETE confirmation ----
   Reuses .modal/.modal-content/.modal-header so it inherits both generations' dialog
   treatment (v1 navy header, v2 light) with no per-version rule here. Only the parts
   Bootstrap has no equivalent for are styled. */
/* ⚠ `max-width` MUST be declared in the compound-selector block below, not on its own here.
   On its own this reads (0,4,0) — `:is()` contributes only its most specific argument — which
   TIES v2's `.modal-dialog.modal-dialog-centered` drawer rule and then loses on source order,
   because that rule appears later in the file. BWDesignSystem's own `.m-confirm-modal` never
   exposed the tie: it does not carry `modal-dialog-centered`, so the only v2 rule matching it
   is the plainer (0,3,0) one, which this outranks. Serenity's `.s-MessageDialog` DOES carry it,
   and the symptom was a correctly-shaped card still 760px wide. */
/* ⚠ A CONFIRMATION IS NOT A DRAWER. Inheriting v2's create/edit geometry stretched this
   520px card to the full viewport height, leaving the question at the top and the two buttons
   400px below it with nothing in between — and a destructive choice is the last thing that
   should be spread out. The prototype draws it as a compact card pinned near the top over the
   scrim (460px, 140px down), which is what both generations get here: the drawer rules are
   for a form you work in, not for a question you answer.

   ⚠ **`.s-MessageDialog` needs exactly the same treatment, and was missed until 2026-08-18.**
   That is Serenity's OWN alert / confirm / information / warning / success dialog — nothing to
   do with BWDesignSystem's typed-delete card — and because it is just another `.modal`, v2 handed it
   the full drawer: a one-line "Invalid username or password!" rendered as a **760 x 639**
   right-hand sheet with a 505px empty body and the OK button stranded at the bottom. Found on
   the sign-in screen, where it is the first thing a failed login shows, so it is worth knowing
   these dialogs come from the framework and appear on screens BWDesignSystem never wrote. */
:root[data-theme-version] :is(.m-confirm-modal, .s-MessageDialog) .modal-dialog,
:root[data-theme-version] :is(.m-confirm-modal, .s-MessageDialog) .modal-dialog.modal-dialog-centered {
  --bw-dialog-top: 140px;
  align-items: flex-start;
  height: auto;
  margin: var(--bw-dialog-top) auto;
  max-width: 520px;
  min-height: 0;
  width: auto;
}
:root[data-theme-version] :is(.m-confirm-modal, .s-MessageDialog) .modal-content {
  border-left-width: 1px;
  border-radius: var(--bw-radius-lg);
  height: auto;
  max-height: calc(100vh - var(--bw-dialog-top) - 24px);
  min-height: 0;
}
/* ⚠ The tone glyph is `.modal-body::before` — `position: absolute`, 60x60 at left:14px, drawn
   by common-theme.css. Stock Serenity reserves room for it with the body's own padding; this
   file overrides `.modal-body` padding for form dialogs, which silently removed that
   reservation and let the message text run UNDERNEATH the icon (the red "!" sat on top of the
   first words). The space is given back here rather than by restoring the stock padding, so
   form dialogs keep their own density. */
:root[data-theme-version] .s-MessageDialog .modal-body {
  display: block;
  padding: 22px 22px 22px 84px;
}
:root[data-theme-version] .s-MessageDialog .modal-body > .message {
  color: var(--bw-text);
  font-size: var(--bw-text-base);
  line-height: 1.55;
  min-height: 44px;                 /* keeps a one-line message clear of the 60px glyph */
}
:root[data-theme-version] .m-confirm-consequence {
  color: var(--bw-text);
  font-size: var(--bw-text-base);
  line-height: 1.5;
  margin: 0 0 10px;
}
:root[data-theme-version] .m-confirm-items {
  background-color: var(--bw-field);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius-sm);
  font-size: var(--bw-text-sm);
  list-style: none;
  margin: 0 0 12px;
  padding: 8px 12px;
}
:root[data-theme-version] .m-confirm-items li { padding: 3px 0; }
:root[data-theme-version] .m-confirm-more { color: var(--bw-faint); font-style: italic; }
:root[data-theme-version] .m-confirm-warn {
  color: var(--bw-danger);
  font-size: var(--bw-text-sm);
  font-weight: 600;
  margin: 0 0 14px;
}
:root[data-theme-version] .m-confirm-typed { display: flex; flex-direction: column; gap: 6px; }
:root[data-theme-version] .m-confirm-typed > span {
  color: var(--bw-text);
  font-size: var(--bw-text-sm);
  font-weight: 600;
}
:root[data-theme-version] .m-confirm-input {
  background-color: var(--bw-input-bg);
  border: 1px solid var(--bw-border-strong);
  border-radius: var(--bw-radius-sm);
  color: var(--bw-text);
  font-family: var(--bw-font-mono);
  font-size: var(--bw-text-base);
  letter-spacing: .08em;
  min-height: var(--bw-control-h);
  padding: 0 10px;
}
:root[data-theme-version] .m-confirm-input:focus {
  border-color: var(--bw-primary);
  box-shadow: var(--bw-focus-ring);
  outline: 0;
}

:root[data-theme-version] #nprogress .bar { background: var(--bw-accent); height: 2px; }
:root[data-theme-version] #nprogress .peg { box-shadow: 0 0 10px var(--bw-accent), 0 0 5px var(--bw-accent); }
:root[data-theme-version] #nprogress .spinner-icon { border-top-color: var(--bw-accent); border-left-color: var(--bw-accent); }

/* ============================================================================
   6. STRUCTURAL SHELL — where the two designs stop being a palette swap.

   v1 is a 56px ICON RAIL that expands to 236px on hover, beside a 52px white top
   bar, with create/edit as centred dialogs.
   v2 is a 248px ALWAYS-LABELLED sidebar beside a 56px white top bar, with
   create/edit as a right-hand drawer.

   Both are driven from the SAME Serene markup (_Sidebar.cshtml + .s-main) rather
   than a forked partial. That partial carries the nav search, language picker and
   user menu with their dropdown wiring; reproducing it per version would mean
   maintaining that behaviour twice and re-testing it twice. Everything below is a
   layout decision CSS can express, so CSS expresses it.

   ⚠ Specificity note: Serenity's common-theme.css sets `.s-main { padding-left:
   300px }` and `.s-sidebar { top: 48px; width: 300px }` inside a
   `@media (min-width: 1200px)` block. Media queries do not add specificity, so
   the `:root[data-theme-version]` prefix here (attribute + class) outranks those
   plain class selectors at every width — no !important and no duplicated media
   query needed. The one genuine exception is the top bar's own
   `height: 48px !important`, which has to be met in kind.
   ============================================================================ */

/* The nav runs the FULL height of the viewport in both designs and the top bar
   starts to its right — not the stock arrangement, where a full-width bar sits
   above a nav that begins 48px down. */
:root[data-theme-version] .s-sidebar {
  bottom: 0;
  display: flex;
  overflow: hidden;                 /* labels must not spill while collapsed */
  top: 0;
  transition: width .16s ease;
  width: var(--bw-nav-w);
  z-index: 1040;                    /* above the sticky top bar, so the v1 rail
                                       can expand over it rather than under it */
}
:root[data-theme-version] .s-sidebar-header {
  margin-left: var(--bw-nav-w);
  transition: margin-left .16s ease;
  z-index: 1020;
}
:root[data-theme-version] .s-main {
  padding-left: var(--bw-nav-w);
  transition: padding-left .16s ease;
}
:root[data-theme-version] section.content { min-height: calc(100vh - var(--bw-topbar-h)); }

/* ⚠ .s-sidebar is a flex ROW whose only child, .s-sidebar-pane, is absolutely
   positioned at a fixed 300px. Narrowing .s-sidebar alone left the pane at 300px
   and pushed it to x=-53, so the search box and link labels were clipped off the
   left edge. Sizing the pane to the same token keeps the two in step at every
   width and through the v1 hover expansion. */
:root[data-theme-version] .s-sidebar-pane {
  flex: 0 0 var(--bw-nav-w);
  min-width: 0;
  transition: width .16s ease, flex-basis .16s ease;
  width: var(--bw-nav-w);
}

/* ---- V1: the icon rail, with hover FLYOUTS ----
   The rail no longer widens on hover. It stays 56px and a hovered group opens its
   children in a white flyout card to the right, which is the pattern the eFMS
   "efms_blue" rail uses (src-dotnet/Efms/Fms.Web/wwwroot/Content/site/layout-rail.css)
   and the one the user asked for after reviewing it.

   WHY THE EXPANDING RAIL WAS REPLACED
   Expanding 56px → 236px revealed the top-level LABELS but not the children: nested
   menus are Bootstrap collapses, so reaching Languages or Roles meant hover, then
   click to expand the group, then click the item — and the rail collapsed the moment
   the pointer left it. The flyout shows a group's contents on hover, one movement,
   which is what an icon rail is for. Only the interaction pattern is borrowed; no
   eFMS markup, naming or domain code came across.

   The nested <ul> already exists in the markup as the collapse target, so the flyout
   needs no new element — it is that list, absolutely positioned. */
:root[data-theme-version="v1"] .s-sidebar-link-text,
:root[data-theme-version="v1"] .s-sidebar-menu-toggle,
:root[data-theme-version="v1"] .m-brand-words,
:root[data-theme-version="v1"] .s-sidebar-group,
:root[data-theme-version="v1"] .s-sidebar-section { display: none; }

/* ⚠ overflow must stay VISIBLE on BOTH axes all the way up, or the flyout is clipped
   at the 56px rail edge. Per the CSS overflow spec, setting ONE axis to anything other
   than `visible` forces the other to compute to `auto` — so an `overflow-y: auto` on
   the pane silently defeats `overflow-x: visible` and there is no way to tell from the
   rule which one broke it. This is the single most confusing failure in the pattern and
   the eFMS stylesheet carries the same warning. */
:root[data-theme-version="v1"] .s-sidebar,
:root[data-theme-version="v1"] .s-sidebar-pane,
:root[data-theme-version="v1"] .s-sidebar-menu { overflow: visible; }

/* The rail's own items: one centred tile per top-level entry. */
:root[data-theme-version="v1"] .s-sidebar-pane > .s-sidebar-menu { padding: 4px 0; }
:root[data-theme-version="v1"] .s-sidebar-item { position: relative; }
:root[data-theme-version="v1"] .s-sidebar-pane > .s-sidebar-menu > .s-sidebar-item > .s-sidebar-link {
  border-radius: var(--bw-radius-sm);
  gap: 0;
  height: var(--bw-nav-item-h);
  justify-content: center;
  margin: 2px auto;
  padding: 0;
  width: var(--bw-nav-item-h);
}
:root[data-theme-version="v1"] .m-brand { justify-content: center; padding: 0; }

/* A group's children, as a flyout card. Kept in flow with visibility/opacity rather
   than display so it can fade, and so Bootstrap's collapse classes cannot fight it:
   `.collapse:not(.show)` sets `display: none`, and toggling `.show` by clicking the
   parent would otherwise pin the panel open with no way to dismiss it. */
:root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius);
  box-shadow: var(--bw-shadow-lg);
  display: block;
  left: 100%;
  margin: 0 0 0 6px;
  min-width: 208px;
  opacity: 0;
  padding: 6px;
  position: absolute;
  top: -6px;
  transform: translateX(-4px);
  transition: opacity .14s ease, transform .14s ease, visibility .14s;
  visibility: hidden;
  z-index: 1050;
}
/* Bootstrap writes an inline height while animating a collapse, and an inline style
   beats any selector — so the click path has to be neutralised with !important or a
   stray click leaves the flyout 0px tall.

   ⚠ MUST be fenced to the width where the flyout actually exists. Below 1200px the drawer
   uses Bootstrap's collapse for real, and `height: auto !important` there defeats the
   animation outright: `!important` in a stylesheet outranks Bootstrap's INLINE height, which
   is the one thing driving the open/close transition. That cannot be undone from the mobile
   block either — overriding an `!important` needs another `!important`, which lands right back
   on top of the inline height. Scoping is the only way out, so this rule is desktop-only. */
@media (min-width: 1200px) {
  :root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu.collapsing {
    height: auto !important;
    overflow: visible !important;
    transition: opacity .14s ease, transform .14s ease, visibility .14s !important;
  }
}
:root[data-theme-version="v1"] .s-sidebar-item:hover > .s-sidebar-menu,
:root[data-theme-version="v1"] .s-sidebar-item:focus-within > .s-sidebar-menu {
  opacity: 1;
  transform: translateX(0);
  visibility: visible;
}
/* The group name heads the panel — without it a flyout of four links is unattributed. */
:root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu::before {
  color: var(--bw-eyebrow-color);
  content: attr(data-title);
  display: block;
  font-size: var(--bw-eyebrow-size);
  font-weight: var(--bw-eyebrow-weight);
  letter-spacing: var(--bw-eyebrow-spacing);
  padding: 6px 10px 8px;
  text-transform: var(--bw-eyebrow-transform);
}
/* Flyout links sit on a WHITE card, so they cannot keep the chrome palette they
   inherit from the dark rail. */
:root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu .s-sidebar-link {
  border-radius: var(--bw-radius-sm);
  color: var(--bw-text);
  font-size: var(--bw-text-base);
  gap: 9px;
  justify-content: flex-start;
  margin: 0;
  min-height: 32px;
  padding: 0 10px;
  white-space: nowrap;
  width: auto;
}
:root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu .s-sidebar-link-text { display: block; }
:root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu .s-sidebar-icon { color: var(--bw-muted); }
:root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu .s-sidebar-link:hover,
:root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu .active > .s-sidebar-link {
  background-color: var(--bw-primary-soft);
  color: var(--bw-link);
}
:root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu .s-sidebar-link:hover .s-sidebar-icon,
:root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu .active > .s-sidebar-link .s-sidebar-icon { color: inherit; }

/* A top-level entry with no children gets the same treatment in miniature: its name
   as a small chip, so every rail tile answers "what is this?" the same way. */
:root[data-theme-version="v1"] .s-sidebar-pane > .s-sidebar-menu > .s-sidebar-item:not(.has-children) > .s-sidebar-link::after {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius);
  box-shadow: var(--bw-shadow-lg);
  color: var(--bw-text);
  content: attr(aria-label);
  font-size: var(--bw-text-sm);
  left: 100%;
  margin-left: 6px;
  opacity: 0;
  padding: 6px 10px;
  pointer-events: none;
  position: absolute;
  top: 50%;
  transform: translate(-4px, -50%);
  transition: opacity .14s ease, transform .14s ease;
  white-space: nowrap;
  z-index: 1050;
}
:root[data-theme-version="v1"] .s-sidebar-pane > .s-sidebar-menu > .s-sidebar-item:not(.has-children) > .s-sidebar-link:hover::after {
  opacity: 1;
  transform: translate(0, -50%);
}

/* On a short viewport the rail has to scroll, and a scrolling ancestor clips the
   flyouts — accepted, exactly as the eFMS rail does, because the alternative is
   positioning them with script against a scroll container. */
@media (max-height: 460px) {
  :root[data-theme-version="v1"] .s-sidebar-pane { overflow-y: auto; scrollbar-width: none; }
}

/* ---- V2: the labelled sidebar ---- */
:root[data-theme-version="v2"] .s-sidebar-link-text {
  flex: 1 1 auto;
  font-size: 13.5px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- V1: create/edit as a centred dialog ----
   The doc puts it 60px from the top at 1000px wide rather than vertically
   centred, so a long form grows downward from a fixed, predictable position
   instead of jumping as fields are added. */
/* Serenity adds .modal-dialog-centered, which vertically centres the sheet; the doc
   pins it 60px from the top so a form that grows as fields are filled in stays put
   instead of creeping upward. min-height has to be reset too — that is the property
   Bootstrap uses to do the centring. */
:root[data-theme-version="v1"] .modal-dialog,
:root[data-theme-version="v1"] .modal-dialog.modal-dialog-centered {
  align-items: flex-start;
  /* The offset lives in a custom property so the height cap below can subtract the SAME
     number — see the note there. Stacked levels override just this one value. */
  --bw-dialog-top: 60px;
  /* ⚠ `height: auto` is load-bearing BELOW 768px, and only there. Serenity puts Bootstrap's
     `.modal-fullscreen-md-down` on every dialog, whose `@media (max-width: 767.98px)` rule
     sets `height: 100%`. Media queries add no specificity, so this selector outranks it —
     but only for properties it actually declares, and `height` was not one of them. The
     result was the third variant of the size-vs-position bug this file keeps hitting: the
     sheet took its WIDTH and MARGIN from here (520px, 60px down) and its HEIGHT from
     Bootstrap (100%), so 60px of it started below the fold and .modal became a scroller with
     120px of travel — the 60px overhang plus the 60px bottom margin. Nothing was unreachable
     (the .modal-content cap below keeps the form and its action bar inside), so it read as a
     sheet that mysteriously jiggles on a phone. `auto` is what the dialog computes at desktop
     anyway, so this makes one geometry correct at every width, and it is exactly what
     .m-confirm-modal already does for the same reason. */
  height: auto;
  margin: var(--bw-dialog-top) auto;
  max-width: min(var(--bw-dialog-w), calc(100vw - 48px));
  min-height: 0;
}
/* Same shape as v2's drawer for the same reason: the sheet is capped, so it must be a flex
   column whose BODY absorbs the difference — otherwise a dialog with its own footer (the
   permission editor) grows past the cap and `overflow: hidden` on .modal-content clips OK /
   Cancel clean off the bottom, with nothing to scroll.

   ⚠ The cap must subtract the sheet's OWN top offset, not a constant. It used to be
   `100vh - 120px`, which silently assumed the 60px offset — correct for a level-1 dialog and
   wrong for section 06's stacked pair, where the child sits 190px down. On a 695px viewport
   that put its OK / Cancel at y=764, sixty-nine pixels below the fold and unreachable,
   because the sheet owns the scroll and the page does not. Inheriting --bw-dialog-top from
   .modal-dialog keeps one rule correct for every level. */
:root[data-theme-version="v1"] .modal-content {
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - var(--bw-dialog-top) - 24px);
}
:root[data-theme-version="v1"] .modal-content > .modal-body { flex: 1 1 auto; min-height: 0; }
:root[data-theme-version="v1"] .modal-content > .modal-header,
:root[data-theme-version="v1"] .modal-content > .modal-footer { flex: 0 0 auto; }
:root[data-theme-version="v1"] .modal-body { overflow-y: auto; }

/* ---- V2: create/edit as a right drawer ----
   Serenity centres its dialogs. v2 slides them in from the right, full height and
   square-cornered against the viewport edge. Only .modal-dialog is repositioned —
   the dialog's own content, toolbar and form keep working exactly as they do in
   v1, so this is presentation, not a second dialog implementation. Width caps at
   90vw so a phone still gets a usable sheet. */
:root[data-theme-version="v2"] .modal-dialog,
:root[data-theme-version="v2"] .modal-dialog.modal-dialog-centered {
  align-items: stretch;
  margin: 0 0 0 auto;
  max-width: var(--bw-dialog-w);
  min-height: 100vh;
  width: var(--bw-dialog-w);
}
:root[data-theme-version="v2"] .modal-body { overflow-y: auto; }
/* ⚠ height AND max-height, not just min-height, and the sheet has to be a flex column.
   With min-height alone the drawer was free to grow TALLER than the viewport: a dialog with
   its own `.modal-footer` — the permission editor, which puts OK / Cancel there rather than
   in the body toolbar our forms use — came to 673px in a 639px viewport, so both buttons were
   cut off below the fold with no way to scroll to them (the sheet, not the page, owns the
   scroll). Pinning the sheet to the viewport and letting only the BODY flex keeps the header
   and footer on screen at any content length. */
:root[data-theme-version="v2"] .modal-content {
  border: 0;
  border-left: 1px solid var(--bw-border);
  border-radius: 0;
  box-shadow: var(--bw-shadow-pop);
  display: flex;
  flex-direction: column;
  height: 100vh;
  max-height: 100vh;
  min-height: 100vh;
}
/* min-height: 0 is what allows a flex child to shrink below its content size — without it
   the body refuses to shrink and pushes the footer off the bottom again. */
:root[data-theme-version="v2"] .modal-content > .modal-body { flex: 1 1 auto; min-height: 0; }
:root[data-theme-version="v2"] .modal-content > .modal-header,
:root[data-theme-version="v2"] .modal-content > .modal-footer { flex: 0 0 auto; }
/* v2's drawer header is light, so it needs a rule under it that v1's navy bar
   does not, and the title sits above a kicker rather than beside a subtitle. */
:root[data-theme-version="v2"] .modal-header,
:root[data-theme-version="v2"] .ui-dialog-titlebar {
  border-bottom: 1px solid var(--bw-border);
  padding: 18px var(--bw-pad-lg);
}
:root[data-theme-version="v2"] .modal-title,
:root[data-theme-version="v2"] .ui-dialog-title { font-size: 20px; letter-spacing: -.015em; }
@media (prefers-reduced-motion: no-preference) {
  :root[data-theme-version="v2"] .modal.show .modal-dialog { animation: m-drawer-in .18s ease-out; }
}
@keyframes m-drawer-in {
  from { opacity: .4; transform: translateX(24px); }
  to   { opacity: 1;  transform: translateX(0); }
}

/* ---- dialog form density ----
   Both handoffs draw a form control 2px taller inside a dialog than in a list: v1 is
   32px in the tools row and 34px in the create dialog, v2 is 34px and 36px. Rather than
   duplicate every input, select2, button and select rule with a dialog-scoped copy, the
   dialog body simply REDEFINES the two height tokens for its subtree — custom properties
   inherit, so every rule already written against --bw-control-h / --bw-btn-h picks the
   taller value up unchanged. Adding a control to a dialog needs no new rule.

   ⚠ Both selectors are required: the editor body is .modal-body in modal mode and
   .s-DialogContent in panel / jQuery-UI mode (see CLAUDE.md). */
:root[data-theme-version] :is(.modal-body, .s-DialogContent) {
  --bw-control-h: var(--bw-form-h);
  --bw-btn-h:     var(--bw-form-h);
  /* ⚠ The two Serenity input variables must be restated here. A var() inside a custom
     property is substituted where the property is DECLARED, and both are declared on
     :root in the framework bridge — so they were already resolved against the list
     height and would inherit into the dialog as a fixed number, leaving Serenity's own
     inputs 2px short inside a form whose --bw-control-h had grown. */
  --s-input-inner-height: calc(var(--bw-form-h) - 2px);
  --s-input-line-height:  calc(var(--bw-form-h) - 2px);
}
/* The action bar needs nothing extra: it is Serenity's own div.s-Toolbar sitting INSIDE
   .modal-body (moved to the bottom edge with flex order, see "dialog action bar" above),
   so Cancel / Save / Save & add another inherit the taller --bw-btn-h with the fields. */

/* ---- stacked dialogs: the two levels are not the same sheet ----
   StackedDialogs.ts stamps data-m-level on each visible .modal, 1-based, and ONLY when
   two or more are open — so [data-m-level="1"] means "this dialog has something on top
   of it", which is exactly the state section 06 draws differently. No script change was
   needed for this; the attribute was already there for the breadcrumb.

   v1: the parent recedes (960px, 74px from the top) and the child comes in narrower and
   lower (820px, 190px), so both are visible and which one is in front is unambiguous.
   v2: the parent stays put and the child is a narrower drawer over it — the doc's
   min(560px, 80vw) against the parent's 760px. This is what closes the gap where a
   level-2 drawer covered level 1 completely and only the breadcrumb conveyed depth. */
/* Only the offset token changes; `margin` and the height cap both read --bw-dialog-top, so a
   stacked level cannot end up positioned by one number and sized by another. */
:root[data-theme-version="v1"] .modal[data-m-level="1"] .modal-dialog,
:root[data-theme-version="v1"] .modal[data-m-level="1"] .modal-dialog.modal-dialog-centered {
  --bw-dialog-top: var(--bw-dialog-top-back);
  max-width: min(var(--bw-dialog-w-back), calc(100vw - 48px));
}
:root[data-theme-version="v1"] .modal[data-m-level="2"] .modal-dialog,
:root[data-theme-version="v1"] .modal[data-m-level="2"] .modal-dialog.modal-dialog-centered {
  --bw-dialog-top: var(--bw-dialog-top-2);
  max-width: min(var(--bw-dialog-w-2), calc(100vw - 48px));
}
/* The child's own scrim, over an already-scrimmed parent — the doc dims a second time at
   .30 rather than reusing the full-strength overlay, so the parent stays readable. */
:root[data-theme-version="v1"] .modal[data-m-level="2"] { background-color: var(--bw-overlay-2); }
:root[data-theme-version="v1"] .modal[data-m-level="2"] .modal-content { box-shadow: var(--bw-shadow-pop); }

:root[data-theme-version="v2"] .modal[data-m-level="2"] .modal-dialog,
:root[data-theme-version="v2"] .modal[data-m-level="2"] .modal-dialog.modal-dialog-centered {
  max-width: var(--bw-dialog-w-2);
  width: var(--bw-dialog-w-2);
}
/* ⚠ Not every level-2 dialog is a narrow child FORM. The permission editors are wide GRIDS:
   at 560px their Permission column alone took 543px, so Grant and Revoke sat off the right
   edge and the only way to tick a permission was to scroll the grid sideways first. They keep
   the full drawer width, where the 703px the grid asks for fits. The narrowing is about
   showing a form on top of a form, and these are not that. */
:root[data-theme-version="v2"] .modal[data-m-level="2"]:is(.s-UserPermissionDialog, .s-RolePermissionDialog) .modal-dialog,
:root[data-theme-version="v2"] .modal[data-m-level="2"]:is(.s-UserPermissionDialog, .s-RolePermissionDialog) .modal-dialog.modal-dialog-centered {
  max-width: var(--bw-dialog-w);
  width: var(--bw-dialog-w);
}
:root[data-theme-version="v2"] .modal[data-m-level="2"] { background-color: var(--bw-overlay-2); }

/* jQuery-UI dialogs (used for filter and column-picker panels) follow the same
   rules, but they are absolutely positioned by script, so only their chrome is
   restyled — not their position. */
:root[data-theme-version] .s-FilterDialog .ui-dialog-content,
:root[data-theme-version] .s-ColumnPickerDialog .ui-dialog-content { padding: var(--bw-pad); }

/* ---- narrow viewports ----
   Serene translates .s-sidebar off-canvas below 1200px and reveals it via
   .s-sidebar-expanded. That behaviour is kept for both versions; only the gutter
   is reclaimed, since there is no rail to leave room for once it is off-screen. */
@media (max-width: 1199.98px) {
  :root[data-theme-version] .s-main { padding-left: 0; }
  :root[data-theme-version] .s-sidebar-header { margin-left: 0; }
  :root[data-theme-version] .s-sidebar,
  :root[data-theme-version] .s-sidebar-pane {
    flex-basis: var(--bw-nav-w-open);
    width: var(--bw-nav-w-open);
  }
  /* ---- the drawer starts BELOW the top bar ----
     ⚠ At `top: 0` the drawer (z-index 1040) covered the top bar (1020) — and with it the
     `#s-sidebar-toggler` that opened it, so `elementFromPoint` at the hamburger returned
     `.m-brand-mark` and the universal "tap the burger again to close" gesture did nothing.
     Raising the toggler cannot fix that: the header is `position: sticky` with a z-index, so
     it forms a stacking context and no child can escape above 1040. Offsetting the drawer is
     the fix, and it is the conventional mobile pattern anyway — the top bar keeps the version
     switch, language picker and user chip reachable while the nav is open. */
  :root[data-theme-version] .s-sidebar { top: var(--bw-topbar-h); }
  /* ---- the scrim ----
     Serenity creates no backdrop element for the sidebar (the same absence noted for
     .modal-backdrop), so it is drawn as a pseudo-element on <body>. Declared always and
     toggled by opacity rather than created by .s-sidebar-expanded, because a pseudo-element
     that springs into existence cannot transition — this way it fades.
     ⚠ `pointer-events` must flip with it. Left as `none` the scrim would be decorative and a
     tap would fall through to whatever sits under it — closing the drawer (any outside click
     does) but ALSO hitting a grid row on the way, opening an edit dialog the user never asked
     for. Set to `auto` it swallows the tap, and the click still lands on <body>, which is
     outside the sidebar, so Serene's own handler closes the drawer.
     ⚠ It stops at the top bar for the same reason the drawer does: at z-index 1035 a
     full-viewport scrim would sit over the 1020 header and re-break the hamburger. */
  :root[data-theme-version] body::after {
    background-color: var(--bw-overlay);
    content: "";
    inset: var(--bw-topbar-h) 0 0 0;
    opacity: 0;
    pointer-events: none;
    position: fixed;
    transition: opacity .16s ease;
    z-index: 1035;
  }
  :root[data-theme-version] body.s-sidebar-expanded::after { opacity: 1; pointer-events: auto; }
  /* Off-canvas, the v1 rail is a full-width drawer, so it behaves like v2's sidebar:
     labels shown, children listed inline. A hover flyout is meaningless on the touch
     devices this width implies — there is no hover — so the panel is un-positioned
     back into the flow rather than left as a card that can never be opened. */
  :root[data-theme-version="v1"] .s-sidebar .s-sidebar-link-text,
  :root[data-theme-version="v1"] .s-sidebar .s-sidebar-menu-toggle,
  :root[data-theme-version="v1"] .s-sidebar .s-sidebar-group { display: block; }
  /* ⚠ `.m-brand-words` must come back as FLEX, not block — it was in the group above and that
     is what clipped the tagline. It is a <span> wrapping two <span>s, so `display: block` on
     the wrapper leaves both children `inline`: the name and the tagline ran side by side on one
     line into .m-brand's `overflow: hidden; white-space: nowrap`, reading as "BWDesignSystem
     FRANCHISE MANAGEM…". The base rule's `flex-direction: column` is the whole lockup — it
     blockifies the children and stacks them — so re-showing it needs the display value it was
     designed with, not merely a value that is not `none`. */
  :root[data-theme-version="v1"] .s-sidebar .m-brand-words { display: flex; }
  :root[data-theme-version="v1"] .m-brand { justify-content: flex-start; padding: 0 12px; }
  :root[data-theme-version="v1"] .s-sidebar-pane > .s-sidebar-menu { padding: 4px 9px; }
  :root[data-theme-version="v1"] .s-sidebar-pane > .s-sidebar-menu > .s-sidebar-item > .s-sidebar-link {
    gap: 10px;
    justify-content: flex-start;
    margin: 2px 0;
    padding: 0 10px;
    width: auto;
  }
  :root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu {
    background: none;
    border: 0;
    box-shadow: none;
    margin: 0;
    min-width: 0;
    opacity: 1;
    padding: 0 0 0 14px;
    position: static;
    transform: none;
    visibility: visible;
  }
  /* ⚠ Hand control back to Bootstrap's collapse, or a group can never be CLOSED.
     The flyout base rule above pins `display: block` at (0,4,0), which outranks Bootstrap's
     `.collapse:not(.show) { display: none }` at (0,2,0) — deliberately, because on the desktop
     rail the flyout is shown and hidden with visibility/opacity and must not be display-toggled.
     But the mobile block then also forces `visibility: visible; opacity: 1`, so below 1200px
     BOTH of the properties that could hide the submenu were pinned open: tapping a group
     flipped `.show` and `aria-expanded` and moved nothing on screen, with a chevron still
     promising it would collapse. Adding `.collapse` and `:not(.show)` takes this to (0,6,0) so
     the collapsed state wins again, while `.collapsing` — which carries neither class mid-
     animation — keeps `display: block` from the base rule and animates normally now that its
     `height: auto !important` is fenced to desktop. */
  :root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu.collapse:not(.show) { display: none; }
  :root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu::before { display: none; }
  :root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu .s-sidebar-link { color: var(--bw-chrome-muted); }
  :root[data-theme-version="v1"] .s-sidebar-menu .s-sidebar-menu .s-sidebar-link:hover {
    background-color: var(--bw-chrome-hover);
    color: var(--bw-chrome-text);
  }
  :root[data-theme-version="v1"] .s-sidebar-pane > .s-sidebar-menu > .s-sidebar-item:not(.has-children) > .s-sidebar-link::after { display: none; }
  :root[data-theme-version] .s-sidebar-search { max-width: none; }
  :root[data-theme-version] .m-topbar-heading { display: none; }
}
@media (max-width: 767.98px) {
  :root[data-theme-version] .s-sidebar-search { display: none; }
  /* ⚠ `.modal-dialog-centered` MUST be listed here too, and that is the whole point of the
     second line. Serenity puts `modal-dialog-centered` on every dialog, and in a selector
     LIST each selector carries its own specificity — so the un-mediated base rule above
     matches via `.modal-dialog.modal-dialog-centered` at (0,4,0) while this one matched only
     `.modal-dialog` at (0,3,0). Specificity beats both source order and the media query, so
     for a long time this rule was DEAD CODE: v2's drawer stayed at its 90vw desktop width on
     a phone, leaving a useless sliver of page beside it, and nothing looked wrong in the
     stylesheet. Mirror the base rule's selector list whenever overriding it. */
  :root[data-theme-version="v2"] .modal-dialog,
  :root[data-theme-version="v2"] .modal-dialog.modal-dialog-centered { width: 100vw; max-width: 100vw; }

  /* ---- the form grid has to collapse, or the fields are unusable ----
     --bw-form-cols is a fixed 3 (v1) / 2 (v2) with no breakpoint, so a create dialog kept
     three columns all the way down: at a 568px viewport v1's fields measured 147px each,
     narrow enough that a date field's calendar glyph, a select2 and a text input all had to
     share it. Overridden on the dialog body rather than :root because that is where the
     dialog already re-points --bw-control-h at --bw-form-h, and because custom properties
     inherit — .categories resolves var(--bw-form-cols) at ITS OWN element, so an ancestor
     override reaches it without touching the grid-template-columns rule itself. */
  :root[data-theme-version] :is(.modal-body, .s-DialogContent) { --bw-form-cols: 2; }
}
@media (max-width: 575.98px) {
  /* One column below Bootstrap's sm boundary — two 250px fields do not fit a phone. */
  :root[data-theme-version] :is(.modal-body, .s-DialogContent) { --bw-form-cols: 1; }
}

/* ============================================================================
   7. AUTH SCREENS — the one place markup was unavoidable.
   Both designs specify the same two-column sign-in: a dark brand panel beside a
   plain form column, no card and no shadow. A brand panel next to a form is
   structure, not styling, so it lives in Views/Shared/_AuthBrandPanel.cshtml plus
   the AuthBrand branch of _LayoutNoNavigation.cshtml; only the --bw-auth-* tokens
   differ per version.
   ============================================================================ */
:root[data-theme-version] body.no-navigation { background-color: var(--bw-surface); }

/* ⚠ `height`, not just `min-height`. With only a min-height the grid rows grow to their
   content, so a tall form (sign-up has five fields) scrolls the whole PAGE — and the brand
   panel scrolls away with it, taking the wordmark off screen. Pinning the shell to the
   viewport makes each column exactly 100vh, so the two scroll independently and the
   lockup stays put however long the form is. */
:root[data-theme-version] .m-auth {
  display: grid;
  grid-template-columns: var(--bw-auth-cols);
  height: 100vh;
  min-height: 100vh;
}

/* The narrow-viewport collapse for these screens is NOT here — it has to come after the
   .m-auth-brand base rule below, or its `display: none` loses on source order. See the
   "auth: narrow viewports" block at the end of this section. */

/* ---- auth: brand panel ---- */
:root[data-theme-version] .m-auth-brand {
  background-color: var(--bw-auth-brand-bg);
  color: var(--bw-auth-brand-fg);
  display: flex;
  flex-direction: column;
  gap: 40px;
  justify-content: space-between;
  overflow-y: auto;
  padding: var(--bw-auth-brand-pad);
}
:root[data-theme-version] .m-auth-lockup {
  align-items: center;
  display: flex;
  flex-shrink: 0;
  gap: 12px;
}
:root[data-theme-version] .m-auth-mark {
  align-items: center;
  background-color: var(--bw-auth-mark-bg);
  border-radius: var(--bw-auth-mark-radius);
  color: var(--bw-auth-mark-fg);
  display: flex;
  flex-shrink: 0;
  font-family: var(--bw-font-mono);
  font-size: 15px;
  font-weight: 500;
  height: var(--bw-auth-mark-size);
  justify-content: center;
  width: var(--bw-auth-mark-size);
}
:root[data-theme-version] .m-auth-sitename { font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
:root[data-theme-version] .m-auth-tagline {
  color: var(--bw-auth-brand-lede);
  font-size: 10.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
}
:root[data-theme-version] .m-auth-pitch {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  gap: 28px;
  max-width: 440px;
}
:root[data-theme-version] .m-auth-headline {
  color: inherit;                   /* the heading rule in section 5 would otherwise
                                       paint this --bw-text-strong, which is a dark ink
                                       on an equally dark brand panel */
  font-size: var(--bw-auth-title-size);
  font-weight: 600;
  letter-spacing: var(--bw-auth-title-spacing);
  line-height: 1.12;
  margin: 0;
  text-wrap: pretty;
}
:root[data-theme-version] .m-auth-lede {
  color: var(--bw-auth-brand-lede);
  font-size: 15px;
  line-height: 1.6;
  margin: 0;
}
:root[data-theme-version] .m-auth-points {
  border-top: 1px solid var(--bw-auth-brand-rule);
  display: flex;
  flex-direction: column;
  gap: 14px;
  list-style: none;
  margin: 0;
  padding: 24px 0 0;
}
:root[data-theme-version] .m-auth-points li {
  align-items: baseline;
  color: var(--bw-auth-brand-item);
  display: flex;
  font-size: 13.5px;
  gap: 10px;
}
:root[data-theme-version] .m-auth-point-num {
  color: var(--bw-auth-num);
  font-family: var(--bw-font-mono);
  font-size: 12px;
}
:root[data-theme-version] .m-auth-legal { color: var(--bw-auth-brand-foot); flex-shrink: 0; font-size: 12px; }

/* The two designs disagree on the headline wording and on whether there is a
   supporting paragraph, so both are in the markup and CSS picks one. */
:root[data-theme-version="v1"] .m-auth-only-v2,
:root[data-theme-version="v2"] .m-auth-only-v1 { display: none; }

/* ---- auth: form column ---- */
/* ⚠ `safe center`, not `center` — this is the classic flex-centring-in-a-scroller trap and
   it is exactly what made SIGN-UP's heading collide with the top edge. With plain `center`,
   a form taller than the column is centred, so its top half overflows ABOVE the scroll
   origin, where nothing can reach it: `scrollTop` cannot go below 0. Sign-in has 2 fields and
   always fit, so it never showed; sign-up has 5 plus a confirm-email, and at a 651px viewport
   its "Create a new account" heading rendered at y=33 — above this rule's own 48px padding —
   with the text jammed into the top edge and unreachable by scrolling.
   `safe` degrades the alignment to `flex-start` exactly when the content would overflow, so
   the form stays centred while it fits and becomes a normal top-anchored scroll when it does
   not. Chrome supports it (verified via CSS.supports); older engines ignore `safe` and fall
   back to today's behaviour, so it is safe to ship unprefixed. */
:root[data-theme-version] .m-auth-main {
  align-items: safe center;
  background-color: var(--bw-surface);
  display: flex;
  justify-content: center;
  overflow-y: auto;
  padding: 48px;
  position: relative;                 /* anchors the theme cycler below */
}
/* The sign-in screen's theme control: one round button that cycles light → dark → auto,
   because there is no profile menu here to hold the segmented version. Absolutely
   positioned so it never enters the form's flow — the form column is a centred flex box
   and an extra child would shift the card off centre. */
:root[data-theme-version] .m-auth-theme {
  align-items: center;
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border-strong);
  border-radius: 50%;
  color: var(--bw-muted);
  cursor: pointer;
  display: flex;
  height: 36px;
  justify-content: center;
  position: absolute;
  right: 20px;
  top: 16px;
  width: 36px;
  z-index: 2;
}
:root[data-theme-version] .m-auth-theme:hover {
  border-color: var(--bw-primary);
  color: var(--bw-link);
}
:root[data-theme-version] .m-auth-theme > i { font-size: 15px; }
/* Serene's page wrappers assume they own the viewport; inside the right-hand
   column they must not. */
:root[data-theme-version] .m-auth-main .s-full-page {
  display: block;
  min-height: 0;
  width: 100%;
}
:root[data-theme-version] .m-auth-main .s-container-tight,
:root[data-theme-version] .m-auth-main .s-container-narrow {
  background: none;
  border: 0;
  box-shadow: none;                 /* no card: the split screen already separates
                                       form from brand */
  margin: 0 auto;
  max-width: var(--bw-auth-form-w);
  width: 100%;
}
:root[data-theme-version] .m-auth-form { display: flex; flex-direction: column; gap: var(--bw-auth-form-gap); }
:root[data-theme-version] .m-auth-form-head { display: flex; flex-direction: column; gap: 6px; }
:root[data-theme-version] .m-auth-form-title {
  color: var(--bw-text-strong);
  font-size: 26px;
  font-weight: 600;
  letter-spacing: -.02em;
  margin: 0;
}
:root[data-theme-version] .m-auth-form-sub { color: var(--bw-muted); font-size: 13px; margin: 0; }
/* Serenity's property grid lays a field out horizontally: a fixed 150px caption
   column, right-aligned, with the editor beside it. Both handoffs stack the label
   above a full-width input on the sign-in screen, so the field is re-flowed here
   rather than by asking the panel for a different form mode — the login form is
   generated from LoginForm.cs and this keeps that generation untouched. */
:root[data-theme-version] .m-auth-main .s-PropertyGrid .field {
  flex-wrap: wrap;
  padding: 0;
}
:root[data-theme-version] .m-auth-main .s-PropertyGrid .categories > .category > .field + .field { margin-top: 16px; }
:root[data-theme-version] .m-auth-main .s-PropertyGrid .caption {
  color: var(--bw-label-color);
  display: flex;
  flex: 0 0 100%;
  font-size: var(--bw-label-size);
  font-weight: var(--bw-label-weight);
  letter-spacing: var(--bw-label-spacing);
  line-height: 1.4;
  margin: 0 0 6px;
  text-align: left;
  width: auto;
}
/* The asterisk leads the caption in Serene's markup; both designs put it after
   the word, so it is reordered rather than hidden. */
:root[data-theme-version] .m-auth-main .s-PropertyGrid .caption sup { order: 2; padding-left: 3px; }
:root[data-theme-version] .m-auth-main .s-PropertyGrid .editor,
:root[data-theme-version] .m-auth-main .s-PropertyGrid .widget-wrapper { flex: 1 1 100%; width: 100%; }
:root[data-theme-version] .m-auth-main .s-PropertyGrid input.editor {
  font-size: 14px;
  height: var(--bw-auth-control-h);
}
:root[data-theme-version] .m-auth-form-aside { display: flex; font-size: var(--bw-text-sm); justify-content: flex-end; }
:root[data-theme-version] .m-auth-submit {
  font-size: 14px;
  font-weight: 600;
  height: var(--bw-auth-btn-h);
  letter-spacing: .01em;
  margin-top: 18px;
  width: 100%;
}
:root[data-theme-version] .m-auth-form-foot {
  border-top: 1px solid var(--bw-border);
  color: var(--bw-faint);
  font-size: var(--bw-text-sm);
  line-height: 1.6;
  padding-top: 18px;
}

/* ---- auth: narrow viewports ----
   Below the brand column's own comfortable width the panel stops being useful and starts
   squeezing the form, so it drops out entirely rather than shrinking.

   ⚠ THIS BLOCK MUST STAY AFTER the `.m-auth-brand` base rule above. It used to sit before
   it, where `display: none` collided with that rule's `display: flex` at the SAME
   specificity — (0,3,0) each, both `:root[data-theme-version] .m-auth-brand` — so source
   order decided and the base rule won. The media query was dead code, and the symptom was
   not a visible brand panel but a CLIPPED one: `.m-auth` is pinned to `height: 100vh` (so
   the desktop lockup cannot scroll away), and once collapsed to one column those 100vh were
   split between two stacked rows, leaving the panel 328px for 524px of content. The missing
   196px — the whole feature list — was reachable only by scrolling INSIDE the panel, because
   the page itself could not scroll at all. A media query with no `!important` and no extra
   class is only as strong as its position in the file. */
@media (max-width: 900px) {
  :root[data-theme-version] .m-auth {
    grid-template-columns: 1fr;
    /* Release the viewport pin along with the panel: with one column the shell must be able
       to grow, or a tall form (sign-up has five fields) is clipped the same way the brand
       panel was. min-height keeps it filling a short page. */
    height: auto;
    min-height: 100vh;
  }
  :root[data-theme-version] .m-auth-brand { display: none; }
}

/* ---- the password-action pages: Change / Forgot / Reset / Set ----
   Rendered by AccountPasswordActionsPageBase from Serenity.Extensions, whose Razor views are
   COMPILED INTO THE PACKAGE — there is no .cshtml here to edit. They also do not set
   ViewData["AuthBrand"], so they never enter .m-auth-main and therefore received NONE of the
   auth form treatment above. That is why they arrived as untouched stock Serene: a Serenity
   swirl logo belonging to no BWDesignSystem design, a plain bordered card, and the property grid's
   DEFAULT horizontal layout — a fixed 150px right-aligned caption column beside a
   shrink-wrapped ~170px input, with the required asterisk leading the label.

   ⚠ Matched on [class*="PasswordPanel"] rather than the four literal names
   (s-ChangePasswordPanel / s-ForgotPasswordPanel / s-ResetPasswordPanel / s-SetPasswordPanel)
   so a page added by a future package version is covered without another edit here.

   ⚠ No !important anywhere below, deliberately. Bootstrap 5 compiles its utilities WITH
   !important, and these views lean on them (.text-center, .p-4, .mb-4, .w-100). Rather than
   fight each one, this block only sets what the utilities do not: colour, type, borders,
   radius and the field layout. The centred title and the utility paddings are left exactly as
   the package emits them — a centred card is a legitimate BWDesignSystem shape, and every
   !important avoided here is one that cannot rot later. */
/* ⚠ THESE PAGES COME IN TWO SHAPES, and the difference is not visible in any .cshtml —
   each page's pageInit assigns the wrapper class itself, in the package's own JS
   (staticwebassets/esm/Modules/Membership/PasswordActions/*.js):

     A. standalone, its own page   — Forgot, Reset  → `s-full-page justify-content-center`
                                                      title is an <h5> plus a <p>
     B. inside the main app layout — Change, Set    → `s-container-tight mt-5`
                                                      title is an <h3 class="page-title">

   So only shape A may be given a full-page shell. An earlier version of this block applied
   the shell to everything and would have stretched CHANGE PASSWORD's narrow container to
   100vh and painted the canvas colour inside the app layout, beside the real sidebar. Shape B
   is already positioned by the layout and needs the card treatment only. */
:root[data-theme-version] [class*="PasswordPanel"].s-full-page {
  background-color: var(--bw-canvas);
  /* Row direction on purpose: vertical centring then lands on align-items, which is ours,
     while horizontal centring stays with the .justify-content-center utility the markup
     already carries — so nothing has to be overridden. `safe` for the same reason as
     .m-auth-main above: a tall form must top-anchor instead of losing its heading. */
  align-items: safe center;
  display: flex;
  min-height: 100vh;
  padding: 40px 20px;
}
/* In shape A the container is a CHILD of the panel; in shape B the panel IS the container. */
:root[data-theme-version] [class*="PasswordPanel"]:is(.s-container-tight, .s-container-narrow),
:root[data-theme-version] [class*="PasswordPanel"] :is(.s-container-tight, .s-container-narrow) {
  max-width: var(--bw-auth-form-w);
  width: 100%;
}
/* Shape A's Razor view opens with `<h2><img class="s-site-logo-img"> BWDesignSystem</h2>` — a
   Serenity swirl belonging to no BWDesignSystem design. Hiding the image leaves the wordmark, which
   is the half that belongs to this app. Shape B has no such header, so this simply idles. */
:root[data-theme-version] [class*="PasswordPanel"] .s-site-logo-img { display: none; }
:root[data-theme-version] [class*="PasswordPanel"] h2 {
  color: var(--bw-text-strong);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -.01em;
}
:root[data-theme-version] [class*="PasswordPanel"] .s-Panel {
  background-color: var(--bw-surface);
  border: 1px solid var(--bw-border);
  border-radius: var(--bw-radius-lg);
  box-shadow: var(--bw-shadow);
  /* ⚠ The two shapes do not agree on padding, and shape B has NONE of its own.
     Shape A's Razor view emits `<div class="s-Panel p-4">`, so Bootstrap's utility padded it;
     shape B's `renderContents()` emits a bare `class="s-Panel"`. Leaving padding to the
     utilities — the original reasoning, to avoid fighting Bootstrap's `!important` — therefore
     left CHANGE PASSWORD with `padding: 0`: the title sat on the card's top edge and all three
     inputs ran flush to its sides, 1px of border the only gap. Declaring it here fixes shape B
     and is a no-op for shape A, because `.p-4` carries `!important` and still wins. That is the
     rare case where the utilities' `!important` is useful rather than an obstacle — no override
     needed in either direction. */
  padding: var(--bw-pad-lg);
}
/* h5 is shape A's title, h3.page-title is shape B's — both get the same treatment so the
   two families read as one screen. */
:root[data-theme-version] [class*="PasswordPanel"] .s-Panel > :is(h5, h3) {
  color: var(--bw-text-strong);
  font-size: 18px;
  font-weight: 600;
}
:root[data-theme-version] [class*="PasswordPanel"] .s-Panel > p {
  color: var(--bw-muted);
  font-size: var(--bw-text-sm);
}
/* The fields — the actual "text boxes" complaint. Same stacked shape the auth screens use:
   label on its own line above a full-width control at the auth control height. */
:root[data-theme-version] [class*="PasswordPanel"] .s-PropertyGrid .field {
  flex-wrap: wrap;
  padding: 0;
}
:root[data-theme-version] [class*="PasswordPanel"] .s-PropertyGrid .categories > .category > .field + .field { margin-top: 16px; }
:root[data-theme-version] [class*="PasswordPanel"] .s-PropertyGrid .caption {
  color: var(--bw-label-color);
  display: flex;
  flex: 0 0 100%;
  font-size: var(--bw-label-size);
  font-weight: var(--bw-label-weight);
  letter-spacing: var(--bw-label-spacing);
  line-height: 1.4;
  margin: 0 0 6px;
  text-align: left;
  width: auto;
}
/* The asterisk leads the caption in Serene's markup; both designs put it after the text. */
:root[data-theme-version] [class*="PasswordPanel"] .s-PropertyGrid .caption sup { order: 2; padding-left: 3px; }
:root[data-theme-version] [class*="PasswordPanel"] .s-PropertyGrid :is(.editor, .widget-wrapper) {
  flex: 1 1 100%;
  width: 100%;
}
:root[data-theme-version] [class*="PasswordPanel"] .s-PropertyGrid input.editor {
  font-size: 14px;
  height: var(--bw-auth-control-h);
}
/* The submit already carries .w-100, so only its height and weight are missing. */
:root[data-theme-version] [class*="PasswordPanel"] .s-Panel button[type="submit"] {
  font-size: 14px;
  font-weight: 600;
  height: var(--bw-auth-btn-h);
  margin-top: 18px;
}
/* ⚠ Shape B wraps its submit in `<div class="px-field mt-4">`, whose 10.125px side padding made
   the full-width button 20px NARROWER than the fields above it and inset from both their edges —
   subtle but visibly out of line, since everything else in the card shares one left edge. The
   padding is Serenity's own utility for a form row, not something the button needs inside a card
   this narrow. `.px-field` is not one of Bootstrap's `!important` utilities, so a plain
   declaration is enough. */
:root[data-theme-version] [class*="PasswordPanel"] .s-Panel .px-field {
  padding-left: 0;
  padding-right: 0;
}

/* ============================================================================
   9. PRINT — the sheet the header's Print action builds.

   ⚠ Printing the app itself does not work, and this is not a styling opinion:
   SleekGrid is virtualized, so only the rows currently scrolled into view exist
   in the DOM. `window.print()` over a real grid prints twenty-odd rows and drops
   the rest, at inline pixel widths sized for a viewport. GridExport.ts therefore
   builds a plain table from the same snapshot the CSV export uses, and this
   section is what makes that table the only thing on the page.
   ============================================================================ */
:root[data-theme-version] .m-print-sheet { display: none; }

@media print {
  /* Tokens are re-pointed at a light, ink-on-paper set rather than the component
     rules being rewritten. That keeps the "nothing below the token blocks holds a
     literal" rule intact, and it means a dark-mode user gets a legible page
     instead of a black rectangle the printer would either dump ink into or drop
     entirely (browsers discard backgrounds by default, which would have left
     white text on white paper). */
  :root[data-theme-version] {
    --bw-surface:      #FFFFFF;
    --bw-canvas:       #FFFFFF;
    --bw-field:        #FFFFFF;
    --bw-text:         #000000;
    --bw-text-strong:  #000000;
    --bw-muted:        #333333;
    --bw-faint:        #444444;
    --bw-border:       #999999;
    --bw-border-strong:#666666;
    --bw-rule:         #CCCCCC;
    --bw-colhead-color:#000000;
  }

  /* Everything except the sheet. A blanket rule rather than a list of the app's
     own containers: modals, toasts, the flatpickr calendar and anything a future
     module appends to body are all covered without naming them. */
  :root[data-theme-version] body > *:not(.m-print-sheet) { display: none !important; }

  :root[data-theme-version] body {
    background: var(--bw-surface);
    color: var(--bw-text);
  }
  :root[data-theme-version] .m-print-sheet { display: block; }
  :root[data-theme-version] .m-print-head { margin-bottom: 12px; }
  :root[data-theme-version] .m-print-head h1 {
    font-size: 17pt;
    font-weight: 600;
    margin: 0 0 3px;
  }
  :root[data-theme-version] .m-print-head p {
    color: var(--bw-muted);
    font-family: var(--bw-font-mono);
    font-size: 8.5pt;
    margin: 0;
  }
  :root[data-theme-version] .m-print-sheet table {
    border-collapse: collapse;
    font-size: 8.5pt;
    width: 100%;
  }
  :root[data-theme-version] .m-print-sheet th {
    border-bottom: 1px solid var(--bw-border-strong);
    color: var(--bw-colhead-color);
    font-size: 7.5pt;
    font-weight: 600;
    letter-spacing: var(--bw-colhead-spacing);
    padding: 4px 6px;
    text-align: left;
    text-transform: uppercase;
  }
  :root[data-theme-version] .m-print-sheet td {
    border-bottom: 1px solid var(--bw-rule);
    padding: 4px 6px;
    vertical-align: top;
  }
  /* The head repeats on every sheet of a long table — a printed page 3 with no
     column names is unreadable. */
  :root[data-theme-version] .m-print-sheet thead { display: table-header-group; }
  :root[data-theme-version] .m-print-sheet tr { break-inside: avoid; }

  @page { margin: 14mm; }
}
