﻿/* =============================================================================
   SalesKor app shell.

   Hand-written plain CSS on purpose. css/app.css and app.min.css are Visual Studio
   Web Compiler output from the .scss sources, and `dotnet build` does not regenerate
   them - so anything written there has to be edited twice and reverts on the next
   compile in VS. This file has no build step and cannot drift.

   Loaded AFTER app.min.css so it wins over MDBootstrap, and BEFORE AppMain.styles.css
   so Blazor scoped component styles still win over it.

   Scope: the shell only - sidebar, top bar, page surface. Everything inside <main> is
   AppMain.Components markup and keeps its own Bootstrap/MDB styling. The package emits
   md-form and waves-effect, so MDB cannot be removed; see the plan.

   Palette: Apple's iOS system colours. Light is systemBackground #ffffff, dark is
   systemBackground #000000, and the greys between are the systemGray family. Only the neutrals
   are Apple's - the accent stays Korzite red, so this is an iOS *surface* treatment on a branded
   app, not an iOS clone. Everything downstream (Telerik's --kendo-color-*, the package's
   --appmain-*, MDB's --mdb-* aliases) reads the --sk-* tokens, so the palette lives in the two
   blocks below and nowhere else.
   ============================================================================= */

/* ---------- Inter ---------------------------------------------------------
   Self-hosted, SIL Open Font License (see fonts/inter/OFL.txt - shipping the licence is a
   condition of use).

   Why not San Francisco: Apple's font licence permits SF only for developing apps FOR Apple
   platforms, so it cannot be served to Windows browsers. Inter was designed in the same
   screen-UI lineage and is the closest thing that can be distributed legally. On Apple devices
   this now overrides SF deliberately - the point is that every machine renders identically, so
   a screenshot from a Mac matches one from Windows.

   Self-hosted rather than Google-hosted, so the CSP stays font-src 'self' - the whole reason
   the Google Fonts link was removed in the first place.

   Two subsets with unicode-range: the browser fetches latin (47 KB) for ordinary content and
   only pulls latin-ext (83 KB) if a character actually needs it. The SOW is English (CA) only,
   so latin-ext will rarely load - but a Polish or Turkish customer name will render correctly
   rather than falling back mid-word.                                                        */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;   /* variable: one file covers every weight */
    font-display: swap;
    src: url('/fonts/inter/inter-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                   U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                   U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/fonts/inter/inter-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
                   U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                   U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---------- tokens ------------------------------------------------------- */
:root {
    /* Neutral ramp - Apple's iOS system greys, not a generic slate.

       The point of the choice is the two ends: --sk-n0 is systemBackground in light mode and
       --sk-n900 is systemBackground in dark, so the app is pure #ffffff and true #000000 rather
       than an off-white and a blue-black. Everything between is the iOS systemGray family, which
       is a NEUTRAL ramp - the previous one was slate, and its blue cast is what made a "black"
       shell read as navy.

       n600 has no iOS counterpart. secondaryLabel is #3C3C43 at 60%, which resolves to about
       #8a8a8e on white - 3.0:1, under the 4.5:1 body text needs, and --sk-text-muted carries real
       content here (the email column, page subtitles, empty-state copy). #6c6c70 is the same
       family at 5.23:1. Same trade as --sk-border-input below: match iOS unless iOS fails WCAG. */
    --sk-n0:   #ffffff;   /* white / systemBackground (light)          */
    --sk-n50:  #f2f2f7;   /* systemGray6                               */
    --sk-n100: #e5e5ea;   /* systemGray5                               */
    --sk-n200: #d1d1d6;   /* systemGray4                               */
    --sk-n300: #c7c7cc;   /* systemGray3                               */
    --sk-n400: #aeaeb2;   /* systemGray2                               */
    --sk-n500: #8e8e93;   /* systemGray - the hinge, same in both modes */
    --sk-n600: #6c6c70;   /* accessible secondaryLabel - see above     */
    --sk-n700: #48484a;   /* systemGray3 (dark)                        */
    --sk-n800: #1c1c1e;   /* secondarySystemBackground (dark)          */
    --sk-n900: #000000;   /* systemBackground (dark)                   */

    /* brand - #b5170c is the Korzite primary. The MainLogo gradient reads from these two tokens
       (see .sk-logo-stop-* below), so the mark follows the brand rather than the other way round.
       --sk-brand-lite is the lightest point of that ramp and carries white avatar initials at
       .8rem, so it cannot go lighter than this: #d93a1e measures 4.59:1 against white and
       anything brighter drops under the 4.5:1 needed for small text. */
    --sk-brand-deep: #b5170c;
    --sk-brand-lite: #d93a1e;

    /* Logo gradient - the supplied artwork's own ramp, deliberately SEPARATE from --sk-brand-*.
       They cannot be shared: --sk-brand-lite also paints the avatar, which carries white initials
       at .8rem, and the logo's light end #ff1e27 measures 3.84:1 against white - under the 4.5:1
       small text needs. #d93a1e clears it at 4.59:1. Two ramps because they have two jobs. */
    --sk-logo-deep: #7d141d;
    --sk-logo-lite: #ff1e27;
    --sk-accent:     #b5170c;
    --sk-accent-cont:#ffffff;
    --sk-accent-soft:rgba(181, 23, 12, .10);

    /* THE BRAND AS A FILL, which in LIGHT MODE is the same colour and this pair changes nothing.
       It exists for dark mode, where one token provably cannot do both jobs:

         accent as TEXT on --sk-surface needs 4.5:1  ->  relative luminance >= 0.225
         a WHITE label on the accent needs 4.5:1     ->  relative luminance <= 0.183

       There is no colour in that band, so the foreground accent and the fill are two colours or
       one of them fails. Splitting them is what lets the primary BUTTON stay a brand red in dark
       mode while the active nav icon and the focus ring keep the lifted tone they need to be
       legible at all. */
    --sk-accent-fill:      var(--sk-accent);
    --sk-accent-fill-cont: var(--sk-accent-cont);
    /* The fill's own edge. Transparent here because a dark fill on a light page needs no help
       being seen; dark mode uses it to keep the boundary at 3:1 without touching the colour. */
    --sk-accent-fill-edge: transparent;

    /* danger - deliberately a DIFFERENT red from the brand.

       The previous error colour was #b91c1c, which measures dE(ab) 6.0 from #b5170c. Under about
       10 two colours are not reliably told apart at a glance, so a destructive action would have
       been indistinguishable from a primary one - a red Delete sitting next to a red Save. Rose
       is dE 31.8 away and still unmistakably an error red, mainly because it is cooler (hue 343
       against the brand's 4) as well as darker.

       Keep that separation in mind before changing either value: the brand may move, but these
       two have to stay apart. --sk-danger-border is decorative and measures 1.81:1, the same as
       the border it replaces - the state is carried by the icon and the text, both of which clear
       4.5:1, so WCAG 1.4.11 does not bind here (same reasoning as --sk-border-strong above). */
    --sk-danger:        #9f1239;
    --sk-danger-soft:   #fff1f2;
    --sk-danger-tint:   #ffe4e6;
    --sk-danger-border: #fda4af;
    /* On-danger, the pair to --sk-accent-cont, for the one place --sk-danger is a fill rather than
       ink: the rose outline button on hover. The label has to invert with the fill - white on
       #9f1239 is 8.04:1, black on the dark theme's #fda4af is 11.1:1 - and the rule used to
       hardcode #fff, which put white on a pale pink button the moment the theme flipped. */
    --sk-danger-cont:   #ffffff;

    /* warning - the environment tag and the Blazor error bar. Amber rather than a third red,
       so it stays distinct from both the brand and --sk-danger. */
    --sk-warn:      #92400e;
    --sk-warn-soft: #fef3c7;

    /* success - a settled, finished state (a redeemed invitation today). Teal rather than a green,
       because a green next to the amber warning above reads as a traffic light and invites the
       reading that pending is a problem. #0f766e is 5.31:1 on its own tint. */
    --sk-ok:      #0f766e;
    --sk-ok-soft: #ccfbf1;

    /* Segmented control. The pair simply swaps between themes, which is what keeps the selected
       segment LIGHTER than its track in both: on white the track sinks to systemGray6 and the
       pill is white, and on black the track lifts to #1c1c1e and the pill lifts again to #2c2c2e.
       Using --sk-surface for the pill in both themes would put a DARKER pill on a lighter track
       in dark mode, which reads as a hole rather than as the selected segment. */
    --sk-segment-track: var(--sk-surface-sunk);
    --sk-segment-on:    var(--sk-surface);

    /* Status pill fills - see .sk-chip--* for why these are the accessible variants of the iOS
       hues rather than the headline ones. Kept SEPARATE from --sk-ok / --sk-warn / --sk-danger
       above: those are text-and-tint colours for the error bar, validation messages and the
       environment tag, and a fill that carries white text is a different job. Collapsing the two
       sets would mean either unreadable pills or washed-out error text.

       White on each: green 4.51:1, orange 5.17:1, red 5.38:1, grey 5.23:1. */
    --sk-pill-green:  #22883d;
    --sk-pill-orange: #a85800;
    --sk-pill-red:    #d70015;
    --sk-pill-gray:   var(--sk-n600);
    --sk-pill-on:     #ffffff;

    /* Role tints. The three working roles used to share one neutral chip, so a Users list read as
       a column of identical grey pills and the role - the thing that column exists for - had to be
       read word by word.

       TINTS, not the solid fills above, and that distinction is the hierarchy: a status is an
       event (waiting, declined) and shouts; a role is an attribute and should be legible at a
       glance without competing. Making all five solid would have given every row three saturated
       pills and told you nothing about which mattered.

       Sales rep keeps the neutral chip on purpose - it is the common case, and colouring the
       default makes the exceptions quieter rather than louder. Indigo and blue are the iOS system
       hues, darkened in light mode to clear 4.5:1 on their own tint at .75rem, exactly as the pill
       fills are. Neither is the brand red: a role chip is not an action. */
    --sk-role-admin:        #4b48b8;
    --sk-role-admin-soft:   rgba(88, 86, 214, .12);
    --sk-role-manager:      #0a63c9;
    --sk-role-manager-soft: rgba(0, 122, 255, .12);

    /* Semantic surfaces.

       This is iOS's GROUPED style: --sk-canvas is systemGroupedBackground #f2f2f7 and the panels
       on it - cards, sidebar, top bar - are white. The plain style (canvas and surface both
       #ffffff, separated only by a hairline) was built first and reads flatter; the grouped one
       is what InvestAdvisor uses and what the shell is tuned for now. Reverting is one line:
       --sk-canvas: var(--sk-n0). */
    --sk-canvas:        var(--sk-n50);
    --sk-surface:       var(--sk-n0);
    --sk-surface-sunk:  var(--sk-n50);
    --sk-border:        var(--sk-n100);
    --sk-border-strong: var(--sk-n300);
    /* Form-field boundaries are UI components under WCAG 1.4.11 and need 3:1 against the field
       background - decorative borders do not, which is why this is a separate token rather than
       darkening --sk-border-strong everywhere. systemGray #8e8e93 is 3.28:1 on white and is the
       one grey iOS keeps identical in both modes, so this token no longer needs a dark override. */
    --sk-border-input:  var(--sk-n500);
    --sk-text:          var(--sk-n900);
    --sk-text-muted:    var(--sk-n600);
    --sk-text-invert:   var(--sk-n0);

    /* spacing - 4px base */
    --sk-1: .25rem;  --sk-2: .5rem;   --sk-3: .75rem;  --sk-4: 1rem;
    --sk-5: 1.25rem; --sk-6: 1.5rem;  --sk-8: 2rem;    --sk-10: 2.5rem;

    /* radius, elevation, motion */
    /* Curvature. 14px on cards is the generous iOS radius rather than the 8px Material one, and
       it is most of what reads as "modern" before a single colour changes. --sk-radius-sm is the
       input/menu step; --sk-radius-pill is the capsule, which every compact button takes.

       iOS uses a capsule for compact controls and a rounded rectangle for full-width ones. Every
       button in this app is compact - Apply, Cancel, Add holding, the state actions - so the
       capsule is right everywhere and there is no second case to handle. */
    --sk-radius:      14px;
    --sk-radius-sm:   10px;
    --sk-radius-pill: 999px;
    /* Neutral black, not the old slate rgba(15, 23, 42, ...) - a blue-tinted shadow is invisible
       on a white canvas and a visible cast on a black one. The second layer is slightly deeper
       than before because a card is now white on white, so the shadow carries separation the
       canvas used to provide. */
    --sk-shadow:    0 1px 2px rgba(0, 0, 0, .04), 0 1px 3px rgba(0, 0, 0, .08);
    /* ONE band, and it starts at the control's own edge.

       This used to be `0 0 0 2px var(--sk-surface), 0 0 0 4px var(--sk-accent)` - a surface-
       coloured gap and then a solid accent ring floating 2px off the control. The gap was there to
       stop the ring fusing with a coloured control underneath, and on a plain white background it
       is invisible, so the reasoning looked sound. It is not what you see: on any control that has
       a border of its own - every input, every ghost button, every Telerik picker - the eye reads
       the grey border, the white gap and the red ring as TWO borders on one field, one dark and
       one red. That is the single most-reported thing about this theme.

       So the ring now touches, and the control's own border goes to the accent at the same time
       (see the focus rule below), which fuses the two into one 3px red edge. Still solid accent -
       6.79:1 light, 6.64:1 dark - so WCAG 1.4.11's 3:1 for a focus indicator is unaffected; this
       is a geometry change, not a colour one.

       --sk-ring-inner is for the case the gap was protecting: a control already filled with the
       accent, where an accent ring around it just makes the button look 2px bigger. currentColor,
       not a token, so it resolves to whatever the label is wearing - white on the light theme's
       red fill, black on the dark theme's coral one - without a rule per variant. */
    --sk-ring:       0 0 0 2px var(--sk-accent);
    --sk-ring-inner: inset 0 0 0 2px currentColor;
    --sk-ease:      160ms cubic-bezier(.2, 0, .2, 1);
    /* Layout moves need longer than colour changes - 160ms reads as a jump at this distance. */
    --sk-ease-layout: 220ms cubic-bezier(.2, 0, .2, 1);

    /* ---------- Liquid Glass -------------------------------------------------
       Fixed chrome - sidebar, top bar, the filter drawer - is translucent and blurred rather
       than a flat panel, which is the macOS/iOS sidebar material. Content surfaces (cards,
       tables) stay opaque: dense reading content behind glass is just harder to read.

       Two declarations per surface, not one. `background-color` is set twice, plain first and
       color-mix() second, so a browser that cannot parse color-mix keeps the opaque value
       instead of dropping the declaration and rendering a transparent panel.

       The edge is what sells it: a bright inset top lip and a fainter bottom one, where light
       catches the sheet. Near-invisible on a white light-mode panel, clearly visible in dark. */
    --sk-glass-blur:   blur(40px) saturate(190%);
    --sk-glass-edge:   inset 0 1px 0 rgba(255, 255, 255, .28), inset 0 -1px 0 rgba(255, 255, 255, .06);
    --sk-glass-sheen:  linear-gradient(to bottom, rgba(255, 255, 255, .12), rgba(255, 255, 255, 0) 50%);
    --sk-glass-shadow: 0 12px 32px rgba(0, 0, 0, .16), 0 2px 8px rgba(0, 0, 0, .08);
    /* Rail and top bar sit against the page and can be thinner; the drawer carries form fields
       over live content and needs to stay readable, so it is nearer opaque. */
    --sk-glass-chrome: 78%;
    --sk-glass-panel:  84%;
    /* The continuous radius on a drawer or dialog sheet. Much larger than --sk-radius, which is
       for cards - this is the iOS sheet corner. */
    --sk-radius-sheet: 28px;

    /* shell metrics */
    --sk-sidebar:      256px;
    --sk-sidebar-rail:  68px;
    --sk-topbar:       56px;

    /* system stack - replaces the render-blocking Google Fonts request, which also
       let style-src/font-src tighten to 'self' in staticwebapp.config.json */
    /* Inter first for cross-platform consistency. The fallbacks matter for the swap window and
       for anyone blocking web fonts: system-ui resolves to the platform UI font directly (it was
       missing before), and "Segoe UI Variable Text" is Windows 11's newer UI face - both are a
       better landing spot than plain Segoe UI. */
    --sk-font: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Text',
               'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif,
               'Apple Color Emoji', 'Segoe UI Emoji';
}

/* Dark mode is opt-in only: light is the default regardless of the OS setting, and the account
   menu switches it. There is deliberately no prefers-color-scheme rule - with one, an OS-dark
   user would land in dark mode without choosing it.

   Note the scope: this themes the SHELL. AppMain.Components renders MDBootstrap markup, and the
   dark-mode patches further down are what stop it looking broken. */
:root[data-theme="dark"] {
    /* Three iOS backgrounds, back to front: systemBackground #000000 for the page, then
       secondarySystemBackground #1c1c1e for panels, then tertiarySystemBackground #2c2c2e for
       what sits on a panel. True black is the point of the palette - it is what makes this read
       as iOS rather than as a dark-grey admin theme, and on an OLED display it costs nothing.

       The separator and the grey below it are the iOS dark values rather than ramp entries: the
       ramp has to serve both modes and iOS's dark greys are not the reverse of its light ones. */
    --sk-canvas: var(--sk-n900);       --sk-surface: var(--sk-n800);
    --sk-surface-sunk: #2c2c2e;        --sk-border: #38383a;
    --sk-border-strong: var(--sk-n700); --sk-text: var(--sk-n0);
    --sk-text-muted: var(--sk-n400);   --sk-accent: #f77f6a;
    --sk-accent-soft: rgba(247, 127, 106, .14);
    /* The brand #b5170c cannot carry over into dark mode: against --sk-surface it measures 2.51:1,
       so active nav links and focus rings would both fail. #f77f6a is the same hue lifted to
       6.64:1 against the surface and 5.30:1 against --sk-accent-soft, which is what the active nav
       link actually sits on. Both improved when the surface went from slate #1e293b to #1c1c1e.

       On-accent has to flip with it. A light accent cannot carry white label text - white on
       #f77f6a is 2.56:1 - so the primary button's text goes black in dark mode, at 8.20:1. This
       is the standard light-accent/dark-on-accent pairing, not an inconsistency with the light
       theme, where the accent is dark and the label stays white. */
    --sk-accent-cont: var(--sk-n900);
    /* THE FILL DOES NOT FOLLOW THE ACCENT UP, and it is the BRAND COLOUR EXACTLY - the same
       #b5170c the light theme uses, which is the red in the SalesKor logo. Everything above is
       about the accent as a FOREGROUND - the active nav icon, the focus ring - where #f77f6a is
       the price of being legible at all. Charging that price to the primary BUTTON as well made
       every one of them a pale coral slab: the brand's own colour rendered as the one thing it
       is not, and visibly a different red from the same button in light mode.

       A white label on it is 6.79:1, so the text flips back to white and matches light mode.

       WHAT THE EDGE IS FOR. #b5170c against --sk-surface is 2.51:1, under the 3:1 that WCAG
       1.4.11 asks of the boundary that identifies a control. Rather than lift the colour until it
       passes - which is what produced the coral, and would produce a slightly-wrong red here too -
       the boundary is carried by the border .sk-btn already reserves as transparent: #d81c0f at
       3.32:1 against the surface. Against the fill it is 1.32:1, so it reads as a faint rim rather
       than an outline, and the button stays the brand's red at full strength.

       See the note on --sk-accent-fill in :root for why the fill and the foreground cannot be one
       token in this theme. */
    --sk-accent-fill: #b5170c;
    --sk-accent-fill-cont: #ffffff;
    --sk-accent-fill-edge: #d81c0f;
    /* Danger inverts the same way the accent does. #fda4af sits dE 28.1 from the dark accent
       #f77f6a, so the two stay apart in this theme as well. */
    --sk-danger: #fda4af;         --sk-danger-soft: #2b0f18;
    --sk-danger-tint: #4c0519;    --sk-danger-border: #881337;
    /* Same flip as --sk-accent-cont above, and for the same reason: a light fill cannot carry a
       white label. Black on #fda4af is 11.1:1. */
    --sk-danger-cont: var(--sk-n900);
    /* Warning inverts too, or the env tag and the Blazor error bar stay pale-yellow slabs in a
       dark shell. 9.61:1 on its own surface, and dE 69.9 from danger / 59.7 from the accent, so
       the three semantic colours remain unmistakable. */
    --sk-warn: #fcd34d;           --sk-warn-soft: #3a2a0a;
    /* Success inverts as well. #5eead4 is 11.2:1 on its own dark tint, and dE 47.1 from the dark
       warning, so pending and redeemed stay tellable apart in this theme too. */
    --sk-ok: #5eead4;             --sk-ok-soft: #0b3b36;
    /* Status pills invert to the bright iOS dark-mode system colours carrying BLACK labels.
       Black on each: green 10.39:1, orange 13.5:1, red 6.16:1, grey 6.44:1 - all clear, and the
       bright fills are legible against #1c1c1e in a way the darkened light-mode ones are not. */
    --sk-pill-green: #30d158;     --sk-pill-orange: #ff9f0a;
    --sk-pill-red:   #ff453a;     --sk-pill-gray:   var(--sk-n500);
    --sk-pill-on:    #000000;
    /* Role tints invert the same way the accent does: the bright system hue as the LABEL over a
       deepened tint of itself, rather than the light-mode pairing darkened - which on #1c1c1e
       reads as a smudge. Both clear 4.5:1 on their own tint. */
    --sk-role-admin:        #a5a3ff;
    --sk-role-admin-soft:   rgba(94, 92, 230, .22);
    --sk-role-manager:      #6cb6ff;
    --sk-role-manager-soft: rgba(10, 132, 255, .22);
    /* Swapped, not inherited - see the note on these in :root. */
    --sk-segment-track: var(--sk-surface);
    --sk-segment-on:    var(--sk-surface-sunk);
    /* No --sk-border-input override here any more, and that is the palette doing its job rather
       than an omission: systemGray #8e8e93 is the one grey Apple keeps identical in both modes,
       and it clears WCAG 1.4.11 on each - 3.26:1 on the white field, 4.28:1 on the #2c2c2e one.
       The #48484a used for decorative borders is 1.53:1 against that field and would fail, which
       is why the two tokens stay separate. */
}

/* ---------- base --------------------------------------------------------- */

/* No `scrollbar-gutter: stable` here, and that is a decision rather than an omission.

   It was added to stop the layout shifting when a modal opens, on the reading that Bootstrap's
   `body.modal-open { overflow: hidden }` plus its JS paddingRight were two competing shifts. That
   was wrong: the paddingRight IS Bootstrap's compensation for hiding the scrollbar, and the two
   cancel. Nothing needed fixing.

   What the gutter did instead was reserve ~15px on the html element permanently, so the body -
   and with it the whole .sk-shell grid, top bar included - stopped short of the right edge on
   every page, leaving a dead strip beside the account menu.

   The "whole app refreshed" feeling it was chasing had two real causes, both since fixed at
   source: AppButton's hard-coded 500ms spinner delay (now AppButton.SubmitDelayMs, passed as 0 by
   the filter drawer) and backdrop-filter on the full-height sidebar forcing a viewport repaint on
   every backdrop change (now applied only to the mobile overlay). */
body.sk-app {
    background: var(--sk-canvas);
    color: var(--sk-text);
    font-family: var(--sk-font);
    /* MDB sets body{font-family:Roboto;font-weight:300}. The family is overridden by this rule's
       higher specificity, but the WEIGHT was inherited everywhere - Inter at 300 reads as thin
       and washed out on Windows. 400 is the correct body weight. */
    font-weight: 400;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

/* Accessible skip link - the shell had no keyboard entry point at all before. */
.sk-skip {
    position: absolute; left: var(--sk-2); top: -3rem; z-index: 100;
    padding: var(--sk-2) var(--sk-4);
    background: var(--sk-surface); color: var(--sk-text);
    border: 1px solid var(--sk-border); border-radius: var(--sk-radius-sm);
    transition: top var(--sk-ease);
}
.sk-skip:focus { top: var(--sk-2); }

/* Scoped to things a user actually operates, NOT to `*`.

   The universal version put the ring on any element that happened to take focus, including
   containers that are only focusable because something focused them programmatically - which is
   how a red rounded rectangle ended up drawn around an entire card. [tabindex="-1"] is excluded
   for exactly that reason: it means "focusable by script, not by tab", and script moving focus to
   a wrapper is not the user navigating to it. */
.sk-app a[href]:focus-visible,
.sk-app button:focus-visible,
.sk-app input:focus-visible,
.sk-app select:focus-visible,
.sk-app textarea:focus-visible,
.sk-app summary:focus-visible,
.sk-app [role="button"]:focus-visible,
/* Scrolling CONTAINERS are excluded, not just [tabindex="-1"] elements.
   Telerik gives .k-tabstrip-content `tabindex="0"` and `role="tabpanel"` so the panel can be
   reached by keyboard - which is correct behaviour, but it means the blanket rule below drew the
   ring around the entire tab panel, a red rectangle enclosing the whole card. That is the same
   failure the [tabindex="-1"] exclusion was written for; it just did not go far enough, because
   this container is genuinely tabbable rather than only script-focusable. */
.sk-app [tabindex]:not([tabindex="-1"]):not([role="tabpanel"]):not(.k-tabstrip-content):focus-visible {
    outline: 2px solid transparent;
    box-shadow: var(--sk-ring);
    /* The other half of the one-edge rule in --sk-ring. Without this the control keeps its own
       border colour and the ring is a second line beside it; with it, border and ring are the same
       red and read as one thicker edge. Harmless on the borderless cases - a link or a filled
       button whose border is already `transparent` or the accent itself. */
    border-color: var(--sk-accent);
}

/* NO border-radius here, deliberately.
   This rule used to force `border-radius: var(--sk-radius-sm)` onto whatever had focus, so the
   ring's corners never matched the control's own. That was survivable at 6px; it broke visibly
   once buttons became capsules and the token moved to 10px - a pill button drew a 10px rounded
   rectangle around itself. Letting each element keep its own radius is the only thing that scales
   across pills, 10px inputs and square cells. */

/* Telerik draws its own focus state, on the WRAPPER rather than the element that has focus. Left
   alone, the blanket ring above lands on the inner <input> or button and you get two rings at
   different radii - which on a textbox reads as a stray coloured arc down one side, because the
   inner element is square and the wrapper is rounded.

   These opt those elements out and let Kendo's own .k-focus do the job. It is not a downgrade:
   the Kendo focus shadow resolves through --kendo-color-primary, which the block at the bottom of
   this file points at --sk-accent, so it is the same brand colour with the right geometry.      */
.sk-app .k-input-inner:focus-visible,
.sk-app .k-input:focus-visible,
.sk-app .k-picker:focus-visible,
.sk-app .k-button:focus-visible,
.sk-app .k-link:focus-visible,
.sk-app .k-list-item:focus-visible,
.sk-app .k-chip:focus-visible,
/* ANYTHING focusable inside a Telerik input wrapper, not just the classes listed above. The old
   list missed the suffix controls a widget renders beside the text - the clear button on a
   TextBox, the spinners on a numeric, the toggle on a picker - and each of those drew its own
   ring inside a wrapper that clips to a rounded rect, which is what shows as a red arc hugging
   one end of the field. */
.sk-app .k-input :focus-visible,
.sk-app .k-picker :focus-visible {
    box-shadow: none;
    outline: none;
    /* Also reset the radius. The blanket rule above sets border-radius as well as the ring, and
       the previous version of this opt-out cancelled only the ring - so a focused inner element
       kept a 10px radius it has no business having inside a wrapper of a different shape. */
    border-radius: inherit;
}

/* The opt-out above, again, for the elements it could not actually reach - and this is the rule
   that fixes the ringed dropdown rather than the geometry change in --sk-ring.

   A Telerik picker is a `<span class="k-picker" tabindex="0">`, so it matches the [tabindex] rule
   as well as the opt-out - and that one scores (0,6,0) against the opt-out's (0,3,0), because each
   :not() argument counts towards specificity. The opt-out simply lost. A focused dropdown drew the
   accent border from the .k-focus block below AND the full ring on top of it: dark border, white
   gap, red line. The same applies to any chip or clear button inside a widget that carries its own
   tabindex.

   No selector can out-specify that rule without hard-coding the Kendo class list into it, so this
   takes !important instead. Deliberately NOT extended to .k-button and .k-link: those are real
   <button>/<a> elements, the blanket rules for them are only (0,3,0), and the opt-out already beats
   those on source order. Silencing them here with !important would take Kendo's own .k-focus
   indicator off the pager buttons with it. */
.sk-app .k-input:focus-visible,
.sk-app .k-picker:focus-visible,
.sk-app .k-input [tabindex]:focus-visible,
.sk-app .k-picker [tabindex]:focus-visible {
    box-shadow: none !important;
    outline: none !important;
}

/* No ring on Telerik fields. The focus state is the field's own border going to the accent.

   The ring went through three versions before landing here, and the reason is worth keeping:

     :focus-within   - fired on a mouse click, so opening a dropdown ringed it red.
     :has(:focus-visible) - fixed the dropdowns and did nothing for the MultiSelect, because
                       :focus-visible always matches a text-entry control. Browsers assume anything
                       you can type into deserves the indicator whether you clicked or tabbed, so
                       there is no selector that distinguishes the two cases for a textbox.

   So the ring is gone and the indicator is a border colour change instead - subtler, never
   clipped, and the same thing Telerik does natively (.k-picker-solid.k-focus sets border-color
   to --kendo-color-primary). It still satisfies WCAG 2.4.7's "visible focus indicator", which is
   why this is a swap rather than a deletion.

   !important because the border-color reset for the Material underline further down carries one,
   and without it that rule would hold the border grey through focus. */
.sk-app .k-input.k-focus,
.sk-app .k-picker.k-focus,
.sk-app .k-input:focus-within,
.sk-app .k-picker:focus-within {
    border-color: var(--sk-accent) !important;
}

/* Keeps a real h1 in the document for screen readers and FocusOnNavigate without drawing it. */
.sk-visually-hidden {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ---------- shell grid --------------------------------------------------- */
/* The rail width is driven by data-nav on <html>, which js/env.js sets before first paint.
   Deliberately NOT a class rendered by Blazor: WASM boots after the browser paints, so a
   C#-driven class makes the sidebar snap from 256px to 68px on every load. */
.sk-shell {
    display: grid;
    /* auto, so the column simply follows the sidebar's own width.
       grid-template-columns cannot be transitioned here - per spec it only interpolates when
       the differing tracks are <length>/<percentage>/calc(), and this list contains 1fr, a
       <flex>. Animating the aside's width instead sidesteps that entirely: width is
       universally animatable and needs no @property registration. */
    grid-template-columns: auto 1fr;
    grid-template-rows: var(--sk-topbar) 1fr;
    grid-template-areas: "aside header" "aside main";
    min-height: 100vh;
}

/* ---------- sidebar ------------------------------------------------------ */
.sk-aside {
    grid-area: aside;
    width: var(--sk-sidebar);
    transition: width var(--sk-ease-layout);
    display: flex; flex-direction: column;
    /* Opaque on desktop, glass only as an overlay - see the media query at the foot of this file.

       The rail has its own grid column here, so nothing ever scrolls behind it and the blur had
       nothing to work on: it resolved to the flat canvas colour and looked identical to a solid
       panel. What it did cost was real. A full-height backdrop-filter forces the browser to
       re-composite everything behind it whenever the backdrop changes, so every modal open and
       close repainted the entire viewport through it. Paying that for an effect nobody can see is
       the wrong trade; the top bar keeps its glass because content genuinely does scroll under
       that one. */
    background: var(--sk-surface);
    box-shadow: var(--sk-glass-edge);
    border-right: 1px solid var(--sk-border);
    position: sticky; top: 0; height: 100vh;
    overflow: hidden auto;
    overscroll-behavior: contain;
}
.sk-aside__brand {
    display: flex; align-items: center; gap: var(--sk-2);
    height: var(--sk-topbar);
    padding: 0 var(--sk-4);
    border-bottom: 1px solid var(--sk-border);
    background: none; border-left: 0; border-right: 0; border-top: 0;
    width: 100%; cursor: pointer;
    /* MainLogo's wordmark is fill="currentColor", so it follows the theme in light and dark. */
    color: var(--sk-text);
    font-family: var(--sk-font);
}
.sk-aside__brand:hover { background: var(--sk-surface-sunk); }
.sk-aside__nav {
    padding: var(--sk-3) var(--sk-2);
    display: flex; flex-direction: column; gap: 2px;
    flex: 1 1 auto;              /* pushes the collapse control to the foot */
}
/* The drawer runs the full height of the screen, so its foot needs to clear the home indicator
   or the collapse control sits under it. 0 on anything without one. */
.sk-asidetoggle { margin-bottom: calc(var(--sk-2) + env(safe-area-inset-bottom)); }

:root[data-nav="rail"] .sk-aside { width: var(--sk-sidebar-rail); }

/* ---------- rail (collapsed) --------------------------------------------
   Labels COLLAPSE rather than disappear. The previous version swapped them to a
   visually-hidden box, which made the text vanish in a single frame while the sidebar was
   still moving - the jump you see as unpolished.

   Animating max-width and opacity means the text slides away with the panel and fades as it
   goes. opacity:0 keeps an element in the accessibility tree (unlike display:none or
   visibility:hidden), so every nav link keeps its accessible name - without one, the eleven
   nav items the SOW calls for would read as eleven unnamed links.                          */
.sk-nav__label {
    max-width: 12rem; opacity: 1;
    overflow: hidden; white-space: nowrap;
    transition: max-width var(--sk-ease-layout), opacity var(--sk-ease);
}
:root[data-nav="rail"] .sk-nav__label { max-width: 0; opacity: 0; }

/* the gap has to collapse too, or the icon sits off-centre in the rail */
.sk-nav__link { transition: background var(--sk-ease), color var(--sk-ease), gap var(--sk-ease-layout); }
:root[data-nav="rail"] .sk-nav__link { gap: 0; }
:root[data-nav="rail"] .sk-nav__link { justify-content: center; padding-inline: 0; }
:root[data-nav="rail"] .sk-aside__brand { justify-content: center; padding-inline: 0; }
:root[data-nav="rail"] .sk-asidetoggle__label { display: none; }
:root[data-nav="rail"] .sk-asidetoggle { justify-content: center; }

/* ---------- collapse control, pinned to the foot of the sidebar ---------- */
.sk-asidetoggle {
    display: flex; align-items: center; gap: var(--sk-3);
    margin: var(--sk-2);
    padding: var(--sk-2) var(--sk-3);
    border: 0; border-radius: var(--sk-radius-sm);
    background: none; color: var(--sk-text-muted);
    font-size: .8rem; font-weight: 500; cursor: pointer;
    transition: background var(--sk-ease), color var(--sk-ease);
}
.sk-asidetoggle:hover { background: var(--sk-surface-sunk); color: var(--sk-text); }
.sk-asidetoggle__icon { width: 1.25rem; text-align: center; flex: none; }

.sk-nav__link {
    display: flex; align-items: center; gap: var(--sk-3);
    padding: var(--sk-2) var(--sk-3);
    border-radius: 8px;
    color: var(--sk-text-muted);
    font-size: .9rem; font-weight: 500;
    text-decoration: none; white-space: nowrap;
    transition: background var(--sk-ease), color var(--sk-ease), padding-left var(--sk-ease);
}
.sk-nav__link:hover { background: var(--sk-surface-sunk); color: var(--sk-text); text-decoration: none; }

/* Active state: NEUTRAL fill, ACCENT icon - the macOS sidebar treatment (Mail, Notes, Reminders
   all do this), and a deliberate middle between the two versions that came before it.

   A full brand tint made the selected row a pale pink wash, which is the least modern thing the
   shell had. Pure monochrome - InvestAdvisor's choice - looks better but throws away hue as a
   scanning cue; that costs nothing at two nav items and quite a lot at the eleven the SOW calls
   for, where finding the row with a *slightly different grey* is real work. Keeping the fill
   neutral and moving the red onto the icon buys the calm surface and keeps the hue.

   InvestAdvisor can afford full monochrome because its content IS colour - green/red market data
   on every row - so a tinted nav would compete with meaning. SalesKor's content is names and
   emails; its only saturated colour is the status pills.

   Three cues, not one: fill, weight and icon colour. The weight is what survives for anyone who
   cannot separate the red from the grey, so the state is never carried by hue alone.

   The plain background is the fallback for browsers without color-mix - same two-declaration
   trick as the glass surfaces above. */
.sk-nav__link.active {
    background: var(--sk-surface-sunk);
    background: color-mix(in srgb, var(--sk-text) 8%, transparent);
    color: var(--sk-text);
    font-weight: 600;
}
.sk-nav__link.active .sk-nav__icon { color: var(--sk-accent); }

/* The label slides right a few px on hover. Cheap, and it is most of what makes the nav feel
   responsive rather than static. Reverted under prefers-reduced-motion further down. */
.sk-nav__link:hover { padding-left: calc(var(--sk-3) + 4px); }
:root[data-nav="rail"] .sk-nav__link:hover { padding-left: 0; }
.sk-nav__icon { width: 1.25rem; text-align: center; flex: none; font-size: 1rem; }

/* ---------- top bar ------------------------------------------------------ */
/* Sticky, so the page genuinely scrolls underneath it - this is the one piece of chrome where
   the blur has real content to work on. */
/* Safe-area insets here and on .sk-aside / .sk-main. env() resolves to 0 on anything without a
   notch or a home indicator, so these cost nothing on a desktop and are the difference between
   the top bar sitting under an iPhone's status bar or beside it. Written as max(), not a bare
   env(), so the existing padding is still the floor. */
.sk-topbar {
    grid-area: header;
    display: flex; align-items: center; gap: var(--sk-3);
    padding: 0 max(var(--sk-5), env(safe-area-inset-right)) 0 max(var(--sk-5), env(safe-area-inset-left));
    background-color: var(--sk-surface);
    background-color: color-mix(in srgb, var(--sk-surface) var(--sk-glass-chrome), transparent);
    background-image: var(--sk-glass-sheen);
    backdrop-filter: var(--sk-glass-blur);
    -webkit-backdrop-filter: var(--sk-glass-blur);
    box-shadow: var(--sk-glass-edge);
    border-bottom: 1px solid var(--sk-border);
    position: sticky; top: 0; z-index: 20;
}
.sk-iconbtn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 2rem; height: 2rem;
    border: 0; background: none; color: var(--sk-text-muted);
    border-radius: var(--sk-radius-sm); cursor: pointer;
    transition: background var(--sk-ease), color var(--sk-ease);
}
.sk-iconbtn:hover { background: var(--sk-surface-sunk); color: var(--sk-text); }
/* Mobile-only: above the breakpoint the sidebar is always open, so the control has nothing to do. */
.sk-navtoggle { display: none; }
.sk-topbar__spacer { margin-left: auto; }
.sk-envtag {
    font-size: .68rem; font-weight: 700; letter-spacing: .04em;
    padding: .2rem .5rem; border-radius: 999px;
    background: var(--sk-warn-soft); color: var(--sk-warn);
}

/* account control in the top bar */
.sk-app .sk-account {
    display: inline-flex; align-items: center; gap: var(--sk-2);
    padding: var(--sk-1) var(--sk-2);
    border-radius: 999px;
    color: var(--sk-text);
    transition: background var(--sk-ease);
}
.sk-app .sk-account:hover { background: var(--sk-surface-sunk); text-decoration: none; }
/* MainLogo's gradient stops. Driven from the brand tokens rather than hardcoded in the SVG:
   stop-color as a presentation attribute does not accept var(), so it has to be set through CSS
   for the mark to follow a brand change. */
.sk-logo-stop-a { stop-color: var(--sk-logo-deep); }
.sk-logo-stop-b { stop-color: var(--sk-logo-lite); }

.sk-avatar {
    display: inline-flex; align-items: center; justify-content: center;
    width: 2rem; height: 2rem; flex: none;
    border-radius: 50%;
    background: linear-gradient(135deg, var(--sk-brand-deep), var(--sk-brand-lite));
    color: #fff; font-size: .8rem;
}
.sk-account__name { font-size: .875rem; font-weight: 500; }

/* Account dropdown. Shell chrome, so it is tokenised for BOTH themes rather than patched for
   dark - it lives in the top bar, outside <main>, which is why the dark patches lower down do
   not reach it.

   Bootstrap gives .dropdown-menu a white background and .dropdown-item a light hover; MDB then
   layers on a rule that forces color:#fff !important on item hover (the same declaration block
   that colours .side-nav and .white-text). Beating that needs matching specificity AND
   !important - which is why only the hover/focus rule carries one. */
.sk-app .dropdown-menu {
    background-color: var(--sk-surface);
    border: 1px solid var(--sk-border);
    border-radius: var(--sk-radius);
    box-shadow: 0 6px 16px rgba(0, 0, 0, .18);
    padding: var(--sk-1) 0;
    min-width: 12rem;
    color: var(--sk-text);
}
.sk-app .dropdown-item {
    color: var(--sk-text);
    padding: var(--sk-2) var(--sk-4);
    font-size: .875rem;
}
.sk-app .dropdown .dropdown-menu .dropdown-item:hover,
.sk-app .dropdown .dropdown-menu .dropdown-item:focus,
.sk-app .dropdown-item:hover,
.sk-app .dropdown-item:focus {
    background-color: var(--sk-surface-sunk) !important;
    color: var(--sk-text) !important;
}
.sk-app .dropdown-header { color: var(--sk-text-muted); font-size: .75rem; }
.sk-app .dropdown-divider { border-top: 1px solid var(--sk-border); margin: var(--sk-1) 0; }

/* ---------- content ------------------------------------------------------ */
.sk-main {
    grid-area: main;
    /* Side padding is deliberately modest: AppPage renders a Bootstrap .container-fluid that
       adds another 15px of its own, so the real gutter is this plus 15.

       The safe-area insets are IN the shorthand rather than in a separate padding-bottom
       declaration below it - a shorthand resets all four sides, so a longhand written above it is
       silently discarded. Bottom clears the home indicator, the sides clear the notch in
       landscape, and every env() is 0 on hardware without them. */
    padding:
        var(--sk-6)
        max(var(--sk-4), env(safe-area-inset-right))
        calc(var(--sk-10) + env(safe-area-inset-bottom))
        max(var(--sk-4), env(safe-area-inset-left));
    background: var(--sk-canvas);
    min-width: 0;            /* lets wide tables scroll instead of blowing out the grid */
}
/* No max-width.
   A 1440px cap is a reading-measure - right for prose, wrong for a CRM. Every screen the SOW
   calls for is data-dense: pipeline by stage and rep, quote line items, the approvals queue,
   close-rate tables, the report builder. Capping the width left large empty margins on a wide
   monitor and squeezed exactly the content that needs room. Salesforce, HubSpot and Dynamics
   are all effectively full-width for the same reason.

   If an ultrawide display ever makes a specific table unwieldy, that page can constrain itself -
   better than the shell deciding it for every screen. */
.sk-main__inner { width: 100%; }

/* Card chrome for the AppMain.Components cards rendered inside <main>. Flat borders
   instead of MDB's material shadows. Scoped under .sk-app so it cannot leak.

   Opaque, deliberately - the glass treatment is for chrome only. A table of holdings read
   through a blurred sheet is just a table that is harder to read. */
.sk-app main .card {
    border: 1px solid var(--sk-border);
    border-radius: var(--sk-radius);
    box-shadow: var(--sk-shadow);
    background: var(--sk-surface);
}

/* Rounding the card is not enough: its header and footer round their OWN outer corners and paint
   over the parent's.

     .card-header:first-child { border-radius: calc(.25rem - 1px) calc(.25rem - 1px) 0 0 }
     .card-footer:last-child  { border-radius: 0 0 calc(.25rem - 1px) calc(.25rem - 1px) }

   That is ~3px, which next to a 14px card reads as a square top - and it only shows on the header
   because AppCard emits `card-header bg-white`, so the header actually paints, while .card-body
   is transparent and lets the parent's rounded bottom through. That asymmetry is the whole bug:
   curved at the bottom, square at the top.

   `overflow: hidden` on the card would also fix it and must NOT be used - ListUsers renders the
   row ellipsis menu inside the card, and clipping the card would clip the open menu. Matching the
   children's radius to the parent's is the fix that leaves the overflow alone.

   The -1px matches Bootstrap's own arithmetic: the child sits inside a 1px border, so its radius
   is the parent's less the border width or the curves do not sit concentric. */
/* Generic on purpose: `> :first-child` / `> :last-child`, NOT a list of .card-header,
   .card-footer, .card-body, .app-card-footer.

   Enumerating the child types was tried and missed - AppCard used with no title, as ListUsers and
   ListInvitations do, renders .card-body as the ONLY child, so neither a header rule nor a footer
   rule reached it and the card stayed square.

   Why it showed only in DARK mode is the part worth keeping: in light mode .card-body is
   transparent and the parent's rounded corners show through it. The dark-mode patch further down
   paints it opaque (`.card-body { background-color: var(--sk-surface) !important }`) because MDB
   gives it a light fill - and the moment it paints, it paints square over the parent's curve.

   Rounding an element that has no background is a no-op, so applying this to every end child is
   harmless and cannot be outgrown by whatever AppCard renders next. */
.sk-app main .card > :first-child {
    border-top-left-radius:  calc(var(--sk-radius) - 1px);
    border-top-right-radius: calc(var(--sk-radius) - 1px);
}
.sk-app main .card > :last-child {
    border-bottom-left-radius:  calc(var(--sk-radius) - 1px);
    border-bottom-right-radius: calc(var(--sk-radius) - 1px);
}
/* AppListView renders list groups inside cards; their end items own the corners the same way. */
.sk-app main .card > .list-group:first-child .list-group-item:first-child {
    border-top-left-radius:  calc(var(--sk-radius) - 1px);
    border-top-right-radius: calc(var(--sk-radius) - 1px);
}
.sk-app main .card > .list-group:last-child .list-group-item:last-child {
    border-bottom-left-radius:  calc(var(--sk-radius) - 1px);
    border-bottom-right-radius: calc(var(--sk-radius) - 1px);
}

/* ---------- the filter drawer -------------------------------------------
   AppDomainFilter is an AppMain.Components <Modal ModalType="FluidRight">, which renders
   Bootstrap modal markup: .app-modal.modal.fade.right > .modal-dialog.modal-full-height
   .modal-right > .modal-content. So the sheet to style is .modal-content, and .modal-right on
   its parent is what distinguishes the side drawer from the centred dialogs (MediumCenter is
   used by ListInvitations) - without it these rules would round the corners off every modal in
   the app.

   Same material as the sidebar, one step more opaque: it carries form fields over live content
   and has to stay readable. */
.sk-app .modal-right .modal-content {
    /* Full height, because MDB only ever gave it a width.

       `.modal-full-height` sets `min-height: 100%` on the DIALOG, and MDB's only rule for the
       sheet inside it is `.modal-full-height .modal-content { width: 100% }` - width, no height.
       So the wrapper ran the height of the screen while the white panel stopped at the bottom of
       its own content, which is the drawer looking like a floating card rather than a side sheet.

       max-height has to be cleared too: Bootstrap caps .modal-content at calc(100vh - 1rem). */
    height: 100%;
    max-height: none;

    /* OPAQUE, AND NO backdrop-filter. This is the same call .sk-aside already made, for the same
       reason, and this surface is the worse offender: a full-height blurred element that is also
       TRANSFORM-ANIMATED as it slides in. The compositor has to promote it to its own layer and
       resample everything behind it mid-animation, and on the first frame of that it paints the
       whole viewport black. It reads as the entire application blinking.

       That was survivable while a drawer was something you opened occasionally - the filter, the
       roster. It is not, now that every row on every list opens one.

       Nothing is lost that anyone could see: the drawer sits over a list at 84% with a 40px blur,
       which is close enough to opaque that the difference was the flash and nothing else. The sheen,
       the edge and the shadow stay, so it still reads as a sheet rather than a slab. Glass now lives
       only where content genuinely scrolls underneath it - the top bar, and the mobile sidebar
       overlay. */
    background-color: var(--sk-surface);
    background-image: var(--sk-glass-sheen);
    border: 0;
    border-left: 1px solid var(--sk-border);
    box-shadow: var(--sk-glass-edge), var(--sk-glass-shadow);
}

/* The rounded sheet edge, and the reason it is behind a media query: below 640px the drawer is
   full-width, and a 28px radius on a panel with no page beside it reads as a rendering fault
   rather than as a sheet. */
@media (min-width: 640px) {
    .sk-app .modal-right .modal-content {
        border-radius: var(--sk-radius-sheet) 0 0 var(--sk-radius-sheet);
    }
}

/* Header and footer inherit the sheet rather than painting their own bands - Bootstrap gives
   them a white fill and #dee2e6 rules, which on glass show as two opaque stripes. */
.sk-app .modal-right .modal-header,
.sk-app .modal-right .modal-footer {
    background: transparent;
    border-color: var(--sk-border);
}
.sk-app .modal-right .modal-title { color: var(--sk-text); font-weight: 600; }

/* A footer with no buttons in it is not a footer. Modal always emits the element and only fills it
   conditionally, so SkDetailPanel - which switches both buttons off, because its actions live in
   the body and a second Close beside the header cross is one affordance too many - would otherwise
   end in an empty bordered band with the drawer's safe-area padding still in it.

   :not(:has(button)) rather than :empty: the element is never truly empty, Razor leaves whitespace
   text nodes inside it, and :empty does not match those. The other drawers all have buttons, so
   this reaches only the panels. */
.sk-app .modal-right .modal-footer:not(:has(button)) { display: none; }

/* Now that the sheet is full height, the BODY is what scrolls - so the title and the
   Apply/Cancel pair stay put instead of scrolling away with a long filter list. The footer also
   clears the home indicator. */
.sk-app .modal-right .modal-body { overflow-y: auto; flex: 1 1 auto; }
.sk-app .modal-right .modal-header,
.sk-app .modal-right .modal-footer { flex: 0 0 auto; }
.sk-app .modal-right .modal-footer { padding-bottom: calc(var(--sk-4) + env(safe-area-inset-bottom)); }

/* ---------- centred modals ------------------------------------------------
   AppModalType.MediumCenter renders `class="modal-dialog"` and nothing else - Bootstrap's
   vertical centring lives in a SEPARATE class, .modal-dialog-centered, which the package never
   applies. So "Center" in that enum name means horizontally only, and the dialog sits under the
   top edge with a .5rem margin while Telerik's .k-dialog centres itself. Two dialog systems, two
   different positions, in the same application.

   This is the .modal-dialog-centered rule, applied to the centred modal types. .modal-right is
   excluded: the filter drawer is a full-height sheet pinned to the edge and must not be centred.

   .modal-content already carries `max-height: calc(100vh - 1rem)`, so the usual objection to
   centring - a tall dialog outgrowing the viewport and losing its own header - does not apply. */
.sk-app .modal:not(.right) .modal-dialog {
    display: flex;
    align-items: center;
    min-height: calc(100% - 1rem);
}

/* And the corners. Only .modal-right was rounded when the drawer was done, so the centred dialogs
   kept Bootstrap's .3rem and sat out of family with the 28px drawer and the 14px cards. The
   sheet radius rather than the card radius, because this is a dialog, same as .k-dialog.

   No child-corner rule needed here: .modal-content is `overflow: hidden`, so it clips its own
   header and footer to the curve. The cards needed one because they are not. */
.sk-app .modal:not(.right) .modal-content {
    border-radius: var(--sk-radius-sheet);
    border: 1px solid var(--sk-border);
}

/* Bootstrap's backdrop is .5 of pure black and, with a blurred sheet over it, reads as a dark
   slab. Matched to .sk-scrim and .k-overlay so all three dimmers in the app agree.

   ONLY the opacity is changed, and that is the whole point. Bootstrap keeps the element opaque
   black and animates opacity alone:

     .modal-backdrop      { background-color: #000 }
     .modal-backdrop.fade { opacity: 0 }
     .modal-backdrop.show { opacity: .5 }

   An earlier version of this rule set `opacity: 1` with `background-color: rgba(0,0,0,.4)` on the
   .show state. That looked identical while open and flashed the entire screen black on close:
   removing .show reverts background-color to opaque #000 while opacity is still 1, and only then
   does the fade start. Never move the transparency of this element between the two properties -
   keep it in opacity, where the transition already lives.

   THE BASE STATE IS PINNED TO 0 for the same failure in the other direction, on OPEN. Bootstrap
   sets no opacity on a bare `.modal-backdrop` - the transparency comes from `.fade`, which is added
   only for an animated modal - so any frame where the element is in the DOM as plain
   `.modal-backdrop` paints opaque #000 across the whole viewport. That is one frame of the entire
   application going black, and it is exactly the bug above wearing the opposite sign. Stating the
   base explicitly means the element can never be visible without `.show`, whatever order the
   classes arrive in. */
.modal-backdrop { opacity: 0; }
.modal-backdrop.show { opacity: .4; }

/* Side drawers dim far less, and that is the difference between the two shapes rather than a
   preference. A centred dialog is a decision - the invite form, the role picker, a confirmation -
   and the page behind it genuinely is inert, so .4 is honest. A right-hand drawer is a working
   surface you use AGAINST the list behind it: the filter one is described in its own file as "a
   side panel you adjust against the list behind it", and the roster drawer was given a drawer
   rather than a dialog on exactly that reasoning. At .4 you cannot read that list, so both were
   claiming one thing and doing another.

   :has() because the backdrop is a SIBLING of the modal, appended to body - there is no selector
   path from one to the other, so the condition has to be asked of their shared parent. Supported
   everywhere this app runs; a browser without it simply keeps the .4 above, which is the safe way
   to lose.

   Opacity only, never background-color - see the note above this rule, which is load-bearing. */
body:has(.modal.show .modal-right) .modal-backdrop.show { opacity: .15; }

/* ---------- Telerik containers that should not be boxes ------------------
   A square box drawn around a rounded one is the most visible way this shell goes wrong, and
   both offenders here are containers that have no business drawing a box at all.

   The tab panel wraps the AppCard on the Admin screen. Telerik pins every one of its corners:

     .k-tabstrip-content { border-top-left-radius: 0px; border-top-right-radius: 0px; ... }
     .k-tabstrip-content { border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; ... }

   so it can never follow --sk-radius. Rounding it would only trade one nested box for two, which
   is the actual flaw - the card inside already carries the surface, the border and the shadow.
   The panel is a region. It gets no border and no fill, and the square disappears with it.

   Same argument for the pager. .k-pager sets `border-width: 1px` and a --kendo-color-surface-alt
   fill, so the list's footer drew a bordered rectangle with square corners INSIDE the 14px card.
   app-list-view.scss anticipated this and flattened `.k-pager-wrap`, but that is the old class
   name and Telerik renders .k-pager now, so the rule has been dead for some time.              */
.sk-app .k-tabstrip-content,
.sk-app .k-tabstrip > .k-content,
.sk-app .k-pager,
.sk-app .k-pager-wrap {
    border-width: 0 !important;
    background-color: transparent !important;
}

/* ---------- AppTabStrip as an iOS segmented control ----------------------
   Telerik renders a Material tab bar: UPPERCASE labels from

     .k-tabstrip-items { text-transform: uppercase }

   and an active indicator drawn as a bottom border on `.k-item.k-active::after`, tinted with
   --kendo-color-primary - which the colour bridge points at --sk-accent, so it came out as the
   red underline. Same shouting the MDB buttons had, and it was the one place left doing it.

   This converts the bar into a segmented control: a sunken pill track with a raised pill on the
   selected segment. It is the right control for this screen - Admin has two tabs live and three
   commented out at Admin.razor:32-34, so five segments at most, well inside where a segmented
   control works and where a tab bar would be the wrong choice.

   No markup change. The one thing that cannot be reproduced is the SLIDE: MudBlazor has a
   .mud-tab-slider element to animate, Telerik has no equivalent, so the pill changes instantly.
   A background transition on the segment is the closest honest approximation.

   The red does not vanish - it moves to the active segment's icon, exactly as it did in the
   sidebar nav, so the two selected-state treatments in the shell agree. */
.sk-app .k-tabstrip-items-wrapper { border-width: 0 !important; }

.sk-app .k-tabstrip-items {
    text-transform: none;
    letter-spacing: 0;
    display: inline-flex;
    flex: 0 0 auto;
    gap: 4px;
    padding: 4px;
    margin-bottom: var(--sk-4);
    background: var(--sk-segment-track);
    border-radius: var(--sk-radius-pill);
}

/* Telerik stretches items with `flex: 1`. A segmented control is sized by its content, or the
   two segments would span the whole page. */
.sk-app .k-tabstrip-items .k-item {
    flex: 0 0 auto;
    border: 0 !important;
    margin: 0 !important;
    background: transparent;
}

.sk-app .k-tabstrip-items .k-link {
    padding: 6px var(--sk-4);
    border-radius: var(--sk-radius-pill);
    font-size: .9rem;
    font-weight: 500;
    color: var(--sk-text-muted);
    transition: background-color var(--sk-ease), color var(--sk-ease);
}
.sk-app .k-tabstrip-items .k-item:not(.k-active):hover .k-link {
    background: transparent;
    color: var(--sk-text);
}

.sk-app .k-tabstrip-items .k-item.k-active .k-link {
    background: var(--sk-segment-on);
    color: var(--sk-text);
    font-weight: 600;
    box-shadow: var(--sk-shadow);
}
/* The icon is where the brand lives now - see the sidebar nav for the same decision. */
.sk-app .k-tabstrip-items .k-item.k-active .k-link .k-icon,
.sk-app .k-tabstrip-items .k-item.k-active .k-link i { color: var(--sk-accent); }

/* Kill the Material underline. It is a ::after with a border tinted by --kendo-color-primary,
   and left alone it would draw a red bar under the pill. */
.sk-app .k-tabstrip-items-wrapper .k-item.k-active::after { display: none !important; }

/* ---------- Telerik inputs: undo the Material underline ------------------
   The single reason a square field sits next to a capsule button in this app. It is NOT Telerik's
   default - it is an override baked into the package's Kendo build:

     .k-input-solid, .k-picker-solid {
         border-bottom-right-radius: 0 !important;
         border-bottom-left-radius: 0 !important;
         border-width: 1px 0;
         border-top-color: rgba(0, 0, 0, 0) !important;
     }

   A Material filled text field: square bottom, no side borders, invisible top border. Because it
   carries !important, --kendo-border-radius-md reaches the top corners and stops - which is why
   the search box and the page-size picker read as square while everything the token actually
   reaches is round.

   The doubled class is deliberate. `.k-input-solid.k-input-solid` matches exactly the same
   elements but scores (0,2,0) against their (0,1,0), so this wins on specificity rather than on
   source order - and it keeps working for the pickers Telerik portals to the end of <body>,
   outside .sk-app, which a scoped selector would never reach. */
.k-input-solid.k-input-solid,
.k-picker-solid.k-picker-solid,
.k-input-flat.k-input-flat,
.k-picker-flat.k-picker-flat {
    border-radius: var(--sk-radius-sm) !important;
    border-width: 1px !important;
    border-color: var(--sk-border-input) !important;
}

/* ---------- Telerik dialogs ----------------------------------------------
   Deliberately NOT scoped to .sk-app. Telerik portals its dialogs and popups to the end of
   <body>, outside the app class - the same reason the .k-popup patches further down are on
   :root. Scoping these would make them apply to nothing.

   The red titlebar was ours, not Telerik's default. The package's rule is

     .k-dialog-titlebar { color: var(--kendo-color-on-primary); background-color: var(--kendo-color-primary) }

   and the colour bridge above points --kendo-color-primary at --sk-accent, so bridging the brand
   turned every dialog header into a solid Korzite block. A title is a title - it gets text
   colour, not a filled band. The brand still reaches the dialog through the Save button.        */
/* Opaque, no backdrop-filter - the same call as the drawer sheet above and .sk-aside before it.
   These are the confirmations and the role picker, so they open on top of the very interactions the
   drawer does, and a second blurred surface promoted mid-animation is a second chance at the same
   black frame. A dialog dims the page to .4 anyway; there was never much behind it to see. */
.k-dialog, .k-window {
    border-radius: var(--sk-radius-sheet);
    border: 1px solid var(--sk-border);
    background-color: var(--sk-surface);
    background-image: var(--sk-glass-sheen);
    box-shadow: var(--sk-glass-edge), var(--sk-glass-shadow);
}

/* Kill the filled band.

   Doubled class rather than a descendant selector, and that is the fix for the version below
   that did not work. `.k-dialog .k-dialog-titlebar` assumes the titlebar sits INSIDE the element
   carrying .k-dialog, and in this Telerik build it does not - the box rules above landed (the
   sheet is visibly 28px) while the titlebar rule matched nothing, which is only possible if the
   two are not in that ancestor relationship.

   `.k-dialog-titlebar.k-dialog-titlebar` matches exactly the same element, scores (0,2,0) against
   Telerik's (0,1,0), and depends on no DOM shape at all. Same trick as the input fields. */
.k-dialog-titlebar.k-dialog-titlebar,
.k-window-titlebar.k-window-titlebar {
    /* !important, and only on the background.

       The doubled class already out-specifies Telerik's .k-dialog-titlebar, and the proof is that
       the border-radius rule below it lands - the red band has the 28px top corners. The colour
       does not, so something at higher priority is still setting it and inference has cost two
       rounds already. This ends it. Everything else here wins on specificity alone. */
    background: var(--sk-surface) !important;
    color: var(--sk-text) !important;
    /* Centred and bordered to match the Bootstrap modal, which renders
       `modal-title w-100 font-weight-bold` inside a `modal-header text-center`. The two dialog
       systems should be indistinguishable to whoever is using the app. */
    justify-content: center;
    text-align: center;
    font-size: 1.25rem;
    font-weight: 700;
    border-bottom: 1px solid var(--sk-border);
    padding-block: var(--sk-4);
}

/* The heading INSIDE the title, not just the bar.

   font-weight on .k-window-titlebar is inherited, and inheritance loses to a rule set directly on
   the element - both dialogs render `<h4 class="mb-0">` inside <DialogTitle>, and Bootstrap's
   `h1,h2,h3,h4,h5,h6 { font-weight: 500 }` lands on that h4. So the bar was bold and the text was
   not. The Bootstrap modal never showed this because Modal.razor puts font-weight-bold directly
   on its own title element.

   Every heading level is covered rather than just h4: DialogTitle takes arbitrary markup, and the
   next dialog to use an h3 or an h5 should not have to rediscover this. */
.k-window-title.k-window-title,
.k-dialog-title.k-dialog-title,
.k-window-title h1, .k-window-title h2, .k-window-title h3,
.k-window-title h4, .k-window-title h5, .k-window-title h6 {
    font-size: 1.25rem;
    font-weight: 700;
    line-height: 1.3;
    color: var(--sk-text);
}

/* Body and footer, so the sheet is one colour rather than a grey slab under a white bar. Telerik
   fills the content with --kendo-color-surface, which is a step darker than --kendo-color-base. */
.k-dialog-content.k-dialog-content,
.k-window-content.k-window-content {
    background: var(--sk-surface) !important;
    color: var(--sk-text);
    padding: var(--sk-5);
}
.k-dialog-actions.k-dialog-actions,
.k-window-actions.k-window-actions {
    background: var(--sk-surface) !important;
    border-top: 1px solid var(--sk-border);
    padding: var(--sk-4);
    gap: var(--sk-2);
    justify-content: center;
}

/* Titlebar and actions each round their OWN outer corners at 4px and paint over the sheet -
   identical to the .card-header problem, and it shows the moment the parent radius goes past
   theirs. Both have to follow --sk-radius-sheet, less the 1px border so the curves stay
   concentric. */
.k-dialog-titlebar.k-dialog-titlebar,
.k-window-titlebar.k-window-titlebar {
    border-top-left-radius:  calc(var(--sk-radius-sheet) - 1px);
    border-top-right-radius: calc(var(--sk-radius-sheet) - 1px);
}
.k-dialog-actions.k-dialog-actions,
.k-window-actions.k-window-actions {
    border-bottom-left-radius:  calc(var(--sk-radius-sheet) - 1px);
    border-bottom-right-radius: calc(var(--sk-radius-sheet) - 1px);
    border-top-color: var(--sk-border);
}

/* Telerik's dimmer is #000 at .32. Matched to .sk-scrim and the Bootstrap backdrop so all three
   overlays in the app agree - a dialog and the filter drawer dimming the page by different
   amounts is the kind of thing you only notice once and then cannot stop noticing. */
.k-overlay { background-color: #000; opacity: .4; }

/* ---------- glass fallbacks ----------------------------------------------
   Both of these matter. Without backdrop blur, translucency alone smears page content through
   the chrome and the sidebar becomes unreadable; and prefers-reduced-transparency is a real
   accessibility setting that Windows exposes as "Transparency effects". Either way the surfaces
   go fully opaque, which is exactly what they were before this section existed. */
/* The drawer sheet and the Telerik dialogs are NOT in these two lists any more: they are
   unconditionally opaque now (see the notes on each), so naming them here would be a declaration
   that changes nothing and reads as though it does. Only the surfaces that are still glass need a
   way out of it. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .sk-aside, .sk-topbar {
        background-color: var(--sk-surface);
        background-image: none;
    }
}

@media (prefers-reduced-transparency: reduce) {
    .sk-aside, .sk-topbar {
        background-color: var(--sk-surface);
        background-image: none;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
}

/* ---------- responsive: sidebar becomes an overlay drawer ---------------- */
.sk-scrim { display: none; }

@media (max-width: 992px) {
    .sk-shell {
        grid-template-columns: 1fr;
        grid-template-areas: "header" "main";
    }
    /* The one place the rail's glass has real content behind it: here it is an overlay drawer
       sitting on the page rather than a column beside it. This is the ONLY place the sidebar
       gets backdrop-filter - on desktop it blurred a flat canvas and charged a full-viewport
       repaint for the privilege. Keeps the specular edge and adds the lift shadow, which
       --sk-shadow (a card shadow) was too slight to provide. */
    .sk-aside {
        position: fixed; inset: 0 auto 0 0; z-index: 40;
        width: var(--sk-sidebar);
        transform: translateX(-100%);
        transition: transform var(--sk-ease);
        background-color: var(--sk-surface);
        background-color: color-mix(in srgb, var(--sk-surface) var(--sk-glass-chrome), transparent);
        backdrop-filter: var(--sk-glass-blur);
        -webkit-backdrop-filter: var(--sk-glass-blur);
        box-shadow: var(--sk-glass-edge), var(--sk-glass-shadow);
        border-radius: 0 var(--sk-radius-sheet) var(--sk-radius-sheet) 0;
    }
    .sk-shell.is-open .sk-aside { transform: none; }
    .sk-shell.is-open .sk-scrim {
        display: block; position: fixed; inset: 0; z-index: 30;
        background: rgba(0, 0, 0, .4); border: 0;
    }
    .sk-navtoggle { display: inline-flex; }
    /* The rail is desktop-only. On mobile the sidebar is an overlay drawer that is either open
       or closed at full width, so data-nav is neutralised here rather than fighting it. */
    .sk-shell, :root[data-nav="rail"] .sk-shell { grid-template-columns: 1fr; }
    :root[data-nav="rail"] .sk-aside { width: var(--sk-sidebar); }
    :root[data-nav="rail"] .sk-nav__label {
        max-width: none; opacity: 1;
        position: static; width: auto; height: auto; margin: 0; clip: auto;
    }
    :root[data-nav="rail"] .sk-nav__link { gap: var(--sk-3); }
    :root[data-nav="rail"] .sk-nav__link,
    :root[data-nav="rail"] .sk-aside__brand { justify-content: flex-start; padding-inline: var(--sk-3); }
    :root[data-nav="rail"] .sk-asidetoggle__label { display: inline; }
    /* Same shorthand-resets-everything trap as the desktop rule: the insets have to be repeated
       here or this override drops them. */
    .sk-main {
        padding:
            var(--sk-5)
            max(var(--sk-4), env(safe-area-inset-right))
            calc(var(--sk-8) + env(safe-area-inset-bottom))
            max(var(--sk-4), env(safe-area-inset-left));
    }

    /* ---- content that does not fit a phone ------------------------------
       The shell above has been mobile-capable from the start; everything below is the content
       inside it, which had not been checked at 390px. See the responsive rule in CLAUDE.md. */

    /* The list toolbar. The search box is Width="18rem" from the markup - a Telerik parameter, so
       it becomes an inline style and cannot be made conditional from Razor without a breakpoint
       in C#. Overriding it here is the only place the two can be kept in step. Right-aligning
       wrapped items also reads as ragged once they stack, so the row stretches instead. */
    .sk-app .sk-listcard__tools { justify-content: stretch; }
    .sk-app .sk-listcard__tools > .k-textbox,
    .sk-app .sk-listcard__tools > .k-input { width: 100% !important; flex: 1 1 100%; }

    /* Row actions may wrap rather than run off the edge. Above the breakpoint they are one flex
       line and nowrap is what keeps them on the row's centre line. */
    .sk-app .sk-userrow__actions { white-space: normal; }

    /* The account control keeps its avatar and drops its name - at this width the top bar is
       carrying a nav toggle, an environment tag, a role switcher and an account menu. */
    .sk-account__name { display: none; }
}

/* ---------- the segmented control on a narrow screen ----------------------
   .k-tabstrip-items is inline-flex and does not wrap, deliberately - a segmented control that
   wraps onto two lines stops reading as one control. So it scrolls instead. Admin has two tabs
   live and three commented out at Admin.razor:32-34; five segments do not fit 390px. */
.sk-app .k-tabstrip-items-wrapper {
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.sk-app .k-tabstrip-items-wrapper::-webkit-scrollbar { display: none; }

/* ---------- AppListView on a phone ----------------------------------------
   The package already renders the mobile labels: AppListViewItem turns Label="Name" into
   `<span class="d-md-none font-weight-bold pr-1">Name</span>`, which is the same markup LSNA
   ServiceConnect writes by hand as `<span class="d-md-none fw-bold">`. That half was never the
   problem.

   What was missing is that the cells never STACK. A row is `<div class="row no-gutters">` and the
   cells carry `col-md-3` and nothing else - and `.col-md-3 { flex: 0 0 25% }` only applies from
   768px up. Below that the cells have no col class at all, so inside a flex row they fall back to
   `flex: 0 1 auto`, sit shoulder to shoulder on one line and wrap mid-value. Giving them a
   full-width basis below the same breakpoint is the whole fix.

   767.98px, not 768px, to match Bootstrap's own max-width edge - at exactly 768 both this and
   .col-md-3 would otherwise apply. */
@media (max-width: 767.98px) {
    .sk-app .app-list-view-row-item > .row > [class*="col-"] {
        flex: 0 0 100%;
        max-width: 100%;
    }

    /* The header is redundant once every cell carries its own label, and it is the thing that
       looks most broken stacked - five column titles in a vertical list above the data. */
    .sk-app .app-list-view-header-item-list { display: none; }

    /* Each row becomes a block rather than a table line, so it needs its own rhythm: the 3rem
       min-height was sized for one line of cells side by side. */
    .sk-app .app-list-view-row-item > .row {
        align-items: flex-start;
        min-height: 0;
        padding: var(--sk-3) 0;
        row-gap: var(--sk-1);
    }

    /* Actions stop being a right-aligned column and become the last line of the block. */
    .sk-app .sk-userrow__actions {
        display: flex;
        justify-content: flex-start;
        gap: var(--sk-2);
        margin-top: var(--sk-2);
    }

    /* The pager wraps rather than pushing the card wider than the screen. */
    .sk-app .k-pager { flex-wrap: wrap; row-gap: var(--sk-2); }
}

/* The side drawer is pinned to the screen edges, so the modal itself must never be padded.

   Bootstrap's _adjustDialog() writes padding-left/padding-right as an INLINE style on the .modal
   element whenever it decides the dialog overflows the viewport:

     if (!isBodyOverflowing && isModalOverflowing) element.style.paddingLeft  = scrollbarWidth
     if (isBodyOverflowing && !isModalOverflowing) element.style.paddingRight = scrollbarWidth

   That logic is written for a centred dialog that should sit inside the scrollbar gutter. Making
   this sheet full height is precisely what tips scrollHeight past clientHeight, so it started
   padding - and a sheet pinned to `right: 0` then floats a scrollbar's width off both edges.

   !important because it is fighting an inline style, which nothing else can outrank. Scoped to
   .right so centred modals keep the compensation they actually want. */
.sk-app .modal.right {
    padding-left: 0 !important;
    padding-right: 0 !important;
}

/* ---------- the filter drawer on a narrow screen --------------------------
   MDB pins `.modal-full-height { width: 400px }`, which is wider than a 390px phone - the sheet
   ran off the right edge with no way to reach its Apply button. min() keeps the 400px sheet on a
   desktop and lets it shrink below that. */
.sk-app .modal-full-height {
    width: min(400px, 100vw);
    /* Bootstrap gives .modal-dialog `margin: .5rem`, and `1.75rem auto` from 576px up. MDB zeroes
       it on .modal-full-height at equal specificity and wins only because mdb.min.css happens to
       load after bootstrap.min.css (index.html lines 40 and 41). Restating it here means the
       drawer cannot pick up a side margin if that order ever changes - which is exactly the shape
       of a gap down each edge of the sheet. */
    margin: 0;
    max-width: none;
    /* height, not just min-height, and this is what makes the sheet inside it full height.

       MDB sets `height: auto; min-height: 100%` here. A percentage height on a child resolves
       against the parent's COMPUTED height, and against `auto` it resolves to auto - so the
       `height: 100%` on .modal-content was a no-op and the white panel kept stopping at the
       bottom of its content. Giving the dialog a definite height is what the child needs to
       resolve against. `.modal` is `position: fixed; height: 100%`, so 100% here is the viewport. */
    height: 100%;
}

/* Reduce motion, do not remove every transition.
   The guidance targets MOVEMENT - slides, parallax, zoom - because that is what provokes
   vestibular symptoms. A cross-fade is explicitly acceptable. So width, gap and position
   changes snap, while opacity still eases: the result is calm rather than jarring.

   Note for anyone debugging "the animation does not work": Windows reports this whenever
   Settings > Accessibility > Visual effects > Animation effects is off, and then none of the
   width transitions above will run. That is correct behaviour, not a bug.                  */
@media (prefers-reduced-motion: reduce) {
    .sk-shell, .sk-aside, .sk-nav__link, .sk-iconbtn, .sk-skip, .sk-asidetoggle {
        transition-property: opacity, background-color, color;
    }
    /* The hover slide is movement, not a cross-fade, so it goes rather than just snapping. */
    .sk-nav__link:hover { padding-left: var(--sk-3); }
    /* Same for the button press - a scale is movement. The button keeps its colour change, so
       there is still feedback on activation. */
    .sk-btn:active,
    .sk-app .btn:not(.btn-link):active,
    .sk-app .k-button:active { transform: none; }
    .sk-nav__label { transition: opacity var(--sk-ease); max-width: none; }
    :root[data-nav="rail"] .sk-nav__label {
        position: absolute; width: 1px; height: 1px; margin: -1px;
        overflow: hidden; clip: rect(0, 0, 0, 0);
    }

    /* THE CROSS-FADE COMES BACK, and this is a bug fix rather than a preference.

       Bootstrap ships `@media (prefers-reduced-motion: reduce) { .fade { transition: none } }`,
       which removes the OPACITY transition, not just movement. That disagrees with the line at the
       top of this block - a cross-fade is not motion, which is why the shell keeps opacity here -
       and on the backdrop it is the difference between a dim and a black flash.

       .modal-backdrop is `background-color: #000` with the transparency living entirely in opacity.
       With the transition gone there is nothing easing it up from zero, so any frame in which the
       element is in the DOM without a class that lowers its opacity paints opaque black across the
       whole viewport, for a full frame, on every modal open AND close. With animations enabled the
       150ms fade hides that frame completely, which is why it only reproduces for someone who has
       turned Windows animations off - i.e. only for people using the accessibility setting.

       Shorter than the default 150ms, because the point of the setting is restraint, not absence.
       The base `.modal-backdrop { opacity: 0 }` further up is the belt to this pair of braces. */
    .sk-app .modal.fade,
    .modal-backdrop.fade { transition: opacity .12s linear; }

    /* The slide IS movement, so that genuinely goes: the drawer and the dialogs appear in place
       instead of travelling in from the edge. */
    .sk-app .modal.fade .modal-dialog { transition: none; transform: none; }
}


/* ---------- Telerik bridge ----------------------------------------------
   Telerik ships 286 --kendo-* custom properties, so its controls can be pointed at the same
   tokens as the shell instead of being overridden rule by rule. Without this, grids and pickers
   keep their own palette, radius and font and look foreign inside the new chrome.
   Scoped to .sk-app so nothing leaks if the shell is ever reused elsewhere.               */
.sk-app {
    --kendo-font-family: var(--sk-font);
    --kendo-border-radius-md: var(--sk-radius-sm);
    --kendo-border-radius-lg: var(--sk-radius);
    --kendo-color-app-surface: var(--sk-surface);
    --kendo-color-base-subtle: var(--sk-surface-sunk);
    --kendo-color-border: var(--sk-border);
    --kendo-color-border-alt: var(--sk-border-strong);
    --kendo-color-primary: var(--sk-accent-fill);
    --kendo-color-on-primary: var(--sk-accent-fill-cont);
}

/* ---------- AppMain.Components bridge ------------------------------------
   The package ships its OWN token block on :root - --appmain-primary: #1571B9, Enable's corporate
   blue - and paints with it directly: AppCard's header icon, Telerik dialog title bars, checked
   checkboxes and radios, active tab underlines, calendar selection, switch handles.

   Telerik was bridged above but this was not, which is why the AppCard icon stayed blue after the
   rebrand while the shell around it went red. Nothing here is a per-rule override; it just points
   the package's tokens at ours, so anything it draws follows a brand change automatically. The
   package also aliases --mdb-* to these, so Bootstrap/MDB utilities follow at the same time.

   Load order is what makes this work: appmain-components.min.css is line 44 of index.html and
   theme.css is line 54. Moving theme.css above it silently reverts all of this.

   --appmain-danger is remapped for the same reason the shell's danger was: the package default is
   #f44336, which against a red brand is a third red nobody can tell apart from the other two.

   Declared on :root, NOT on .sk-app like the Telerik bridge above - and that difference is
   load-bearing. The package also declares `--mdb-primary: var(--appmain-primary)` on :root, and
   a var() substitution resolves against the element the declaration sits on. Scoped to .sk-app
   (the body) our value would never reach that alias, so every MDB utility - .text-primary,
   .btn-primary, .bg-primary - would stay Enable blue while the icons went red. On :root we win
   by source order and the whole alias chain follows.                                        */
:root {
    --appmain-primary:          var(--sk-accent);
    --appmain-primary-dark:     var(--sk-accent);
    --appmain-primary-reduced:  var(--sk-brand-lite);
    --appmain-secondary:        var(--sk-brand-lite);
    --appmain-secondary-dark:   var(--sk-brand-lite);
    --appmain-danger:           var(--sk-danger);
    /* The package's name for on-primary: it pairs this with --appmain-primary backgrounds. It has
       to follow --sk-accent-cont or dark mode puts white text on the light coral accent. */
    --appmain-text-color-for-dark-background: var(--sk-accent-cont);
}

/* ---------- Bootstrap / MDB primary bridge -------------------------------
   The third blue. Bootstrap 4 and MDB predate CSS-variable theming and bake their primary
   straight into the rule - `.text-primary{color:#007bff!important}` in bootstrap.min.css (34
   occurrences) and #4285f4 in mdb.min.css (103). No token points at them, so the --appmain-*
   bridge above cannot reach them either, which is why the boot spinner stayed blue after the
   rebrand: it was `class="spinner-border text-primary"`, and .spinner-border draws its ring from
   currentColor.

   These are declared rules rather than tokens because there is nothing to remap - the only way
   to reach a hardcoded value is to out-specify it. Scoped to .sk-app for that specificity, and
   !important because the vendor rules carry it.

   Not exhaustive by design: this covers the utilities our own markup uses or plausibly will.
   Anything else Bootstrap paints #007bff stays blue until it actually appears on a page.       */
.sk-app .text-primary   { color: var(--sk-accent) !important; }
.sk-app .bg-primary     { background-color: var(--sk-accent) !important; }
.sk-app .border-primary { border-color: var(--sk-accent) !important; }
.sk-app .badge-primary  { background-color: var(--sk-accent) !important; color: var(--sk-accent-cont) !important; }
.sk-app .btn-primary {
    background-color: var(--sk-accent) !important;
    border-color: var(--sk-accent) !important;
    color: var(--sk-accent-cont) !important;
}
.sk-app .btn-outline-primary { color: var(--sk-accent) !important; border-color: var(--sk-accent) !important; }
.sk-app .btn-outline-primary:hover {
    background-color: var(--sk-accent) !important;
    color: var(--sk-accent-cont) !important;
}

/* ---------- app.min.css brand overrides ----------------------------------
   The fourth blue, and the one that actually kept the AppCard icon blue after the other three
   were bridged.

   AppMain.Components emits `.app-bg-text-light` for a card header icon, and the package styles it
   as `color: var(--appmain-primary)` - which the bridge above already fixes. But css/app.min.css
   redeclares the same class as `color:#1571b9!important`, loads AFTER the package (index.html
   line 47 vs 44), and carries !important. Our own stylesheet was overriding the themeable rule
   with a hardcoded one.

   Easy to miss twice: the class is named `-light`, not `-primary`, so it does not turn up when
   grepping for primary; and the family looks unused when checked against this repo's markup,
   because the markup that uses it lives inside the component package.

   app.min.css cannot be edited directly - it is Web Compiler output from the .scss sources and a
   hand edit is reverted on the next compile in VS. Overriding here is the established pattern.
   `-light` and `-dark` are Enable's brand blue and navy, NOT light/dark mode, so they map to the
   accent and the deep brand colour respectively.

   Success, warning, info and the greys are deliberately left alone - they are semantic and
   non-brand.                                                                                  */
.sk-app .app-bg-text-light,
.sk-app .app-bg-text-primary  { color: var(--sk-accent) !important; }
.sk-app .app-bg-text-dark     { color: var(--sk-brand-deep) !important; }
.sk-app .app-bg-text-secondary{ color: var(--sk-brand-lite) !important; }
.sk-app .app-bg-text-danger   { color: var(--sk-danger) !important; }

.sk-app .app-bg-light,
.sk-app .app-bg-primary       { background-color: var(--sk-accent) !important; color: var(--sk-accent-cont) !important; }
.sk-app .app-bg-lighter,
.sk-app .app-bg-secondary     { background-color: var(--sk-brand-lite) !important; color: var(--sk-accent-cont) !important; }
/* The palest tint in the family (#addaff). --sk-accent-soft is the equivalent wash on the brand,
   and it needs dark text rather than the white the blue carried. */
.sk-app .app-bg-lightest      { background-color: var(--sk-accent-soft) !important; color: var(--sk-text) !important; }
.sk-app .app-bg-dark          { background-color: var(--sk-brand-deep) !important; color: var(--sk-accent-cont) !important; }
.sk-app .app-bg-danger        { background-color: var(--sk-danger) !important; color: #fff !important; }

.sk-app .app-bg-border-light  { border-color: var(--sk-accent) !important; }
.sk-app .app-bg-border-dark   { border-color: var(--sk-brand-deep) !important; }

.sk-app .app-bg-button-light  { background-color: var(--sk-accent) !important; color: var(--sk-accent-cont) !important; }
.sk-app .app-bg-button-dark {
    background: linear-gradient(90deg, var(--sk-brand-deep) 0%, var(--sk-brand-lite) 100%) !important;
    color: var(--sk-accent-cont) !important;
}

/* ---------- boot UI ------------------------------------------------------
   The spinner in index.html, shown before the WASM runtime starts. It uses our own class rather
   than .text-primary so the boot screen does not depend on a Bootstrap utility keeping its
   meaning - the bridge above would cover it either way, but our own first paint should not be
   borrowing a vendor colour.                                                                  */
.sk-boot__spinner { color: var(--sk-accent); }
.sk-boot__label   { color: var(--sk-text-muted); }

/* ---------- page header --------------------------------------------------
   One header pattern for every page, so Leads/Quotes/Reports do not each invent one.     */
/* wrap, so a title and its actions stop competing for one line. .sk-page__actions is
   `margin-left: auto`, which on a narrow viewport squeezed the title rather than moving the
   buttons under it. */
.sk-page__head {
    display: flex; align-items: flex-start; gap: var(--sk-4);
    flex-wrap: wrap;
    margin-bottom: var(--sk-5);
}
.sk-page__titles { min-width: 0; }
.sk-page__title {
    display: flex; align-items: center; gap: var(--sk-3);
    font-size: 1.375rem; font-weight: 650; margin: 0; color: var(--sk-text);
}
.sk-page__title .sk-page__icon { color: var(--sk-text-muted); font-size: 1.1rem; }
.sk-page__sub { margin: var(--sk-1) 0 0; color: var(--sk-text-muted); font-size: .875rem; }
.sk-page__actions { margin-left: auto; display: flex; gap: var(--sk-2); flex: none; }

/* ---------- states: empty / error / loading -----------------------------  */
.sk-state {
    display: flex; flex-direction: column; align-items: center; text-align: center;
    padding: var(--sk-10) var(--sk-5);
    border: 1px dashed var(--sk-border-strong);
    border-radius: var(--sk-radius);
    background: var(--sk-surface);
}
.sk-state__icon {
    width: 3rem; height: 3rem; display: grid; place-items: center;
    border-radius: 50%; background: var(--sk-surface-sunk);
    color: var(--sk-text-muted); font-size: 1.25rem; margin-bottom: var(--sk-4);
}
.sk-state__title { font-size: 1rem; font-weight: 600; margin: 0 0 var(--sk-2); color: var(--sk-text); }
.sk-state__body { margin: 0; color: var(--sk-text-muted); font-size: .9rem; max-width: 46ch; }
.sk-state__actions { margin-top: var(--sk-5); display: flex; gap: var(--sk-2); }
/* Tokenised, so the pair of dark-mode override rules this used to need are gone - the tokens
   already carry the theme. */
.sk-state--error { border-style: solid; border-color: var(--sk-danger-border); background: var(--sk-danger-soft); }
.sk-state--error .sk-state__icon { background: var(--sk-danger-tint); color: var(--sk-danger); }

/* Content-shaped placeholders. Better perceived performance than a spinner on a WASM app
   that already pays a multi-megabyte first load. */
.sk-skel { display: flex; flex-direction: column; gap: var(--sk-3); }
/* The highlight is --sk-border-strong, NOT a raw ramp token. It used to be --sk-n200, which does
   not invert - a pale band that was already wrong in dark mode and would have been a white streak
   across a black canvas. Every colour in this gradient has to be a semantic one. */
.sk-skel__line {
    height: .75rem; border-radius: 999px;
    background: linear-gradient(90deg, var(--sk-surface-sunk) 25%, var(--sk-border-strong) 37%, var(--sk-surface-sunk) 63%);
    background-size: 400% 100%;
    animation: sk-shimmer 1.4s ease infinite;
}
@keyframes sk-shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) {
    .sk-skel__line { animation: none; background: var(--sk-surface-sunk); }
}

/* ---------- buttons used by the state components ------------------------ */
/* Capsules. The horizontal padding goes up with the radius and that is not cosmetic: at the old
   16px a pill's curve eats into the label's side bearing and the text looks crammed against the
   ends. 20px gives the cap room to be a cap. */
.sk-btn {
    display: inline-flex; align-items: center; gap: var(--sk-2);
    padding: var(--sk-2) var(--sk-5);
    border-radius: var(--sk-radius-pill); border: 1px solid transparent;
    font-size: .875rem; font-weight: 600; cursor: pointer;
    transition: background var(--sk-ease), border-color var(--sk-ease), transform .12s ease;
}
/* The press. One line, and it is most of what makes a button feel like a control rather than a
   rectangle that navigates. Dropped under prefers-reduced-motion with the other movement. */
.sk-btn:active { transform: scale(.97); }
/* The FILL pair, not the accent pair - see the tokens. In light mode they are the same colour, so
   this button does not change there. */
.sk-btn--primary { background: var(--sk-accent-fill); color: var(--sk-accent-fill-cont); border-color: var(--sk-accent-fill-edge); }
.sk-btn--primary:hover { filter: brightness(.94); color: var(--sk-accent-fill-cont); text-decoration: none; }

/* Hover LIGHTENS in dark mode. brightness(.94) is the right feedback on a light page, where the
   button darkens towards the text around it; on a dark one it walks the fill back towards the
   surface it has just been lifted off, so the control gets quieter when it should get louder. */
:root[data-theme="dark"] .sk-btn--primary:hover { filter: brightness(1.12); }
.sk-btn--ghost { background: var(--sk-surface); color: var(--sk-text); border-color: var(--sk-border-strong); }
.sk-btn--ghost:hover { background: var(--sk-surface-sunk); color: var(--sk-text); text-decoration: none; }
.sk-btn--danger { background: transparent; color: var(--sk-danger); border-color: var(--sk-danger); }
/* --sk-danger-cont, not #fff: in dark mode the fill is a pale coral and a white label on it is
   2.5:1. Same flip the primary button's label makes. */
.sk-btn--danger:hover { background: var(--sk-danger); color: var(--sk-danger-cont); text-decoration: none; }
.sk-btn:disabled { opacity: .55; cursor: not-allowed; }

/* Focus on a FILLED button. The ring is the accent and so is the button, so on its own it just
   makes the button look 2px bigger - this is the case the old ring's white gap existed to handle,
   and the inset line handles it without putting a gap around every other control in the app.

   Note the .sk-app prefix on all three: the blanket focus rule near the top of this file is
   `.sk-app button:focus-visible`, (0,3,0). A bare `.sk-btn--primary:focus-visible` is (0,2,0) and
   quietly loses to it - the ring would still be drawn, just without the inset line. Every override
   of that rule needs the class as well, and to sit below it in the file. .btn-primary is here for
   the same reason: it is the fill AppMain.Components' Modal puts in its footer. */
.sk-app .sk-btn--primary:focus-visible,
.sk-app .btn-primary:focus-visible { box-shadow: var(--sk-ring-inner), var(--sk-ring); }

/* A destructive button rings in its OWN red, not the brand's. The two are deliberately 31.8 dE
   apart (see --sk-danger); a rose button that turns Korzite red when you tab to it reads as a
   different button rather than as the same button focused. */
.sk-app .sk-btn--danger:focus-visible {
    border-color: var(--sk-danger);
    box-shadow: 0 0 0 2px var(--sk-danger);
}

/* Compact variant for a button living inside a list row, where the standard height makes the
   rows taller than the text needs. Still a capsule - the radius is already larger than half the
   height, so 999px simply resolves to "fully round" at any size. */
.sk-btn--sm { padding: calc(var(--sk-1)) var(--sk-4); font-size: .8125rem; }

/* The reorder arrows on the Pipeline stages tab (WI 3301). Icon-only, so .sk-btn--sm's horizontal
   padding - sized for a word - would make two of them wider than the column that holds them. Square
   rather than merely narrower, because a capsule around a single glyph reads as a lozenge with an
   arrow lost in it. */
.sk-btn.sk-move { padding: 0; width: 1.75rem; height: 1.75rem; justify-content: center; }

/* The cell holding them. A list row is `row no-gutters`, so nothing separates the second arrow from
   the name beside it and the two read as one run of glyphs. The gap and the padding are the whole
   rule; the flex is what makes the gap apply. */
.sk-app .sk-rowreorder {
    display: inline-flex;
    align-items: center;
    gap: var(--sk-1);
    padding-right: var(--sk-4);
}

/* An arrangement made but not yet sent. Tinted rather than plain, because it is a state the screen
   is IN - the rows below it are not what the server holds - and the two buttons have to read as the
   way out of it rather than as more list chrome. */
.sk-pending {
    display: flex; align-items: center; justify-content: space-between;
    flex-wrap: wrap; gap: var(--sk-2);
    margin-bottom: var(--sk-3);
    padding: var(--sk-2) var(--sk-3);
    border: 1px solid var(--sk-border);
    border-radius: var(--sk-radius-sm);
    background: var(--sk-accent-soft);
    color: var(--sk-text);
    font-size: .875rem;
}
.sk-pending__actions { display: inline-flex; gap: var(--sk-2); flex: none; }


/* ---------- buttons the app does not own --------------------------------
   Four button implementations render in this app and only .sk-btn above is ours: Telerik's
   .k-button (dialog actions, pickers), MDBootstrap's .btn (what AppMain.Components' AppButton
   emits - including the Apply/Cancel pair in the filter drawer), and Bootstrap's own .btn. Left
   alone, the drawer would close on a capsule and a 6px rectangle sitting side by side.

   Telerik takes the token because its build is variable-driven; the other two bake the radius
   into the rule, so they have to be out-specified. Same split as the colour bridges above.

   .btn-link is excluded - it is a link wearing a button class, and a capsule outline around a
   text link is noise. Icon buttons are handled below as circles. */
.sk-app .btn:not(.btn-link),
.sk-app .k-button {
    border-radius: var(--sk-radius-pill);
}

/* MDB uppercases every button label and adds Material letter-spacing. That is the most dated
   thing left in the button, more than the corners were - Apple never shouts. */
.sk-app .btn {
    text-transform: none;
    letter-spacing: 0;
    font-weight: 600;
}

.sk-app .btn:not(.btn-link):active,
.sk-app .k-button:active { transform: scale(.97); }
.sk-app .btn, .sk-app .k-button { transition: transform .12s ease, background-color var(--sk-ease), border-color var(--sk-ease); }

/* ---------- dialog actions ------------------------------------------------
   Two dialog systems draw the footers in this app and they disagreed on every axis.

     TelerikDialog  - our own .sk-btn pair: ghost Cancel, then the confirm.
     Modal          - AppMain.Components, and its footer is fixed markup we do not own:
                      <AppButton Primary> then <AppButton Gray>, both ButtonSize.Large.

   So "Invite someone" put a large red Send on the LEFT of a solid grey Cancel, and "Change role"
   next to it put a small white Cancel on the left of a red Save. Same screen, same decision shape,
   two different dialogs.

   This block settles it in one place rather than per dialog, because Modal's footer cannot be
   changed from this repo and a per-caller fix would be re-litigated by the next dialog anyway:

     - the confirm is on the RIGHT and is a FILL,
     - the cancel is on the LEFT and is the ghost,
     - both are the same size, whichever component drew them.

   Order first. Modal renders confirm-then-cancel, so the footer is reversed - the DOM order is
   right for a screen reader (the primary action first) and row-reverse only moves the paint. */
.sk-app .modal-footer {
    flex-direction: row-reverse;
    gap: var(--sk-2);
    padding: var(--sk-4);
}
/* Bootstrap spaces footer buttons with `.modal-footer > * { margin: .25rem }`, which fights the
   gap above and leaves 8px on one dialog and 12px on the other. */
.sk-app .modal-footer > * { margin: 0; }

/* And the other half of the same disagreement: TelerikDialog defaults ButtonsLayout to Stretched,
   which is `.k-actions-stretched > * { flex: 1 0 0% }` - so Cancel and Save each took half the
   sheet while the Bootstrap footer sized its buttons to their labels. Fixed here rather than by
   passing ButtonsLayout on each TelerikDialog, so a dialog added later cannot forget it. */
.k-dialog-actions.k-actions-stretched > *,
.k-window-actions.k-actions-stretched > * { flex: 0 0 auto; }

/* Size. ButtonSize.Large is MDB's .btn-lg - 1rem/2.4rem padding at .9rem - against .sk-btn's
   8px/20px at .875rem, which is what made the invite dialog's buttons visibly heavier than the
   role dialog's. Out-specified rather than passed as a parameter: Modal hard-codes Large. */
.sk-app .modal-footer .btn:not(.btn-link) {
    padding: var(--sk-2) var(--sk-5);
    font-size: .875rem;
    line-height: 1.5;
}

/* Cancel. AppColorType.Gray is a solid grey slab from the package's own scoped stylesheet
   (`.btn-gray { background-color: var(--appmain-gray-dark) !important }`), and a filled grey
   button beside a filled red one reads as two actions of equal weight. The ghost is the same
   treatment TelerikDialog's Cancel already has. !important to beat the package's own. */
.sk-app .modal-footer .btn-gray {
    background-color: var(--sk-surface) !important;
    color: var(--sk-text);
    border: 1px solid var(--sk-border-strong);
}
.sk-app .modal-footer .btn-gray:hover { background-color: var(--sk-surface-sunk) !important; }

/* Confirm. There is no rule here, and that is the decision: a dialog's confirm button is
   .sk-btn--primary, the brand fill, whether it saves something or destroys it. ONE red.

   The obvious alternative was tried and rejected on sight - filling a destructive confirm with
   --sk-danger instead, which is what most design systems do. It does not work when the BRAND is
   red. Stripe, GitHub and Linear can afford a red Delete because their primary is blue or green;
   here #9f1239 sits next to #b5170c and the only thing it communicates is that two buttons were
   painted by two different people. A destructive signal that makes the reader stop and compare
   swatches is not a signal.

   So destructiveness is carried where it actually reads - by there BEING a confirmation at all, by
   the title ("Revoke this user's access?") and by the label naming the act ("Revoke access", never
   "Yes"). That is what SkConfirm was built around; the colour was never doing the work.

   --sk-danger keeps its real jobs: validation messages, the error state, and the rose OUTLINE on a
   row-level Reject/Revoke, where it sits beside a filled Approve and outline-versus-fill - not hue
   - is what tells them apart. It is never a fill on a dialog button. */

/* Icon-only controls are circles, not capsules - a capsule around a square glyph is an oval. */
.sk-iconbtn,
.sk-app .btn-floating,
.sk-app .k-icon-button { border-radius: 50%; }


/* ---------- sortable list headers ------------------------------------------
   AppListView gives paging and rows but no sorting - its Sortable branch renders nothing - so a
   sortable column is a plain button in the header item that sets the outgoing SortDescriptor.
   Styled to read as a header rather than as a control until you reach for it.                  */
.sk-sort {
    display: inline-flex; align-items: center; gap: var(--sk-1);
    padding: 0; border: 0; background: none;
    font: inherit; color: inherit; cursor: pointer;
}
.sk-sort:hover, .sk-sort--on { color: var(--sk-accent); }
.sk-sort:focus-visible { outline: 2px solid var(--sk-accent); outline-offset: 2px; border-radius: 2px; }
.sk-sort__icon { font-size: .75rem; opacity: .45; }
.sk-sort:hover .sk-sort__icon, .sk-sort--on .sk-sort__icon { opacity: 1; }


/* ---------- form validation ------------------------------------------------
   Blazor's <ValidationMessage> renders .validation-message and nothing in this app styles it -
   not Bootstrap, not MDB, not the Telerik theme - so an unstyled rule message reads as body text
   under the field. Styled here rather than per form, so every form that follows gets it: the repo
   validates front-end forms with FluentValidation (CLAUDE.md), and this is what those rules look
   like on screen.

   Same shape as LSNA ServiceConnect's create-user page - block, small, red, tight under the field,
   with a red asterisk on required labels - which is the look this repo is matching. The one change
   is the colour: LSNA hardcodes #dc3545, which is a light-theme red sitting on a dark surface the
   moment the theme flips. --sk-danger inverts with everything else.                              */
.validation-message {
    display: block;
    margin-top: var(--sk-1);
    font-size: .875em;
    color: var(--sk-danger);
}

/* The required marker is CSS, not markup: the label just carries the class, so a field cannot end
   up with an asterisk that no rule backs, or a rule with no asterisk. */
.sk-required::after {
    content: " *";
    color: var(--sk-danger);
    font-weight: 600;
}

/* Blazor stamps these on the input itself. The invalid state has to be visible without colour
   alone, which the message above provides - this is the supporting cue, not the whole signal.
   .k-invalid is Telerik's own; .invalid is what Blazor's field CSS adds. */
.k-input.invalid, .k-picker.invalid, .k-input.k-invalid, .k-picker.k-invalid,
input.invalid, select.invalid, textarea.invalid {
    border-color: var(--sk-danger) !important;
}


/* ---------- list rows and status chips -------------------------------------
   AppListView renders Bootstrap grid rows, which have no vertical rhythm of their own - every
   cell sits on the text baseline and a row containing a button ends up taller than one that does
   not. These give the rows a consistent height and align the cells to their middle.            */
.sk-userrow__name { font-weight: 500; }
.sk-userrow__email { color: var(--sk-text-muted); word-break: break-word; }
.sk-app .app-list-view-row-item > .row { align-items: center; min-height: 3rem; }
.sk-userrow__actions { white-space: nowrap; }

/* ---------- the list, restyled by subtraction ------------------------------
   app-list-view.scss paints three things that have to go. All three come from the Enable
   template, all three predate the design tokens, and none can be fixed at source: app.min.css is
   Web Compiler output that `dotnet build` does not regenerate, so a hand edit reverts on the next
   compile in VS. Overriding here is the established pattern (see the .app-bg-* block above).

   Note the selector shapes. The zebra rule is
     .app-list-view-row-stripe:nth-child(odd):not(:hover):not(hr)
   which is specificity (0,3,1) - three classes/pseudo-classes and one element, the `hr` inside
   the :not(). `.sk-app .app-list-view-row-stripe` is only (0,2,0) and loses silently, which is
   the trap the dark-mode patch had to work around. These mirror the vendor selector exactly and
   add .sk-app on the front, so they win on specificity rather than on source order.            */

/* 1. No zebra. A striped table is the most dated thing in this shell, and #f2f2f2 now sits a
      hair from the #f2f2f7 canvas, so the stripes read as muddy rather than as structure.
      Separation moves to a hairline per row - which is what InvestAdvisor gets from MudBlazor's
      default table and never had to write a rule for. */
.sk-app .app-list-view-row-stripe:nth-child(odd):not(:hover):not(hr),
.sk-app .app-list-view-row-stripe:nth-child(even):not(:hover):not(hr) {
    background-color: transparent;
}

.sk-app .app-list-view-row-item > .row {
    border-bottom: 1px solid var(--sk-border);
}
/* The card's own border closes the list; a separator under the last row doubles it. */
.sk-app .app-list-view-row-item:last-child > .row { border-bottom: 0; }

/* 2. The hover was #cdddea - a blue, in an application with no blue in it. The transition is
      InvestAdvisor's .15s, which is what stops the fill snapping on and reading as a flash. */
.sk-app .app-list-view-row:hover,
.sk-app .app-list-view-row-stripe:hover {
    background-color: var(--sk-surface-sunk);
    transition: background-color .15s ease;
}

/* 3. Selection was a #bfbfbf slab with !important, so beating it needs one too. A tint carries
      the same meaning without turning the row into a grey block. */
.sk-app .app-list-view-row .app-list-view-row-item-selected {
    background-color: var(--sk-accent-soft) !important;
}

/* The rows are not clickable - every action in them is a button - so the template's
   `cursor: pointer` on the row promises something that does not happen. */
.sk-app .app-list-view-row { cursor: default; }

/* Header: muted, 600, one hairline underneath instead of a rule per cell. The .sk-sort buttons
   inside it are `font: inherit; color: inherit`, so they follow this rather than needing their
   own declaration. */
.sk-app .app-list-view-header-item-list > .row {
    padding: var(--sk-2) 0;
    border-bottom: 1px solid var(--sk-border-strong);
    color: var(--sk-text-muted);
    font-size: .8125rem;
    font-weight: 600;
}

/* The pager sits on the card, not on a panel of its own. */
.sk-app .app-list-view-pager { background-color: transparent; }

/* Chips. Not Bootstrap badges: badge-warning is a solid yellow block that shouts louder than the
   row it is in, and its dark-mode colour is not one of the ones the package patches. */
.sk-chip {
    display: inline-flex; align-items: center;
    padding: .125rem var(--sk-2);
    border-radius: 999px; border: 1px solid transparent;
    font-size: .75rem; font-weight: 600; line-height: 1.5;
    white-space: nowrap;
}
/* Identity and role are not statuses - they stay quiet, tinted rather than filled, so the solid
   pills below are the only saturated thing in a row and actually mean something. */
.sk-chip--you { background: var(--sk-accent-soft); color: var(--sk-accent); margin-left: var(--sk-2); }
/* Sales rep, and the fallback for any role this map does not know yet - see the role-tint note in
   :root for why the common case stays neutral. */
.sk-chip--role { background: var(--sk-surface-sunk); color: var(--sk-text); border-color: var(--sk-border); }
.sk-chip--role-admin   { background: var(--sk-role-admin-soft);   color: var(--sk-role-admin); }
.sk-chip--role-manager { background: var(--sk-role-manager-soft); color: var(--sk-role-manager); }

/* Status pills: SOLID iOS fills carrying reversed text, the Stocks/Wallet treatment, rather than
   the soft tint with coloured text these used to be.

   The fills are the iOS system hues but NOT the headline values, and that is deliberate. White on
   systemGreen #34c759 measures 2.22:1 and on systemRed #ff383c 3.34:1 - both well under the 4.5:1
   this text needs at .75rem. Apple ships those anyway; we cannot. Each light fill is therefore the
   accessible variant of the same hue, which is the trade already made for --sk-text-muted: match
   iOS unless iOS fails WCAG.

   Dark mode flips the pairing rather than darkening further. On a #1c1c1e surface the bright
   system colours are the correct ones, and they carry BLACK text - the same light-fill/dark-label
   inversion --sk-accent-cont already does. */
.sk-chip--pending  { background: var(--sk-pill-orange); color: var(--sk-pill-on); }
.sk-chip--rejected { background: var(--sk-pill-red);    color: var(--sk-pill-on); }
/* Invitation states (WI 3471). Redeemed is a finished, good outcome; expired is spent rather than
   wrong, so it is grey rather than red - nothing went awry, the window simply closed. */
.sk-chip--ok    { background: var(--sk-pill-green); color: var(--sk-pill-on); }
.sk-chip--muted { background: var(--sk-pill-gray);  color: var(--sk-pill-on); }

/* ---------- list screens: no spinners ------------------------------------
   AppListView renders `<Load Element=AppElement.Component Visible=@(Loading || Data is null) />`,
   which is a TelerikLoaderContainer - a dimmed overlay with a bordered spinner - and it raises it
   on EVERY read. Not just the first: search, sort, page, and every refresh after a save. Switching
   Admin tabs made it obvious because that now refreshes the tab being opened, but it was flashing
   on all of those already.

   That is the opposite of how this application loads. A first read draws SkSkeleton in the shape of
   the content; a subsequent read keeps the rows on screen while the new ones arrive, which is why
   AppBaseListTab.Loading starts true and is only set again after an error. A spinner over rows that
   are already correct interrupts to say nothing.

   Hidden rather than argued with, because the markup belongs to a package we cannot edit - one
   block in theme.css, per CLAUDE.md. Scoped to .sk-list, which each list screen opts into on the
   wrapper it already has, so a Telerik loader anywhere else still works.

   The overlay is a SIBLING of the panel inside the container, so both go: leaving the overlay would
   dim the list for the same moment while showing nothing. */
.sk-app .sk-list .k-loader-container,
.sk-app .sk-list .k-loader-container-overlay { display: none !important; }

/* ---------- and the column headers the hidden spinner used to cover ------
   Switching to the Invitations tab flashed a bare Email / Role / State / Invited by header under
   the "No invitations yet" panel, for exactly as long as the refetch took.

   It is AppListView's own condition, and it is the block ABOVE that made it visible. The grid area
   is drawn whenever the list is loading, empty or not:

     <div class="@(Loading is false && Data is not null && Data.Count == 0 ? "d-none" : "")">

   so with no rows yet it renders the header over nothing, and only collapses to d-none once the
   read lands and Loading goes false. The package covered that moment with the TelerikLoaderContainer
   we hide directly above, which is why removing the spinner is what uncovered this.

   A tab switch is where it shows, because AppBaseListTab deliberately does NOT raise its own
   skeleton for one - a refresh keeps what is on screen, and on an empty list what is on screen is
   the empty state. So the fix is not a loading state. A column header over no rows is a table of
   nothing, and it should never render, loading or not.

   :has(~ …) rather than a Loading check, because the markup belongs to a package we cannot edit and
   the DOM already says it: the header and the rows are siblings inside that div, so "no row after
   me" IS the condition. Established here already - see the modal footer and .sk-detail__actions.

   :not(.row) is NOT decoration. AppListView gives its FOOTER the same
   app-list-view-header-item-list class (AppListView.razor:92) and renders it AFTER the rows, so
   "no row after me" is true of every footer that ever exists and this rule would hide it. The two
   differ in shape: the header wraps a `.row.no-gutters` child, the footer carries those classes on
   the element itself. Nothing uses AppListViewFooter today, which is exactly why the guard has to
   go in now - the day somebody adds one, a silently invisible footer is not a failure anybody would
   trace back to here. */
.sk-app .sk-list .app-list-view-header-item-list:not(.row):not(:has(~ .app-list-view-row-item)) {
    display: none;
}

/* ---------- team roster drawer (WI 3465) ---------------------------------
   A row that opens something has to say so before it is clicked. AppListView gives the row no
   affordance of its own when OnRowClick is wired, so the cursor and a hover tint are it - the same
   two signals a link gives, which is what this row now behaves like.

   Opted into by the SCREEN, not applied to .app-list-view-row-item globally: that class is on
   every row of every list, and the Users and Invitations rows do nothing when clicked. A pointer
   over a row that ignores you is worse than no affordance at all. */
.sk-app .sk-rows--clickable .app-list-view-row-item { cursor: pointer; }
.sk-app .sk-rows--clickable .app-list-view-row-item:hover { background: var(--sk-surface-sunk); }

/* ---------- detail panel (SkDetailPanel) --------------------------------
   The side panel a row opens, and since it replaced the row ellipsis menu, the only place most
   actions live. See the note at the top of SkDetailPanel.razor for why the kebab went.

   Every list now opts into sk-rows--clickable, which is why the rule above stopped being a Teams
   exception: a row that opens nothing was the reason it was opt-in, and there are none left. */
/* The sign that a row opens. Cursor and hover tint are the affordance on a pointer device, and
   neither exists on a touch screen - where these lists now hide their actions behind the row, so
   the chevron is the only thing saying there is anything to tap. Muted, and it brightens with the
   row, so it reads as part of the row rather than as a control of its own. */
.sk-app .sk-rowchevron { color: var(--sk-text-muted); font-size: .875rem; }
.sk-app .sk-rows--clickable .app-list-view-row-item:hover .sk-rowchevron { color: var(--sk-text); }

/* The way back out of a nested view. A left chevron and the word, aligned with the content rather
   than indented like a button - it is a place in the panel, not an action on the record, and the
   two must not look alike. Pulled up so it sits under the title rather than floating in the body. */
.sk-app .sk-detail__back {
    align-self: flex-start;
    display: inline-flex; align-items: center; gap: var(--sk-2);

    /* Negative on the LEFT only, and that one is deliberate: it cancels the button's own left
       padding so the chevron lines up with the content below rather than sitting a few pixels
       proud of it. There was a negative BOTTOM here too, pulling the subtitle up - against the
       subtitle's own negative top and the flex gap it came to minus four pixels, and the two
       lines touched. A control cannot tug at its neighbour when its neighbour is already
       tugging back. */
    margin-left: calc(var(--sk-2) * -1);

    /* Roomy enough to hit. var(--sk-1) top and bottom made a 28px target, and this is the primary
       way out of a nested view on a phone, where it is the only one - the header cross is a corner
       reach on a 390px sheet. min-height takes it to 44, which is the smallest target iOS
       considers reliable. */
    padding: var(--sk-2);
    min-height: 2.75rem;

    border: 0; background: none; border-radius: var(--sk-radius-sm);
    color: var(--sk-text-muted); font-size: .875rem; font-weight: 500;
    cursor: pointer;
}
.sk-app .sk-detail__back:hover { color: var(--sk-text); background: var(--sk-surface-sunk); }

/* THE ACTIONS ARE PINNED, and this pair of rules is how. Modal's own footer would have done it for
   free - it is what pins Apply/Cancel on the filter drawer - but that footer is a fixed
   Primary-then-Gray pair we cannot edit from this repo, which is the wrong shape for four different
   verbs. So the panel takes the body over: the body stops scrolling and stops padding, the CONTENT
   region scrolls inside it, and the actions sit outside that region on the sheet's bottom edge.

   Padding moves off the body and onto the two regions, which is what lets the actions' rule run the
   full width of the sheet the way the filter drawer's footer does. Inside the body's own padding it
   would have been an inset line that stopped short at both ends - a different thing that looks
   nearly the same, which is how two drawers stop matching. */
.sk-app .modal-right .modal-body:has(> .sk-detail) {
    display: flex; flex-direction: column;
    overflow: hidden;
    padding: 0;
}

.sk-detail { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }

/* min-height: 0 on both, or a flex item refuses to shrink below its content and the scrollbar
   appears on the page instead of in here. */
.sk-detail__content {
    display: flex; flex-direction: column; gap: var(--sk-3);
    flex: 1 1 auto; min-height: 0;
    overflow-y: auto;
    padding: var(--sk-4);
}

/* The line that tells two rows apart, directly under the Modal's title. Tight to it - they are one
   heading in two weights, not two paragraphs. */
.sk-detail__subtitle {
    margin: 0;
    color: var(--sk-text-muted);
    font-size: .875rem;
    word-break: break-word;
}

/* Tight to the Modal's title - but ONLY when it is the first thing in the body, which is to say on
   the record view. On a nested view the back control comes first, and this rule pulling upwards
   there is half of why the two lines collided. :first-child says exactly the condition the tug is
   for, which the unconditional version only appeared to. */
.sk-detail__subtitle:first-child { margin-top: calc(var(--sk-2) * -1); }

/* Label above value, not beside it. A two-column layout has to guess a label width, and it guesses
   wrong at 390px where the drawer is nearly the whole screen - the value then wraps to three lines
   beside a label with room to spare. Stacked reads the same at every width. */
.sk-detail__field { display: flex; flex-direction: column; gap: 2px; }

/* The rule goes on the SECOND and later fields, not as a bottom border on every one.
   A bottom border needs an exception for the last field, and :last-of-type could not express it:
   .sk-detail__actions is a div as well, so the last FIELD is never the last div, the exception
   never fired, and its bottom rule sat directly above the actions' top rule as a double line.
   :last-child would have been just as wrong, for the same reason.
   A top border on subsequent siblings has no last case to get wrong. */
.sk-detail__field + .sk-detail__field {
    padding-top: var(--sk-3);
    border-top: 1px solid var(--sk-border);
}

.sk-detail__label {
    font-size: .75rem; font-weight: 600; letter-spacing: .02em;
    text-transform: uppercase;
    color: var(--sk-text-muted);
}
.sk-detail__value { color: var(--sk-text); word-break: break-word; }
.sk-detail__value .text-muted { font-size: .8125rem; }

/* The actions, stacked and full width. NOT the dialog-footer shape: that block reconciles a
   cancel/confirm PAIR, and this is three to five different verbs, each of which has to be readable
   on its own. A row of five buttons wraps into an unreadable grid on a phone and gives no clue
   which is which; a stack is the same control at every width and puts the labels in a column the
   eye can run down.

   Separated from the facts above by a real rule that runs the full width of the sheet, because the
   panel changes from "what this is" to "what you can do to it" and nothing else marks that.

   flex: 0 0 auto and no margin: it is the sibling the scrolling region is measured against, so it
   keeps its height and stays on the bottom edge however long the content gets. The safe-area inset
   lands here now rather than on the content, because this is what an iPhone's home indicator would
   otherwise sit over. */
.sk-detail__actions {
    display: flex; flex-direction: column; gap: var(--sk-2);
    flex: 0 0 auto;
    padding: var(--sk-3) var(--sk-4) calc(var(--sk-4) + env(safe-area-inset-bottom));
    border-top: 1px solid var(--sk-border);
    background: var(--sk-surface);
}
.sk-app .sk-detail__actions .sk-btn { width: 100%; justify-content: center; }

/* An actions bar with nothing in it is not a bar. A screen can legitimately render the slot empty -
   an invitation that is already redeemed has nothing that can be done to it - and without this the
   panel ends in a bordered strip of padding that reads as something failing to load.
   :not(:has(*)) rather than :empty, because Razor leaves whitespace text nodes in there and :empty
   does not match those. The "you cannot change your own role" case keeps its bar: that is a <p>,
   so it has an element child and says something. */
.sk-app .sk-detail__actions:not(:has(*)) { display: none; }

/* One member per line: who, their role, and the way out. The name column takes the slack so the
   chip and the button stay put down the list rather than shifting with the longest name. */
.sk-roster__row {
    display: flex; align-items: center; gap: var(--sk-3);
    padding: var(--sk-2) 0;
    border-bottom: 1px solid var(--sk-border);
}
.sk-roster__row:last-child { border-bottom: 0; }
.sk-roster__who { flex: 1 1 auto; min-width: 0; }
/* min-width:0 so a long address ellipsises inside the flex item instead of pushing the button off
   the sheet - the drawer is narrow on a phone and this is where it would break first. */
.sk-roster__who .text-muted { overflow: hidden; text-overflow: ellipsis; }

/* Two-line picker options: the name, and under it the address that actually tells two people
   apart. Display names collide - two accounts for one person, or genuine namesakes - and a list of
   names alone offered identical rows with no way to choose. Used by both people-pickers. */
.sk-option { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
.sk-option__name { display: flex; align-items: center; gap: var(--sk-1); }
.sk-option__detail {
    font-size: .75rem; color: var(--sk-text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The CLOSED picker, where the two must sit on one line inside the control's own height - a
   stacked value would push the picker taller than every other field in the form. */
.sk-option__inline { display: inline-flex; align-items: baseline; gap: var(--sk-2); min-width: 0; }
.sk-option__inline .text-muted { font-size: .75rem; overflow: hidden; text-overflow: ellipsis; }

/* The picker and its Add button on one line, the button sized to its text. Wraps rather than
   overflows once the drawer is narrow. */
.sk-app .sk-roster__add { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sk-2); }
.sk-app .sk-roster__add > .k-picker { flex: 1 1 12rem; }

/* Search and filter, right-aligned above the list. In the card BODY rather than AppCard's own
   header, so the alignment is ours - the header version laid them out with float-right inside an
   h5 and they landed on different baselines. Wraps rather than overflows on a narrow viewport. */
.sk-app .sk-listcard__tools {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end;
    gap: var(--sk-3);
    margin-bottom: var(--sk-3);
}
.sk-app .sk-listcard__tools .badge { margin: 0; line-height: 1.5; }

/* The row ellipsis menu's styling lived here - about fifty lines undoing AppEllipsisMenu's
   float-right toggle, its absolutely-positioned TopRight placement and MDB's navbar-sized dropdown
   items. It is deleted rather than left in place because no list uses one any more: every row
   action moved into the panel the row opens (SkDetailPanel), and dead CSS that looks authoritative
   is the same trap as a duplicated declaration that does nothing.

   If a kebab is ever wanted again, read the note at the top of SkDetailPanel first - it is a
   decision that was made, not an absence. */

/* One flex line for everything in the actions cell, so the inline buttons and the chevron share a
   centre line and spacing comes from one place. Desktop only - below the breakpoint
   AppListViewItem shows its Label and the cell has to flow normally. */
@media (min-width: 768px) {
    .sk-app .sk-userrow__actions {
        display: flex; align-items: center; justify-content: flex-end;
        gap: var(--sk-2);
    }
}


/* ---------- dark mode for AppMain.Components ------------------------------
   The package renders MDBootstrap/Bootstrap markup with utility classes baked in - AppCard
   emits "card-header bg-white border-bottom-0" - and those utilities are !important, so no
   ordinary rule beats them. That is what left a white band across every card.

   This list is not guesswork. Every class the package actually emits was extracted from
   AppMain.Components.dll and cross-referenced against MDB, Bootstrap and app.min.css for
   hardcoded light colours; 58 matched, and the ones below are those that appear in real UI.

   Telerik IS patched here, below, and the note that used to sit in this spot saying it was not -
   "the package ships a complete dark theme and js/env.js swaps the <link>" - was wrong. In
   AppMain.Components 2026.4.8.1415 the two stylesheets are byte-identical:

     md5(telerik-blazor-light.css) == md5(telerik-blazor-dark.css) == 52a7345bac3a8bed4a8fec9a7967ad61

   so the swap is a no-op and every Telerik component stayed light in a dark shell. The swap is
   left in place - it costs nothing and starts working the day the package ships a real dark
   swatch - but it cannot be relied on today.

   Flat selectors, not CSS nesting: this file has no build step, and one invalid nested block
   would drop every rule inside it. Flat rules fail independently.

   !important appears only where the declaration being overridden is itself !important.

   All of this disappears when AppMain.Components drops its Bootstrap/MDB dependency - the item
   already sitting in Enable's backlog.                                                      */

/* surfaces: cards, modals, list groups */
:root[data-theme="dark"] .sk-app .card,
:root[data-theme="dark"] .sk-app .card-body,
:root[data-theme="dark"] .sk-app .card-header,
:root[data-theme="dark"] .sk-app .card-footer,
:root[data-theme="dark"] .sk-app .app-card-footer,
:root[data-theme="dark"] .sk-app .card-body-cascade,
:root[data-theme="dark"] .sk-app .modal-content,
:root[data-theme="dark"] .sk-app .list-group-item,
:root[data-theme="dark"] .sk-app .bg-white,
:root[data-theme="dark"] .sk-app .white {
    background-color: var(--sk-surface) !important;
    color: var(--sk-text);
}

:root[data-theme="dark"] .sk-app .card,
:root[data-theme="dark"] .sk-app .card-header,
:root[data-theme="dark"] .sk-app .card-footer,
:root[data-theme="dark"] .sk-app .modal-content,
/* The header and footer take their background from .modal-content, but Bootstrap gives them their
   own separator borders at #dee2e6 - two light-grey rules across a dark dialog. */
:root[data-theme="dark"] .sk-app .modal-header,
:root[data-theme="dark"] .sk-app .modal-footer,
:root[data-theme="dark"] .sk-app .list-group-item {
    border-color: var(--sk-border) !important;
}

/* sunken/secondary surfaces */
/* .app-list-view-row-stripe is deliberately NOT in this list any more - the zebra is gone in
   both themes now, and re-tinting the stripe here would bring it back in dark only. */
:root[data-theme="dark"] .sk-app .bg-light,
:root[data-theme="dark"] .sk-app .badge-light,
:root[data-theme="dark"] .sk-app .app-card-disabled,
:root[data-theme="dark"] .sk-app .avatar,
:root[data-theme="dark"] .sk-app .progress {
    background-color: var(--sk-surface-sunk) !important;
    color: var(--sk-text);
}

/* tables */
:root[data-theme="dark"] .sk-app .table { color: var(--sk-text); }
:root[data-theme="dark"] .sk-app .table thead th { border-bottom-color: var(--sk-border-strong); color: var(--sk-text-muted); }
:root[data-theme="dark"] .sk-app .table td,
:root[data-theme="dark"] .sk-app .table th { border-top-color: var(--sk-border); }
:root[data-theme="dark"] .sk-app .table-striped tbody tr:nth-of-type(odd) { background-color: var(--sk-surface-sunk); }
:root[data-theme="dark"] .sk-app .table-hover tbody tr:hover { background-color: var(--sk-surface-sunk); color: var(--sk-text); }

/* forms - md-form is MDB's, form-control is Bootstrap's */
:root[data-theme="dark"] .sk-app .form-control,
:root[data-theme="dark"] .sk-app .md-form .form-control,
:root[data-theme="dark"] .sk-app .md-textarea,
:root[data-theme="dark"] .sk-app .md-outline .form-control {
    background-color: var(--sk-surface-sunk) !important;
    color: var(--sk-text) !important;
    border-color: var(--sk-border-input);
}
:root[data-theme="dark"] .sk-app .form-control::placeholder { color: var(--sk-text-muted); }
:root[data-theme="dark"] .sk-app .md-form label { color: var(--sk-text-muted); }

/* buttons - only the neutral ones; branded buttons keep their colour */
:root[data-theme="dark"] .sk-app .btn-light,
:root[data-theme="dark"] .sk-app .btn-outline-secondary {
    background-color: var(--sk-surface-sunk) !important;
    color: var(--sk-text) !important;
    border-color: var(--sk-border-input);
}

/* text utilities */
:root[data-theme="dark"] .sk-app .text-muted { color: var(--sk-text-muted) !important; }
:root[data-theme="dark"] .sk-app .text-dark  { color: var(--sk-text) !important; }

/* misc */
:root[data-theme="dark"] .sk-app .close { color: var(--sk-text); text-shadow: none; }
:root[data-theme="dark"] .sk-app .collapsible-header { background-color: var(--sk-surface) !important; color: var(--sk-text); }
:root[data-theme="dark"] .sk-app hr { border-top-color: var(--sk-border); }


/* ---------- dark mode for Telerik -------------------------------------------
   The pager, the dropdowns, the dialogs, the tab strip: everything the package's Kendo build
   renders. See the note above for why the stylesheet swap does not do this - the "dark" file in
   the package is a byte-for-byte copy of the light one.

   This is nine variables, not a list of .k-* overrides, because that build is variable-driven:
   2,515 of its rules resolve a colour through var(--kendo-color-*). Redefining the palette at the
   root therefore re-themes every Telerik component at once, including ones no page uses yet -
   which is the difference between a fix and a game of whack-a-mole. Values map onto the sk
   palette so Telerik surfaces sit at the same depth as the shell around them.

   The fallbacks baked into the Kendo rules (var(--kendo-color-x, #fff)) only apply when the
   variable is unset, and it is set - so these win everywhere without !important.               */
:root[data-theme="dark"] {
    /* surfaces, back to front: app > surface > surface-alt */
    --kendo-color-app-surface: var(--sk-surface);
    --kendo-color-surface: var(--sk-surface-sunk);
    --kendo-color-surface-alt: var(--sk-surface);
    --kendo-color-on-app-surface: var(--sk-text);

    /* "base" is the neutral control colour: pager buttons, dropdown fields, tab headers */
    --kendo-color-base: var(--sk-surface);
    --kendo-color-base-subtle: var(--sk-surface-sunk);
    --kendo-color-base-hover: var(--sk-surface-sunk);
    --kendo-color-base-active: var(--sk-border);
    --kendo-color-base-emphasis: var(--sk-border-strong);
    --kendo-color-on-base: var(--sk-text);
    --kendo-color-base-on-surface: var(--sk-text);
    --kendo-color-base-on-subtle: var(--sk-text);

    --kendo-color-border: var(--sk-border);
    --kendo-color-border-alt: var(--sk-border-strong);
    --kendo-color-subtle: var(--sk-text-muted);
}

/* The brand half, on :root and NOT on the dark selector - this build's Kendo palette is blue in an
   application that has no blue in it, and it was showing in BOTH themes.

   Two families, because two different blues were involved and only one of them is obvious:

     --kendo-color-primary   #1571b9  focus rings, flat button text
     --kendo-color-secondary #93B5D2  the selected page number, and the highlighted row in the
                                      page-size list (.k-list-item.k-selected takes its COLOUR
                                      from secondary). Note the rest of that family is teal, so
                                      this one value was customised by whoever built the package -
                                      overriding the family keeps it internally consistent.

   These resolve through --sk-accent, which is itself redefined for dark, so one declaration
   follows the theme rather than needing a copy in each.                                        */
:root {
    --kendo-color-primary: var(--sk-accent);
    --kendo-color-primary-hover: var(--sk-accent);
    --kendo-color-primary-active: var(--sk-accent);
    --kendo-color-primary-emphasis: var(--sk-accent);
    --kendo-color-on-primary: var(--sk-accent-cont);
    --kendo-color-primary-subtle: var(--sk-accent-soft);
    --kendo-color-primary-on-subtle: var(--sk-accent);
    --kendo-color-primary-on-surface: var(--sk-accent);

    --kendo-color-secondary: var(--sk-accent);
    --kendo-color-secondary-hover: var(--sk-accent);
    --kendo-color-secondary-active: var(--sk-accent);
    --kendo-color-secondary-emphasis: var(--sk-accent);
    --kendo-color-on-secondary: var(--sk-accent-cont);
    --kendo-color-secondary-subtle: var(--sk-accent-soft);
    --kendo-color-secondary-subtle-active: var(--sk-accent-soft);
    --kendo-color-secondary-on-subtle: var(--sk-accent);
    --kendo-color-secondary-on-surface: var(--sk-accent);
}

/* The popup lists render in a portal at the end of <body>, OUTSIDE .sk-app - so anything scoped
   to .sk-app never reaches them. That is why these are on :root and not on the app class, and
   why a dropdown list looked correct until it was opened. */
:root[data-theme="dark"] .k-animation-container,
:root[data-theme="dark"] .k-popup,
:root[data-theme="dark"] .k-list-container {
    color: var(--sk-text);
}

/* AppListView needs no dark-mode chrome of its own any more.
   It used to need two rules: one to re-tint the zebra stripe for dark, and one to flatten the
   pager. The zebra is gone in both themes (see "the list, restyled by subtraction" above) and the
   pager is transparent in both, so both rules are now stated once, theme-agnostically, rather
   than patched here. The row colours themselves come from the Bootstrap markup underneath, which
   the surface patches above already cover. */

/* ---------- toasts ---------------------------------------------------------
   SweetAlert2's own look, which is what these were: a wide white sheet with its
   own radius, its own shadow and its own type scale, animated in from the corner.
   Next to the cards and drawers it reads as another product's notification, and
   in dark mode it stayed white - the library has no idea our themes exist.

   Restyled here rather than replaced. SweetAlert2 is loaded by index.html and its
   markup is not ours to edit, which is exactly the case CLAUDE.md says to settle
   with one block in theme.css. The alternative - our own toast component - would
   have meant re-solving stacking, timers and dismissal for no gain.

   Everything below resolves through --sk-* tokens, so dark mode needs no rules of
   its own: :root[data-theme="dark"] already redefines them. The container lands
   inside body.sk-app, so these selectors reach it.

   THE CONTAINER MUST NOT EAT CLICKS. SweetAlert2 lays a full-viewport element over
   the page for every popup, toast or not - and while a toast was up, nothing else
   in the application could be clicked at all. backdrop:false and heightAuto:false
   in showHtmlToast stop it behaving modally, but that element is still in the DOM,
   so the pointer-events pair below is what actually lets clicks through. The toast
   takes them back for itself, or its close button would not work either. */
.sk-app .swal2-container { pointer-events: none; background: transparent; }
.sk-app .swal2-toast { pointer-events: auto; }

/* TOP-CENTRE, not top-right. The filter and roster drawers are full-height panels
   pinned to the right edge, and the roster raises a toast on every add and remove -
   so at top-end the confirmation landed on top of the drawer that produced it,
   covering its title. Bottom is no better here: AppListView puts its pager there.
   Centre clears both, and is what Salesforce does on the same kind of screen.

   Offset below the top bar from the token rather than the 60px literal in
   vendor-swal2.scss - that rule names .swal2-top-end and .swal2-top-right, so it
   matches nothing now and this replaces it. env(safe-area-inset-*) is still missing
   app-wide - CLAUDE.md tracks it - but that fix belongs on the chrome, not here. */
.sk-app .swal2-container.swal2-top {
    padding-top: calc(var(--sk-topbar) + var(--sk-3));

    /* Centred explicitly rather than relying on where SweetAlert2's 3x3 grid resolves the "top"
       track to. It rendered visibly LEFT of centre, and the container is position:fixed, so the
       viewport is the only thing it should be measuring against. justify-items centres the toast
       inside its grid area, justify-content centres the track inside the container, and the
       symmetric inline padding neutralises any one-sided padding the library applies - between
       them the outcome no longer depends on which of those was to blame.
       Left-and-right stated together so the pair cannot drift apart later. */
    justify-items: center;
    justify-content: center;
    padding-left: var(--sk-3);
    padding-right: var(--sk-3);
}

/* !important on the two that decide legibility, and body.sk-app to out-specify anything the
   library has. It rendered as a WHITE sheet in dark mode with the message invisible on it - the
   colour rule landed and the background did not, which is the signature of losing a same-specificity
   race rather than a token being wrong (--sk-surface is correct in both themes).
   SweetAlert2 injects its stylesheet at runtime, so its position in the cascade is not ours to
   control and cannot be reasoned about from this file - the same situation as the Kendo wrappers,
   and CLAUDE.md settles it the same way. A toast that is unreadable in one theme is worse than an
   !important. */
body.sk-app .swal2-popup.swal2-toast {
    background: var(--sk-surface) !important;
    color: var(--sk-text) !important;

    /* THE SHADOW, twice corrected. SweetAlert2's own rule is, verbatim:
           .swal2-toast { background:#fff; box-shadow: 0 0 .625em #d9d9d9 }
       A 10px LIGHT GREY shadow at zero offset and zero spread - not a drop shadow at all, a halo,
       and on a dark surface it read as the toast glowing.

       The first replacement was --sk-glass-shadow, the drawer's. That fixed the colour and the
       direction and brought back the halo by another route: 32px of blur under only 12px of offset
       reaches ~20px past every edge INCLUDING the top, which on a full-height drawer is depth and
       on a small floating chip is a grey cloud around it. Elevation does not transfer between
       things of different size, so the toast gets its own: blur close to twice the offset, so the
       shadow sits UNDER the toast rather than around it, and lighter, because a toast is a
       notification and not a sheet lifted off the page. Black, so it still vanishes in dark mode
       where the border does the separating. Not a token - nothing else in the app is this shape. */
    box-shadow: 0 6px 16px rgba(0, 0, 0, .10), 0 2px 4px rgba(0, 0, 0, .06) !important;

    /* SIZED BY THE MESSAGE. .swal2-popup is `width: 32em` - a flat 512px a toast inherits - so
       "Saved." occupied exactly as much of the screen as a three-line refusal, and a long message
       wrapped early inside a box with room to spare. auto lets it shrink to the sentence and grow
       to the cap, which is what makes the wrapping match the width.

       Here with !important rather than in the plain block below, because width is now load-bearing
       for legibility and the library's rule is same-specificity - the background above lost exactly
       that race, and SweetAlert2 injects its stylesheet at runtime so cascade order is not ours to
       reason about. The floor stops a two-word toast rendering as a stub around its icon; the cap
       is 34rem so a sentence naming a person and a team fits on two lines instead of six. */
    width: auto !important;
    min-width: min(92vw, 17rem) !important;
    max-width: min(92vw, 34rem) !important;
}

/* Background, colour, box-shadow and the three widths are deliberately NOT here - they are in the
   !important block above, which is the one that actually reaches the element.
   Stating them twice would leave a copy that looks authoritative and does nothing,
   and the next person would edit the wrong one. */
.sk-app .swal2-toast {
    border: 1px solid var(--sk-border);
    border-radius: var(--sk-radius);
    padding: var(--sk-3) var(--sk-4);
}

/* The message itself carries the colour too. It sits in its own element, so the popup rule above
   does not reach it if the library colours it directly - which is how the text ended up white on
   white rather than merely low-contrast. */
body.sk-app .swal2-toast .swal2-title,
body.sk-app .swal2-toast .swal2-html-container {
    color: var(--sk-text) !important;
}

.sk-app .swal2-toast .swal2-title,
.sk-app .swal2-toast .swal2-html-container {
    color: var(--sk-text);
    font-size: .875rem;
    font-weight: 500;
    line-height: 1.45;
    margin: 0;
    text-align: left;
}

/* The library sizes its toast icon for its own type scale and then centres the
   text against it. Ours is a 1rem line, so the icon shrinks to match and stops
   the label sitting low. */
.sk-app .swal2-toast .swal2-icon {
    width: 1.5rem; height: 1.5rem;
    min-width: 1.5rem;
    margin: 0 var(--sk-3) 0 0;
    border-width: 2px;
}
.sk-app .swal2-toast .swal2-icon .swal2-icon-content { font-size: 1rem; line-height: 1.35; }

/* Semantic colours, from the same tokens the error bar and validation messages
   use - so a failed save is the same red wherever it is reported. The library
   paints each icon's ring and strokes separately, hence one rule per part. */
.sk-app .swal2-toast .swal2-icon.swal2-success {
    border-color: var(--sk-ok); color: var(--sk-ok);
}
.sk-app .swal2-toast .swal2-icon.swal2-success .swal2-success-ring { border-color: var(--sk-ok); }
.sk-app .swal2-toast .swal2-icon.swal2-success [class^='swal2-success-line'] { background-color: var(--sk-ok); }

.sk-app .swal2-toast .swal2-icon.swal2-error {
    border-color: var(--sk-danger); color: var(--sk-danger);
}
.sk-app .swal2-toast .swal2-icon.swal2-error [class^='swal2-x-mark-line'] { background-color: var(--sk-danger); }

.sk-app .swal2-toast .swal2-icon.swal2-warning {
    border-color: var(--sk-warn); color: var(--sk-warn);
}
/* Info is the accent rather than a fourth semantic colour: it carries no verdict,
   it is the application talking. */
.sk-app .swal2-toast .swal2-icon.swal2-info,
.sk-app .swal2-toast .swal2-icon.swal2-question {
    border-color: var(--sk-accent); color: var(--sk-accent);
}

/* The timer bar reads as a progress indicator for something still happening,
   which a toast is not. */
.sk-app .swal2-toast .swal2-timer-progress-bar-container { display: none; }

/* The close button, which only the toasts that do NOT time out are given - a
   warning or an error, where the message is the answer to something the user tried
   and has to survive being read. SweetAlert2's default is a large light-grey glyph
   sized for a full dialog; this is the muted-to-solid treatment the rest of the
   application's quiet controls use, and it takes the standard focus ring so it is
   reachable by keyboard - the whole point of having it. */
.sk-app .swal2-toast .swal2-close {
    width: 1.5rem; height: 1.5rem;
    margin: 0 0 0 var(--sk-2);
    align-self: flex-start;
    color: var(--sk-text-muted);
    font-size: 1.5rem; line-height: 1;
    border-radius: var(--sk-radius-sm);
    transition: color .12s ease;
}
.sk-app .swal2-toast .swal2-close:hover { color: var(--sk-text); background: transparent; }
.sk-app .swal2-toast .swal2-close:focus-visible { box-shadow: var(--sk-ring); outline: none; }

/* SweetAlert2's default entrance is a bounce with a scale overshoot. Nothing else
   here bounces; the drawers and dialogs slide and fade. */
@media (prefers-reduced-motion: no-preference) {
    .sk-app .swal2-toast.swal2-show { animation: sk-toast-in .18s ease-out; }
    .sk-app .swal2-toast.swal2-hide { animation: sk-toast-out .12s ease-in forwards; }
}
@keyframes sk-toast-in  { from { opacity: 0; transform: translateY(-.5rem); } to { opacity: 1; transform: none; } }
@keyframes sk-toast-out { to { opacity: 0; transform: translateY(-.25rem); } }
